Electron Adventures: Episode 68: Malina Hex Editor
Tags: javascript, electron, svelte
Published: 2021-09-29 09:04:59

Time to do something more substantial in Malina - and the obvious thing is port our hex editor from episode 66.

In this episode we'll run into a lot of issues with Malina, but that's pretty much expected when dealing with a new framework.

### `@rollup/plugin-commonjs`

Well, first we need to do some rollup config, my least favorite part of JavaScript.

```
$ npm i @rollup/plugin-commonjs
```

And edit the `rollup.config.js` file to support `commonjs()`:

```javascript
import resolve from '@rollup/plugin-node-resolve';
import derver from 'derver/rollup-plugin';
import css from 'rollup-plugin-css-only';
import { terser } from "rollup-plugin-terser";
import malina from 'malinajs/malina-rollup'
import malinaSass from 'malinajs/plugins/sass'
import commonjs from '@rollup/plugin-commonjs';

const DEV = !!process.env.ROLLUP_WATCH;
const cssInJS = false;

export default {
  input: 'src/main.js',
  output: {
    file: 'public/bundle.js',
    format: 'iife',
  },
  plugins: [
    malina({
      hideLabel: !DEV,
      css: cssInJS,
      plugins: [malinaSass()]
    }),
    resolve(),
    commonjs(),
    !cssInJS && css({ output: 'bundle.css' }),
    DEV && derver(),
    !DEV && terser()
  ],
  watch: {
    clearScreen: false
  }
}
```

There are multiple formats for npm packages, and bundlers need to be configured to support each particular format, and I really don't want to think about it, this should just work out of the box, but it doesn't.

### Install dependencies

Now we can actually install dependencies. They wouldn't work without `@rollup/plugin-commonjs`.

```shell
$ npm i fast-printf buffer
```

Now that this is out of the way let's get to the code.

### `src/StatusBar.xht`

This file is completely identical to `src/StatusBar.svelte` from episode 66.

```html
<script>
  import { printf } from "fast-printf"
  export let offset

  $: hexOffset = printf("%x", offset)
</script>

<div>
  Offset: {offset} ({hexOffset})
</div>

<style>
  div {
    margin-top: 8px;
  }
</style>
```

### `src/AsciiSlice.xht`

This file is also completely identical to `src/AsciiSlice.svelte` from episode 66. So far so good.

```html
<script>
  export let data

  let ascii = ""
  for (let d of data) {
    if (d >= 32 && d <= 126) {
      ascii += String.fromCharCode(d)
    } else {
      ascii += "\xB7"
    }
  }
</script>

<span class="ascii">{ascii}</span>

<style>
  .ascii {
    white-space: pre;
  }
</style>
```

### `src/Slice.xht`

In all the files we need to change `.xht` vs `.svelte` in imports, I won't be mentioning this any further.

There are however more differences from the Svelte version.

First, iterating some number of times. In Svelte if we want to iterate 16 times we can do `{#each {length: 16} as _, i}`. Malina does not support this, and we need to convert that to an array with `{#each Array.from({length: 16}) as _, i}`. To be honest both just need to add `{#range ...}` statement already, this is far too common use case. [This has been an open Svelte issue for over two years, Svelte creator supports it, so I have no idea why it's still not happening](https://github.com/sveltejs/svelte/issues/2968).

The other difference [is one of many bugs in Malina I discovered](https://github.com/malinajs/malinajs/issues/48). We'd like to do `{:else}&nbsp`, but HTML entities do not work properly in Malina in if/else blocks.

I tried a workaround with JavaScript string with `{:else}{"\xa0"}` but that didn't work either, I'm guessing due to Malina agressively collapsing whitespace.

So for placeholder it's just some arbitrary character we'll give `opacity: 0;` to.

As a reminder, we need such placeholder rows to have same height as regular rows for our dynamic rendering logic to figure out which rows should be visible. Episode 66 has all the details.

```html
<script>
  import { printf } from "fast-printf"
  import AsciiSlice from "./AsciiSlice.xht"

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

<div class="row">
  {#if visible}
    <span class="offset">{printf("%06d", offset)}</span>
    <span class="hex">
      {#each Array.from({length: 16}) as _, i}
        <span data-offset={offset + i}>
          {data[i] !== undefined ? printf("%02x", data[i]) : " "}
        </span>
      {/each}
    </span>
    <AsciiSlice {data} />
  {:else}
    <span class="invisible">.</span>
  {/if}
</div>

<style>
  .invisible {
    opacity: 0;
  }
  .row:nth-child(even) {
    background-color: #555;
  }
  .offset {
    margin-right: 0.75em;
  }
  .hex span:nth-child(4n) {
    margin-right: 0.75em;
  }
</style>
```

### `src/MainView.xht`

There's a lot of changes here:

```html
<script>
  import Slice from "./Slice.xht"

  export let data

  let slices
  let main
  let firstVisible = 0
  let lastVisible = 200

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

  $: firstVisible, lastVisible, console.log("Visible:", firstVisible, lastVisible)

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

  function setVisible() {
    let rowHeight = Math.max(10, main.scrollHeight / slices.length)
    firstVisible = Math.floor(main.scrollTop / rowHeight)
    lastVisible = Math.ceil((main.scrollTop + main.clientHeight) / rowHeight)
  }
</script>

<div
  class="main"
  on:mouseover={onmouseover}
  on:scroll={setVisible}
  #main
  use:setVisible
>
  {#each slices as slice, i}
    <Slice {...slice} visible={i >= firstVisible && i <= lastVisible} />
  {/each}
</div>

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

<style>
  .main {
    flex: 1 1 auto;
    overflow-y: auto;
    width: 100%;
  }
</style>
```

First the good changes.

`<svelte:window>` became `<malina:window>`.

And `#main` is a shortcut for setting `main` to refer to that DOM node, something that would be `use:{(node) => main = node}` in Svelte. Longer version would work as well, but I like this shortcut.

Malina has simpler interface for creating custom events. Instead of tedious boilerplate:

```javascript
import { createEventDispatcher } from "svelte"
let dispatch = createEventDispatcher()
dispatch("changeoffset", e.target.dataset.offset)
```

You can just do this with `$emit`:

```javascript
$emit("changeoffset", e.target.dataset.offset)
```

I find that quite often Svelte code looks really clean for the usual use cases, but then doing anything slightly nonstandard turns it into `import { ... } from "svelte"` followed by a block of boilerplate. Malina covers a lot of such cases with special variables like `$emit`, `$context`, `$element`, `$event`, `$onMount`, `$onDestroy` etc. This saves a line or two of code each time, but it looks so much cleaner when there is less boilerplate, as boilerplate intermixed with main code really muddles the logic (boilerplate `import`s are less of a problem, as they stay on the side and you can just ignore them).

And now unfortunately the bad changes.

Unfortunately then we have a downside of Malina. Svelte supports arbitrary statements with `$: { any code }` and will re-run it reactively whenever any state variables referred in it changes.

Malina has much more limited support. It supports assignments. For single statements like `console.log` here you need to list its dependencies, which breaks DRY quite hard. For anything more complex you need to extract it into a function, and then list its dependencies as well. I'm not sure what motivated this change.

The code for setting `slices` from `data` was reactive in Svelte version. It's not reactive here. As right now `data` doesn't change after app is loaded, that's fine, but if we made it dynamic we'd need to extract it into a function, and call that function.

And we have one more problem. In Svelte `use:` actions happen once DOM has fully rendered. Malina will call it as soon as it created its DOM node, before children are rendered. [And as far as I can tell, there's no way to ask Malina to notify us when rendering actually finished](https://github.com/malinajs/malinajs/issues/47).

This is a problem, because we have to wait for children to render, otherwise we won't have `main.scrollHeight`, and so we won't be able to calculate `rowHeight`, and so none of the dynamic rendering logic will work.

I did a dirty workaround of setting `rowHeight` to minimum of 10 if we're called early, to prevent rendering the whole 1MB file. At least after it loads, the updates should be accurate.

### `src/Decodings.xht`

Here's `Decodings` component:

```html
<script>
  export let data
  export let offset

  let int8, uint8, int16, uint16, int32, uint32, int64, uint64, float32, float64

  $: bytesAvailable = data.length - offset
  $: data, offset, update()

  function update() {
    int8 = data.readInt8(offset)
    uint8 = data.readUInt8(offset)

    if (bytesAvailable >= 2) {
      int16 = data.readInt16LE(offset)
      uint16 = data.readUInt16LE(offset)
    } else {
      int16 = ""
      uint16 = ""
    }

    if (bytesAvailable >= 4) {
      int32 = data.readInt32LE(offset)
      uint32 = data.readUInt32LE(offset)
      float32 = data.readFloatLE(offset)
    } else {
      int32 = ""
      uint32 = ""
      float32 = ""
    }

    if (bytesAvailable >= 8) {
      int64 = data.readBigInt64LE(offset)
      uint64 = data.readBigUInt64LE(offset)
      float64 = data.readDoubleLE(offset)
    } else {
      int64 = ""
      uint64 = ""
      float64 = ""
    }
  }
</script>

<table>
  <tr><th>Type</th><th>Value</th></tr>
  <tr><td>Int8</td><td>{int8}</td></tr>
  <tr><td>UInt8</td><td>{uint8}</td></tr>
  <tr><td>Int16</td><td>{int16}</td></tr>
  <tr><td>UInt16</td><td>{uint16}</td></tr>
  <tr><td>Int32</td><td>{int32}</td></tr>
  <tr><td>UInt32</td><td>{uint32}</td></tr>
  <tr><td>Int64</td><td>{int64}</td></tr>
  <tr><td>UInt64</td><td>{uint64}</td></tr>
  <tr><td>Float32</td><td>{float32}</td></tr>
  <tr><td>Float64</td><td>{float64}</td></tr>
</table>

<style>
  table {
    margin-top: 8px;
  }
  th {
    text-align: left;
  }
  tr:nth-child(even) {
    background-color: #555;
  }
</style>
```

As previously mentioned, we can't have that update block as a reactive statement `$: { ... }`. We had to extract it to a function, then call that function with explicit dependencies as `$: data, offset, update()`. I'm not a fan of this change.

### `src/App.xht`

And finally the `App` component.

```html
<script>
  import { Buffer } from "buffer/"
  import MainView from "./MainView.xht"
  import Decodings from "./Decodings.xht"
  import StatusBar from "./StatusBar.xht"

  let data = Buffer.from(window.api.data)
  let offset = 0

  let t0 = performance.now()
  $tick(() => {
    let t1 = performance.now()
    console.log(`Loaded ${Math.round(data.length / 1024)}kB in ${t1 - t0}ms`)
  })
</script>

<div class="editor">
  <MainView {data} on:changeoffset={e => offset = e.detail}/>
  <Decodings {data} {offset} />
  <StatusBar {offset} />
</div>

<malina:head>
  <title>fancy-data.bin</title>
</malina:head>

<style>
  :global(body) {
    background-color: #222;
    color: #fff;
    font-family: monospace;
    padding: 0;
    margin: 0;
  }
  .editor {
    display: flex;
    flex-direction: column;
    height: 100vh;
    overflow: auto;
  }
  :global(.editor > *) {
    background-color: #444;
  }
</style>
```

Trivially, `svelte:head` became `malina:head` and imports changed.

`.editor > :global(*)` CSS rule I wanted [crashed Malina](https://github.com/malinajs/malinajs/issues/46) so I had to do a workaround.

More problematic is lack of anything comparable to Svelte `await tick()` function.

Malina has `$tick(callback)` which we helpfully don't have to import, and is less helpfully a callback instead of a promise. Unfortunately just like the problem we had before in the `MainView`, it is called as soon as parent component renders, before its children do, so this measurement is worthless now.

### Performance

OK, we don't have hard numbers, but how well Malina performs compared with Svelte version, especially considering it was supposed to be higher performance than Svelte?

It is absolutely terrible.

Not only first render is slow - something that was true in Svelte as well before we added our optimizations. Scrolling around - something that was super fast even in unoptimized Svelte - takes forever in Malina. For 1MB scrolling a few lines takes 10s for the screen to update.

Obviously it would be possible to make this program much faster, but Svelte version is fast enough without any extra effort.

### Should you use Malina?

No.

Between all the bugs, missing functionality, and awful performance, there's no reason to use Malina. Just use Svelte like everyone else, at least for the time being.

But I liked some of its ideas. Especially `$emit`, `$context` and friends were definitely positive over Svelte's boilerplate-heavy approach. I didn't have opportunity to use its other shortcuts, but if it cuts on boilerplate, I'm generally for it.

### Results

Here's the results:

![Episode 68 Screenshot](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wv2p3swfhkg6n68i3zn1.png)

In the next episode, we'll go back to our Svelte version and teach it how to load files.

As usual, [all the code for the episode is here](https://github.com/taw/electron-adventures/tree/master/episode-68-malina-hex-editor).