Preloaded base image
Status: draft. ry ships its own build of base R 4.6.2 (just base-r, crates/rcmd/src/base.rs)
in GNU R’s on-disk layout: per-package lazy-load databases, help, and share/. Start-up reads
that tree the way GNU R does. This page is the design for the next step, a preloaded image, and
nothing on it is built yet.
An empty ry run script takes about 140 ms on Linux in a release build. Base alone (with
R_DEFAULT_PACKAGES=NULL) takes about 45 ms, so roughly two thirds of start-up attaches the
default packages: stats, graphics, grDevices, utils, datasets and methods, where methods
rebuilds its S4 tables in ..First.lib. (Rough wall-clock numbers on a loaded machine, not a
profile.) None of that work depends on the user’s code, so it can be done once at build time.
The shape
Section titled “The shape”One file per platform, base.image, written at the end of just base-r by a ry process that has
started normally and attached the default packages:
- Flat and relocatable. The image is the heap’s arenas laid out as they are in memory, with every reference stored as a dense index into its arena rather than an address, so the file is mapped read-only and used in place. Objects in the image are never freed, and a write to one has to copy it first, which needs a copy-on-write path for mapped objects.
- Default packages loaded. The namespaces, their exports, the attached package environments, the S3 method registry and methods’ S4 class and method tables are all in the image, so a start from it runs no R code until the profiles.
- No bytecode. Closures carry their source bodies; ry compiles on its own terms (as for the lazy-load databases today).
- Keyed by content. The image records the hashes of the runtime and the base tree it was built from, and a mismatch means “start the slow way”, never “start from a stale image”.
State outside the heap
Section titled “State outside the heap”An image can only capture what lives in the heap. The interpreter also keeps state in Interp
(crates/interp/src/interp.rs) and in libR’s C globals, and each piece has to be either rebuilt
at start-up, saved beside the heap in the image, or proven to be empty after start-up.
| State | Where | After start-up | Plan |
|---|---|---|---|
| Symbols | Interp::symbols, the heap’s symbol table |
Thousands of interned names | Saved: the table is an arena of its own |
| Builtins and their index | builtins, builtin_index, primitive_formals |
Static, built from Rust tables | Rebuilt: cheap and identical every run |
Global objects (R_GlobalEnv, R_BaseEnv, R_NilValue and the other exported data symbols) |
Interp::globals, crates/libr/src/globals.rs |
Fixed objects | Saved as known indices; the C globals are pointed at them after mapping |
| Default packages’ routine tables | builtin_libraries, builtin_libraries.rs |
Registered DLL entries | Rebuilt: static tables, but the DllInfo objects that R code holds must match by index |
| Compiled code of grid and splines | dyn.load handles |
Loaded shared objects | Not in the image: grid and splines load on first use, as today |
| Connections | Interp::connections |
stdin, stdout, stderr only | Rebuilt |
| Random number generator | Interp::random |
Unseeded | Rebuilt; .Random.seed is absent until first use |
| Method dispatch cache | Interp::method_cache |
Warm | Dropped: it fills again on demand |
| Hooks, handlers, restarts, frames, warnings | hooks, handlers, restarts, frames, warnings |
Empty at top level | Checked empty when the image is written |
| Standard library tags, annotations, moves | library_tags, annotations, moves |
Registered by ry’s own packages | Saved with the heap if they hold heap indices, otherwise rebuilt |
| Options, environment variables, locale | options() is heap; Sys.setenv and the locale are process state |
Set by the profiles | The image is written before the profiles run; profiles always run after mapping |
Command line, interactive(), start time |
command_args, interactive, started |
Per process | Always from the current process |
The rule this table enforces: anything that differs between two runs of the same ry (arguments,
environment, the user’s profiles, the time) stays out of the image, and the image is only written
from a state where every per-run structure is empty.
Open questions
Section titled “Open questions”- Whether the heap’s arenas can be mapped as they are, or need one fix-up pass for pointers that
native code caches (
DATAPTRresults,CHARSXPaddresses handed to C). - How
ry load‘s S4 start-up cache (crates/loader) composes with an image that already holds methods’ tables.
The methods package
Section titled “The methods package”The image is written after methods’ .onLoad, so it already holds the closures that
interp::methods_core::install_fast_paths patched (new, validObject, is, extends and the
default initialize copies) and the class and generic tables they read. The native core keeps no
state outside those R objects, so nothing reruns when the image loads; only the ryS4* builtins
need to resolve by name, which the rebuilt builtin table guarantees. The fast paths install only
where a closure’s hash matches the methods source they were written for, so an image built from a
newer base R silently loses one whose source changed (the bundled 4.6.2’s is is one); the image
writer should report each fast path that did not install.
- Whether one image per platform is enough, or whether
R_DEFAULT_PACKAGESother than the default should fall back to the slow path.