Skip to content

lua: Cache loaded modules to disk as binary chunks - #769

Open
arch1t3cht wants to merge 4 commits into
win8from
lua-load-cache
Open

arch1t3cht wants to merge 4 commits into
win8from
lua-load-cache

Conversation

@arch1t3cht

@arch1t3cht arch1t3cht commented Oct 31, 2026 •

Copy link
Copy Markdown
Member

With my current version of DependencyControl (0.9.0), any automation
script using DependencyControl imports no fewer than 47 MoonScript
files. Every automation scripts has its own isolated Lua environment, so
every script needs to compile its MoonScript files to Lua individually.

When enough such automation scripts are installed, the MoonScript-to-Lua
conversion alone makes up more than 90% of the CPU time when launching
Aegisub or rescanning the autoload folder.

Improve this situation by caching the binary blobs of loaded Lua or
MoonScript modules to the disk.

Some alternative approaches that were considered:

  • Only caching the loaded chunks (or Lua transpile output) for MoonScript
    files and not also for Lua files.
    -> Benchmarks showed that even for Lua files, the time saved by the
    caching outweighs the cost of the additional file IO, see below.
  • Caching the loaded chunks to memory and not to the disk.
    -> This would simplify some of the cache invalidation logic, but not
    improve the load time of the first loaded automation script.
    This is already quite significant; see the first-launch vs.
    second-launch comparisons below.
  • Encouraging script distributors to transpile their MoonScript code to
    Lua prior to distribution.
    -> Apart from imposing extra effort, this makes local error messages
    less useful, makes it harder to locally patch scripts, and
    makes it harder to audit downloaded scripts.
  • Making DependencyControl transpile the modules it installs to Lua or
    chunks.
    -> Has many of the same problems as the previous point.

The trickiest part is ensuring that cached chunks are properly
regenerated when the source files (or MoonScript) are updated.
Since we have full control over the cache file's format, we can build a
cache key out of not just the modification time, but also the size and
inode number (or some equivalent identifier that detects file moves).

Apart from that, we also need to make sure that the line_tables relating
compiled Lua source lines to MoonScript source lines (which are needed
for proper stack traces in error messages) are also preserved when
loading chunks directly from cache files.

On Windows, the file locking causes some difficulties when multiple
threads are trying to write the same cache file, so there we need to
make sure to not make errors due to locked files bloat the logs or
unnecessarily delay the loading.

Some numbers:

With a typical environment with around 25 DependencyControl-enabled
scripts installed, this results in the following reductions in
start time (wall-clock times via hyperfine on Linux):

  • When loading all scripts on a single thread (replacing std::launch::async
    with std::launch::deferred in auto4_base.cpp):

    • Before this commit: 23.136 s ± 0.134 s
    • After this commit, first launch: 4.330 s ± 0.120 s
    • After this commit, second launch: 1.468 s ± 0.026 s

    -> Up to a 93% reduction

  • When loading scripts on multiple threads (current behavior)

    • Before this commit: 6.258 s ± 0.209 s
    • After this commit, first launch: 4.166 s ± 0.302 s
    • After this commit, second launch: 1.944 s ± 0.249 s

    -> Up to a 68% reduction

Of course, the percentages will depend on the number of installed
automation scripts.

Note in particular that loading all scripts on one thread is now
faster on the second launch than loading them on different threads.
This is probably because the different DependencyControl instances are
fighting for the locks on their config files, but I haven't profiled
this in detail yet.

Caching only the chunks of MoonScript files and not of Lua files results
in a start time of 1.670 s ± 0.030 s on the second launch on one
thread, so also caching Lua files yields around a 12% improvement.

Options for future work:

  • Rethink the multithreaded loading of scripts
  • Somehow improve DependencyControl's locking, possibly by adding
    additional features to the Lua API
  • Let automation scripts be loaded in the background instead of blocking
    Aegisub's launching on it

@arch1t3cht
arch1t3cht added this pull request to stack #768 October 31, 2026 23:29
@arch1t3cht
arch1t3cht force-pushed the lua-load-cache branch 2 times, most recently from 6b8d3ea to 20ddd12 Compare October 31, 2026 23:40
@arch1t3cht
arch1t3cht force-pushed the lua-load-cache branch 4 times, most recently from be0efe6 to e1df8ea Compare November 5, 2026 11:44
@arch1t3cht
arch1t3cht marked this pull request as ready for review November 5, 2026 11:55
Prepare to add more functions to the anonymous namespace without too much
extra indentation.
boost::filesystem's unique_path was not ported over to std::filesystem
due to security issues, so the migration made agi::io::Save's temp files
not use any unique paths at all.
This is mostly fine, but when multiple threads are trying to save the
same file (as might happen with the Lua blob caching in the following
commit), bad things might happen.
Hence, give each temp file a name depending on the thread writing them.
This is not trying to be resistant to outside attacks, but it prevents
multiple Aegisub threads from fighting over the same file.
@arch1t3cht
arch1t3cht force-pushed the lua-load-cache branch 5 times, most recently from 7af1017 to 7e429c9 Compare November 10, 2026 12:41
MSVC's STL will output error codes in the system category rather than
the generic category, so the error code values were checked against the
wrong enum. The FileNotFound error detection did work before, but only
by coincidence since the error's integer value was 2 for both POSIX and
Windows.
@arch1t3cht
arch1t3cht force-pushed the lua-load-cache branch 2 times, most recently from 09e1186 to 121efac Compare November 10, 2026 17:43
With my current version of DependencyControl (0.9.0), any automation
script using DependencyControl imports no fewer than 47 MoonScript
files. Every automation scripts has its own isolated Lua environment, so
every script needs to compile its MoonScript files to Lua individually.

When enough such automation scripts are installed, the MoonScript-to-Lua
conversion alone makes up more than 90% of the CPU time when launching
Aegisub or rescanning the autoload folder.

Improve this situation by caching the binary blobs of loaded Lua or
MoonScript modules to the disk.

Some alternative approaches that were considered:
- Only caching the loaded chunks (or Lua transpile output) for MoonScript
  files and not also for Lua files.
  -> Benchmarks showed that even for Lua files, the time saved by the
     caching outweighs the cost of the additional file IO, see below.
- Caching the loaded chunks to memory and not to the disk.
  -> This would simplify some of the cache invalidation logic, but not
     improve the load time of the first loaded automation script.
     This is already quite significant; see the first-launch vs.
     second-launch comparisons below.
- Encouraging script distributors to transpile their MoonScript code to
  Lua prior to distribution.
  -> Apart from imposing extra effort, this makes local error messages
     less useful, makes it harder to locally patch scripts, and
     makes it harder to audit downloaded scripts.
- Making DependencyControl transpile the modules it installs to Lua or
  chunks.
  -> Has many of the same problems as the previous point.

The trickiest part is ensuring that cached chunks are properly
regenerated when the source files (*or* MoonScript) are updated.
Since we have full control over the cache file's format, we can build a
cache key out of not just the modification time, but also the size and
inode number (or some equivalent identifier that detects file moves).

Apart from that, we also need to make sure that the line_tables relating
compiled Lua source lines to MoonScript source lines (which are needed
for proper stack traces in error messages) are also preserved when
loading chunks directly from cache files.

On Windows, the file locking causes some difficulties when multiple
threads are trying to write the same cache file, so there we need to
make sure to not make errors due to locked files bloat the logs or
unnecessarily delay the loading.

Some numbers:

With a typical environment with around 25 DependencyControl-enabled
scripts installed, this results in the following reductions in
start time (wall-clock times via hyperfine on Linux):

- When loading all scripts on a single thread (replacing std::launch::async
  with std::launch::deferred in auto4_base.cpp):

  - Before this commit:                 23.136 s ±  0.134 s
  - After this commit, first launch:    4.330 s ±  0.120 s
  - After this commit, second launch:   1.468 s ±  0.026 s

  -> Up to a 93% reduction

- When loading scripts on multiple threads (current behavior)

  - Before this commit:                 6.258 s ±  0.209 s
  - After this commit, first launch:    4.166 s ±  0.302 s
  - After this commit, second launch:   1.944 s ±  0.249 s

  -> Up to a 68% reduction

Of course, the percentages will depend on the number of installed
automation scripts.

Note in particular that loading all scripts on one thread is now
*faster* on the second launch than loading them on different threads.
This is probably because the different DependencyControl instances are
fighting for the locks on their config files, but I haven't profiled
this in detail yet.

Caching only the chunks of MoonScript files and not of Lua files results
in a start time of 1.670 s ±  0.030 s on the second launch on one
thread, so also caching Lua files yields around a 12% improvement.

Options for future work:
- Rethink the multithreaded loading of scripts
- Somehow improve DependencyControl's locking, possibly by adding
  additional features to the Lua API
- Let automation scripts be loaded in the background instead of blocking
  Aegisub's launching on it
@arch1t3cht

Copy link
Copy Markdown
Member Author

Should be genuinely ready now.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant