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.
Featured Content Ads
add advertising hereYou 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.
Featured Content Ads
add advertising hereThe 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