4x Smaller, 50x Faster

87

It’s been a while since asciinema-player 2.6 was released and a lot has changed
since. Version 3.0 is around the corner with so much good stuff, that even though
it’s not released yet, I couldn’t wait any longer to share.

Long story short: asciinema-player has been reimplemented from scratch in
JavaScript and Rust, resulting in 50x faster virtual terminal interpreter, while
at the same time, reducing the size of the JS bundle 4x.

You may wonder what prompted the move from the previous ClojureScript
implementation. As much as I love Clojure/ClojureScript there were several major
and minor problems I couldn’t solve, mostly around these 3 areas:

  • speed – I wanted the player to be ultra-smooth, even for the most heavy
    animated recordings. Due to ClojureScript’s immutable data structures, there’s a
    lot of objects created and garbage collected all the time, and for the high
    frame-rate, heavy animations this puts a lot of pressure on CPU and memory. The
    new implementation of the virtual terminal interpreter in Rust (compiled to
    WASM) does it 50x faster. Additional speed improvement comes from porting the
    views from React.js to SolidJS, one of the fastest UI
    libraries out there.

  • size – the output bundle from ClojureScript compiler is rather big. It’s fine
    when you build your own app in ClojureScript, however when you provide a library to
    use by other people on their websites, it’s quite bad. 2.6 is 570kb (minified) –
    that’s over half a megabyte. That bundle contains whole ClojureScript standard
    library, several popular and useful libraries like reagent, core.async, and
    finally React.js (via reagent). The new 3.0 is pure JS with pretty much just
    SolidJS as the only dependency (which is tiny itself). This makes the new player
    much smaller, ~140kb (minified), even though it includes embeded WASM bytecode
    (which makes the bulk of the bundle size).

  • integration with JS ecosystem – ClojureScript is not that easy to integrate
    with the JS ecosystem. I know, there’s been a lot of improvements done in this
    space over the years, and I’m sure someone will immediately point me to relevant
    docs, but it’s still the extra mile you need to go when compared to regular JS
    codebase, and some things didn’t have any support last time I checked (like
    embedding WASM in the bundle). Things might have changed here, but first two
    arguments above still hold, so it was worth it. And as a result, you can now use
    the player in your own app by importing the ES module provided by
    asciinema-player npm
    package
    .

Btw, special shout out to Ryan Carniato, the author of SolidJS, for focusing on
speed and simplicity, while not compromising on usability. Thanks Ryan!

Now, on top of all the above, I had fun building terminal control sequence
interpreter in Rust
, using excellent
resource for that – Paul Williams’ parser for ANSI-compatible video
terminals
. Special shout out to Paul
Williams!

But back to speed. It used to be good enough, which is no longer good enough for
me. The old player used to be sufficiently fast for probably 90% of the
recordings people host on asciinema.org. It
exercised many types of optimizations, like memoization (trading memory for CPU
time) and
run-ahead
(which used a lot of memory by precomputing terminal contents for each future
frame).

At first I planned to implement the terminal emulation part in Rust without any
optimizations, just write idiomatic Rust code, then revisit the tricks from the
old implementation. The initial benchmarks blew my mind though, showing that
spending additional time on optimizing the emulation part is absolutely
unnecessary.

The numbers show how many megabytes of text the terminal emulator can process in
each player version (tested on Chrome 88):

50 times faster on average!

Note that the above benchmark represents the speed of text stream parsing
(including control sequences), as well as updating emulator’s internal, virtual
screen buffer. This has been the bottleneck in the previous implementation of
the player. The benchmark doesn’t measure rendering of the buffer to the actual
screen (DOM), therefore the rendering speed improvements coming from
React.js->SolidJS transition are not included here. However, SolidJS has been
benchmarked against React.js and other libs many times already, so I didn’t
bother proving it’s faster.

I still thought I may need to implement some form of terminal state
snapshot/restore to support the “seeking” feature. This feature requires feeding
the terminal emulator with the whole text stream between the current position
and the desired position, or in the worst case when you’re seeking back, feeding
the emulator with the whole text from the very beginning of the recording up to
the desired position. Optimizing this could be done, for example, by keeping
snapshot of the terminal emulator state at multiple time points, sort of like
having key-frames every couple of seconds. In ClojureScript implementation this
came for free, thanks to the immutable data structures. In the new JS+Rust
implementation this would have required extra work, but it turned out, that’s not
needed either – clicking on the p

Read More

Aditya Gaurav
WRITTEN BY

Aditya Gaurav

Simple man, evolving every jiffy.