DEV Community

Tomasz Wegrzanowski
Tomasz Wegrzanowski

Posted on

Electron Adventures: Episode 84: High Performance Hex Editor

In episodes 61-69 we created a hex editor, but it was fairly slow when dealing with big files.

So let's start with what we had in episode 69 and let's make it really fast.

Performance problem

Hex editor's performance story had two parts.

Initially, the app was creating DOM for every row, that made startup very slow, but after that it was very smooth as no more updates were needed.

After the change, app created empty placeholder DOM entry for every row, then whenever scrolling happened, it checked which rows needed to display data (on-screen), and which could stay empty (off-screen). Initial render was much faster, but still not amazing. And now scrolling was slow, as Svelte needed to figure out app needed to update.

New solution

Well, but why do we even bother creating placeholder elements? So here's the new idea - size up container to fit all the elements, then only create the ones we need. To simplify the implementation, I just forced every row to be 16px high.

src/Slice.svelte

<script>
  import { printf } from "fast-printf"
  import AsciiSlice from "./AsciiSlice.svelte"

  export let offset
  export let rowNumber
  export let data
</script>

<div class="row" style={`top: ${16*rowNumber}px`} class:even={rowNumber % 2}>
  <span class="offset">{printf("%06d", offset)}</span>
  <span class="hex">
    {#each {length: 16} as _, i}
      <span data-offset={offset + i}>
        {data[i] !== undefined ? printf("%02x", data[i]) : "  "}
      </span>
    {/each}
  </span>
  <AsciiSlice {data} />
</div>

<style>
  .row {
    position: absolute;
    width: 100%;
    height: 16px;
  }
  .even {
    background-color: #555;
  }
  .offset {
    margin-right: 0.75em;
  }
  .hex span:nth-child(4n) {
    margin-right: 0.75em;
  }
</style>
Enter fullscreen mode Exit fullscreen mode

We only needed to change a few things.

  • removed the whole if visible logic
  • every row gets rowNumber (which is always offset/16 right now, but it seems more logical to pass both)
  • the row is 16px and positioned absolutely based on rowNumber
  • we cannot rely on CSS to do even/odd logic, as we don't know if first actually visible element is odd or even, so we need to manage .even class ourselves

src/MainView.svelte

<script>
  import Slice from "./Slice.svelte"
  import { createEventDispatcher } from "svelte"

  export let data

  let dispatch = createEventDispatcher()
  let slices
  let main1
  let main2
  let firstVisible = 0
  let lastVisible = 200

  $: {
    slices = []
    for (let i=0; i<data.length; i+=16) {
      slices.push({
        rowNumber: i/16,
        offset: i,
        data: data.slice(i, i+16),
      })
    }
  }

  $: visibleSlices = slices.slice(firstVisible, lastVisible+1)
  $: totalHeight = `height: ${16*slices.length}px`

  function onmouseover(e) {
    if (!e.target.dataset.offset) {
      return
    }
    dispatch("changeoffset", e.target.dataset.offset)
  }

  function setVisible() {
    let rowHeight = 16
    firstVisible = Math.floor(main1.scrollTop / rowHeight)
    lastVisible = Math.ceil((main1.scrollTop + main1.clientHeight) / rowHeight)
    main2.focus()
  }

  function init1(node) {
    main1 = node
    setVisible()
  }
  function init2(node) {
    main2 = node
  }
</script>

<div
  class="main1"
  on:scroll={setVisible}
  use:init1
  >
  <div
    class="main2"
    on:mouseover={onmouseover}
    style={totalHeight}
    use:init2
    tabindex="-1"
  >
    {#each visibleSlices as slice (slice.offset)}
      <Slice {...slice} />
    {/each}
  </div>
</div>

<svelte:window on:resize={setVisible} />

<style>
  .main1 {
    flex: 1 1 auto;
    overflow-y: auto;
    width: 100%;
  }
  .main2 {
    position: relative;
  }
</style>
Enter fullscreen mode Exit fullscreen mode

This is possibly not the most tidy code, there's external main1 scrollable viewport div with size flexing to available space, and inner main2 div sized to fit all rows.

There's a few tricks here. We need to add tabindex="-1" on the inner main2 and keep running main2.focus() after every scroll, otherwise keyboard navigation wouldn't work. In previous version what was focused were individual rows, but now we delete them, and that would remove focus completely instead of moving it to main2. By forcing focus to stay on main2, keyboard navigation works. This isn't the most elegant solution, but nothing else is selectable, so it works. In more complex app, we should only steal focus if it belonged to a row that was about to be deleted.

When we iterate with {#each visibleSlices as slice (slice.offset)}, we need to tell Svelte to identify rows by slice.offset, instead of by loop index. Otherwise, we'd need to tell AsciiSlice components to recompute their data every time, instead of only on creation as it does now.

And of course we need to tag main2 as position: relative, to let the browser know that position: absolute of Slice components is based on main2, not on the main window.

Results

Here's the results:

Episode 84 Screenshot

In the next episode we'll write some games.

As usual, all the code for the episode is here.

Discussion (0)