Skip to content

Commit 397ea36

Browse files
authored
Skip redundant host function registration on reload (#422)
`get_loaded_sandbox` and `unload` each called `RegisterHostModules`, although the base snapshot already holds that registration. A load/unload cycle made four guest calls, but only `register_handler` and `RunHandler` are needed. In hyperlight-dev/hyperlight-bench#46, `kvm-hyperlight-js-reload` lost 15% throughput against 0.4.0. This change registers host functions only when a restored snapshot records different ones than the builder supplies. It then takes a new base snapshot, so later cycles skip registration. `full_lifecycle/add_load_handle_unload`: | | main | this PR | |---|---|---| | MSHV | 415 µs | 315 µs | | KVM | 248 µs | 203 µs | KVM stays about 10% slower than 0.4.0 because each guest call costs more in Hyperlight 0.18. Breaking: the snapshot metadata schema is now 2. Snapshots from 0.5.0 can record host functions their base never registered, so they are rejected and must be recreated. --------- Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com>
1 parent 599c573 commit 397ea36

7 files changed

Lines changed: 334 additions & 13 deletions

File tree

‎docs/persistent-snapshots.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,12 @@ The guest can call every supplied host function, including ones the snapshot
155155
does not record. Handlers loaded after `unload()` can import them too. Supply
156156
only the modules the guest should reach.
157157

158+
If the builder supplies host functions the snapshot does not record,
159+
hyperlight-js registers them in the guest and copies the guest state. A
160+
`JSSandbox` does this in `build()` and in each `restore()` of such a snapshot.
161+
A `LoadedJSSandbox` does it in `unload()` after such a restore. Supply exactly
162+
the recorded functions to avoid this cost.
163+
158164
A loaded snapshot records the host functions its evaluated handlers need. Saving
159165
it again after a restore keeps that record, even when the restoring builder
160166
supplied more functions. A `JSSandbox` snapshot records every function supplied
@@ -239,6 +245,8 @@ the same output routing is required.
239245
version for diagnostics. It also records the guest runtime ABI version.
240246
Unsupported metadata schema or guest ABI versions are rejected before starting
241247
guest execution.
248+
- Snapshots saved by hyperlight-js 0.5.0 use metadata schema 1 and are
249+
rejected. Recreate them.
242250
- Digest verification detects accidental or malicious mutation, but does not
243251
authenticate who produced a snapshot. Authenticate snapshot artifacts through
244252
the distribution mechanism used by your application.

‎src/hyperlight-js/src/sandbox/host_fn.rs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ pub(crate) fn register_host_function_manifest(
6868
}
6969

7070
/// Return the registered host functions grouped by module.
71+
///
72+
/// Modules without functions are omitted because the guest has nothing to register for them.
7173
pub(crate) fn host_function_manifest(
7274
modules: &HashMap<String, HostModule>,
7375
) -> HostFunctionManifest {
@@ -81,6 +83,7 @@ pub(crate) fn host_function_manifest(
8183
functions.sort();
8284
(module.clone(), functions)
8385
})
86+
.filter(|(_, functions)| !functions.is_empty())
8487
.collect()
8588
}
8689

‎src/hyperlight-js/src/sandbox/js_sandbox.rs‎

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,23 @@ pub fn validate_namespace_not_reserved(namespace: &str) -> Result<()> {
7474
Ok(())
7575
}
7676

77+
/// Return a base snapshot whose guest state registers `host_functions`.
78+
///
79+
/// `inner` must hold the guest state of `base`, which registers the host
80+
/// functions recorded in `metadata`.
81+
fn snapshot_base_with_host_functions(
82+
inner: &mut Sandbox,
83+
base: Arc<HyperlightSnapshot>,
84+
metadata: &SnapshotMetadata,
85+
host_functions: &HostFunctionManifest,
86+
) -> Result<Arc<HyperlightSnapshot>> {
87+
if metadata.host_functions() == host_functions {
88+
return Ok(base);
89+
}
90+
register_host_function_manifest(inner, host_functions)?;
91+
inner.snapshot()
92+
}
93+
7794
/// A Hyperlight Sandbox with a JavaScript run time loaded but no guest code.
7895
pub struct JSSandbox {
7996
pub(super) inner: Sandbox,
@@ -84,7 +101,8 @@ pub struct JSSandbox {
84101
modules: HashMap<String, Script>,
85102
// Host functions selected by SandboxBuilder.
86103
host_functions: HostFunctionManifest,
87-
// Snapshot of state before any handlers are added.
104+
// Snapshot of state before any handlers are added. Its guest state has
105+
// `host_functions` registered and matches the current guest state.
88106
// This is used to restore state back to a neutral JSSandbox.
89107
snapshot: Arc<HyperlightSnapshot>,
90108
// metric drop guard to manage sandbox metric
@@ -109,10 +127,12 @@ impl JSSandbox {
109127
pub(crate) fn from_loaded(
110128
mut loaded: Sandbox,
111129
snapshot: Arc<HyperlightSnapshot>,
130+
metadata: &SnapshotMetadata,
112131
host_functions: HostFunctionManifest,
113132
) -> Result<Self> {
114133
loaded.restore(snapshot.clone())?;
115-
register_host_function_manifest(&mut loaded, &host_functions)?;
134+
let snapshot =
135+
snapshot_base_with_host_functions(&mut loaded, snapshot, metadata, &host_functions)?;
116136
Ok(Self {
117137
inner: loaded,
118138
handlers: HashMap::new(),
@@ -124,16 +144,22 @@ impl JSSandbox {
124144
}
125145

126146
pub(crate) fn from_snapshot(
127-
inner: Sandbox,
147+
mut inner: Sandbox,
128148
snapshot: &Snapshot<JSSandbox>,
129149
host_functions: HostFunctionManifest,
130150
) -> Result<Self> {
151+
let base = snapshot_base_with_host_functions(
152+
&mut inner,
153+
snapshot.inner(),
154+
snapshot.metadata(),
155+
&host_functions,
156+
)?;
131157
Ok(Self {
132158
inner,
133159
handlers: snapshot.metadata().handlers().collect(),
134160
modules: snapshot.metadata().modules().collect(),
135161
host_functions,
136-
snapshot: snapshot.inner(),
162+
snapshot: base,
137163
_metric_guard: SandboxMetricsGuard::new(),
138164
})
139165
}
@@ -164,9 +190,23 @@ impl JSSandbox {
164190
.metadata()
165191
.validate_host_functions(&self.host_functions)?;
166192
self.inner.restore(snapshot.inner())?;
193+
let base = snapshot_base_with_host_functions(
194+
&mut self.inner,
195+
snapshot.inner(),
196+
snapshot.metadata(),
197+
&self.host_functions,
198+
);
199+
self.snapshot = match base {
200+
Ok(base) => base,
201+
Err(error) => {
202+
// Return the guest to the state the unchanged fields describe.
203+
// Report the original error even if that fails.
204+
let _ = self.inner.restore(self.snapshot.clone());
205+
return Err(error);
206+
}
207+
};
167208
self.handlers = snapshot.metadata().handlers().collect();
168209
self.modules = snapshot.metadata().modules().collect();
169-
self.snapshot = snapshot.inner();
170210
Ok(())
171211
}
172212

@@ -377,10 +417,6 @@ impl JSSandbox {
377417
self.host_functions.clone(),
378418
));
379419

380-
// Publish the builder-selected host functions before compiling modules
381-
// and handlers that may import them.
382-
register_host_function_manifest(&mut self.inner, &self.host_functions)?;
383-
384420
// Register user modules first so that handlers can import them.
385421
// NOTE: HashMap iteration order is non-deterministic, but this is safe
386422
// because modules are lazily compiled by the UserModuleLoader when first

‎src/hyperlight-js/src/sandbox/loaded_js_sandbox.rs‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ use crate::sandbox::metrics::SandboxMetricsGuard;
3838
pub struct LoadedJSSandbox {
3939
inner: Sandbox,
4040
// Snapshot of state before the sandbox was loaded and before any handlers were added.
41+
// Its guest state has the host functions in `snapshot_metadata` registered,
42+
// which `unload` relies on.
4143
// This is used to restore state back to a JSSandbox.
4244
snapshot: Arc<HyperlightSnapshot>,
4345
// Metadata for the currently captured loaded guest state. Its host-function
@@ -205,8 +207,13 @@ impl LoadedJSSandbox {
205207
/// Unloads the Handlers from the sandbox and returns a `JSSandbox` with the JavaScript runtime loaded.
206208
#[instrument(err(Debug), skip_all, level=Level::DEBUG)]
207209
pub fn unload(self) -> Result<JSSandbox> {
208-
JSSandbox::from_loaded(self.inner, self.snapshot, self.available_host_functions)
209-
.inspect(|_| metrics::counter!(METRIC_SANDBOX_UNLOADS).increment(1))
210+
JSSandbox::from_loaded(
211+
self.inner,
212+
self.snapshot,
213+
&self.snapshot_metadata,
214+
self.available_host_functions,
215+
)
216+
.inspect(|_| metrics::counter!(METRIC_SANDBOX_UNLOADS).increment(1))
210217
}
211218

212219
/// Take a snapshot of the the current state of the sandbox.

‎src/hyperlight-js/src/sandbox/sandbox_builder.rs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,9 @@ impl<T: SnapshotTarget> SandboxBuilder<Snapshot<T>> {
424424
///
425425
/// The snapshot's memory layout overrides heap, scratch, and transport
426426
/// settings. Supply every host function and module loader the snapshot uses.
427+
/// Host functions the snapshot does not record are registered in the guest,
428+
/// which costs one guest call and a copy of guest state. A `JSSandbox` pays
429+
/// this in `build()`, and a `LoadedJSSandbox` in `unload()`.
427430
pub fn from_snapshot(snapshot: Snapshot<T>) -> Self {
428431
Self::with_source(snapshot)
429432
}

‎src/hyperlight-js/src/sandbox/snapshot.rs‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ use super::js_sandbox::{validate_module_identifier, validate_namespace_not_reser
3030
use crate::{new_error, JSSandbox, LoadedJSSandbox, Result, Script};
3131

3232
const METADATA_NAMESPACE: &str = "dev.hyperlight.hyperlight-js.snapshot.v1";
33-
const METADATA_SCHEMA_VERSION: u32 = 1;
33+
// Version 2 guarantees that a snapshot's base registers its recorded host functions.
34+
const METADATA_SCHEMA_VERSION: u32 = 2;
3435
const BASE_TAG_SUFFIX: &str = ".hljs-base";
3536
const MAX_OCI_TAG_LENGTH: usize = 128;
3637
const MAX_LOADED_SNAPSHOT_TAG_LENGTH: usize = MAX_OCI_TAG_LENGTH - BASE_TAG_SUFFIX.len();
@@ -119,6 +120,10 @@ impl SnapshotMetadata {
119120
self.kind
120121
}
121122

123+
pub(crate) fn host_functions(&self) -> &HostFunctionManifest {
124+
&self.host_functions
125+
}
126+
122127
pub(crate) fn handlers(&self) -> impl Iterator<Item = (String, Script)> + '_ {
123128
self.handlers
124129
.clone()
@@ -543,6 +548,16 @@ mod tests {
543548
.contains("produced by hyperlight-js 9.8.7"));
544549
}
545550

551+
#[test]
552+
fn metadata_rejects_schema_version_1() {
553+
let mut metadata = metadata(SnapshotKind::JsSandbox);
554+
metadata.schema_version = 1;
555+
556+
let error = metadata.validate().unwrap_err();
557+
558+
assert!(error.to_string().contains("metadata version 1 "));
559+
}
560+
546561
#[test]
547562
fn metadata_rejects_other_guest_abi_version() {
548563
let mut metadata = metadata(SnapshotKind::JsSandbox);

0 commit comments

Comments
 (0)