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()
:
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
.
$ 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.
<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.
<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.
The other difference is one of many bugs in Malina I discovered. We'd like to do {:else} 
, 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.
<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:
<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:
import { createEventDispatcher } from "svelte"
let dispatch = createEventDispatcher()
dispatch("changeoffset", e.target.dataset.offset)
You can just do this with $emit
:
$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.
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:
<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.
<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 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:
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.
Top comments (0)