saghul/txiki.js

Consider lazy-loading some polyfills

saghul opened this issue · 12 comments

Currently, running a file with just "1+1" in it:

hyperfine -N -w 5 "./build/tjs run t.js"
Benchmark 1: ./build/tjs run t.js
  Time (mean ± σ):      12.0 ms ±   0.5 ms    [User: 8.5 ms, System: 1.5 ms]
  Range (min … max):    11.5 ms …  18.2 ms    243 runs

Without eval-ing core nor run-main:

Benchmark 1: ./build/tjs run t.js
  Time (mean ± σ):      11.7 ms ±   0.8 ms    [User: 8.1 ms, System: 1.5 ms]
  Range (min … max):    11.0 ms …  16.6 ms    263 runs

Without polyfills, no core no run-main:

Benchmark 1: ./build/tjs run t.js
  Time (mean ± σ):       5.5 ms ±   0.6 ms    [User: 2.4 ms, System: 1.2 ms]
  Range (min … max):     5.0 ms …  10.6 ms    462 runs

This suggests polyfills take ~30-50% of the startup time.

Screenshot 2024-04-15 at 16 11 42

Looks like web-streams and URL are the biggest offenders.

Just removing webstreams and compression streams does it:

Benchmark 1: ./build/tjs run t.js
  Time (mean ± σ):       7.5 ms ±   0.5 ms    [User: 4.2 ms, System: 1.4 ms]
  Range (min … max):     6.9 ms …  10.4 ms    267 runs

Just a data point.
Experimenting with a small js program on a Cortex A5, the startup time is a little higher than I expected.
image

Would be nice if the startup time can be further improved.

Runtimes like bun uses a bunfig.toml file to specify information about how it should run. It can be either defined per project, or with a default profile .bunfig.toml in HOME.
Can we do something like that where we specify which modules to preload (alongside additional configuration which might be needed in future releases)? This way one can optimize their statup time based on the application.

That feels a bit overkill IMHO. We are not loading any modules other than the polyfills to provide core functionality.

The optimization I discussed above is that some of those (compression streams specifically) are somewhat large, so we could lazy load them on first use.

Yes a bit. I will try to rephrase my discomfort in a more reasonable suggestion :D.

My point is that lazy loading modules introduces few high non-deterministic slowdowns during execution, in exchange of a faster startup for any of the simpler scripts. And for most this is not going to be an issue.
Still, I think there should be a mechanism, even just a flag or an env variable to opt out of that and get all things preloaded.
Basically for the same reason we might want to opt out of the garbage collection in some regions of code when needed.

While what you say is true in theory, I'd like to know more about the implications in practice. Again, as of right now the only polyfill I'm considering to lazy load is compression streams. The only noticeable diffrence for user code would be that new CompressionStream takes a few ms more.

For a "real wold example", I am using using the decompression offered by WebStreams with data from a serial stream with relatively tight timings in its flow control on low power devices which are not just going to take 5 ms to load the polyfill :D.
I don't know exactly how the final implementation you are envisioning would be, but that would likely be enough to desync the whole channel.

That being said, I am fine either way.
Worst case scenario, I will just patch it out on my build. I just wanted to provide feedback about the potential drawbacks, even if they only affect a small subset of cases.

That's good feedback!

The rough idea is to expose a Foo global which is a getter that wil evaluate the code the fist time it gets called.

That means the first time you build the decompression stream a small price is paid.

I'd assume the setup is done before the data starts flowing?

I think it is likely the best approach.
But now, I even more appreciate why languages like Zig purposefully decided against implementing getters and setters as part of the language :D.

I'd assume the setup is done before the data starts flowing?

I cannot do that. The serial protocol I am interfacing with is "packet based". The stream itself is just encoded as payload by a sequence of packets. The fact the packet encodes a substream and not some random standalone command is defined at runtime, based on the signature of such packet. So you can have multiple substreams "concurrently" by time splitting the channel and building the decompression stream must be contextual to that.

So there is no "initial global setup" where I can hide the lazy loading of the first instance outside of the time-sensitive flow (if not as a workaround).

The problem of workarounds is that today they are not needed, tomorrow they might for CompressionStream, and the day after tomorrow possibly anything else.
To be clear, I think lazy loading would be a positive for txiki, and would improve performance in most scenarios.
It is just bad for what I am doing with txiki and my selfish self would prefer to have an opt-out flag :D.

I see.

So, how about this. If / when this is implemented, let's check if it has a real impact and add a flag to side-step it, if it does have an impact.

Coming back to this for a sec. It's not actually the compression streams polyfill, but the whole web streams! That means we'd have to make all of that lazy loaded. Not sure how I feel about it. Startup is pretty damn fast already.

Tentatively closing. We'll see if it ever needs to be reopened...