use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant};
use ferrijs_permissions::{Container, Permissions};
use rquickjs::{AsyncContext, AsyncRuntime, CatchResultExt, Ctx, Module, Value};
use crate::console::{ConsoleCapture, ConsoleSink};
use crate::console_fmt::install_console;
use crate::error::{ScriptError, ScriptErrorKind};
use crate::extension::Extension;
use crate::limits::{AppliedLimits, Deadline, Limits, NeverParked, PauseClock, RunOptions, TimeoutState, run_within};
use crate::modules::{
BoxLoader, BoxResolver, FileLoader, FileResolver, LoaderChain, ModulePolicy, ModuleRegistry, RequireHook,
ResolverChain,
};
use crate::realm::RealmOptions;
use crate::redact::Redactor;
use crate::result::ConsoleEntry;
use crate::source_map::{CompiledModule, SourceMapper};
use crate::vm::{VmHandle, VmShutdown, spawn_vm_loop};
use crate::vm_with;
pub const DEFAULT_MAX_CONSOLE_ENTRIES: usize = 1_000;
pub const DEFAULT_MAX_CONSOLE_BYTES: usize = 1_048_576;
pub const DEFAULT_MAX_CONSOLE_ENTRY_BYTES: usize = 8_192;
#[derive(Clone)]
pub struct ConsoleOptions {
pub max_entries: usize,
pub max_bytes: usize,
pub max_entry_bytes: usize,
pub sink: Option<Arc<dyn ConsoleSink>>,
}
impl Default for ConsoleOptions {
fn default() -> Self {
Self {
max_entries: DEFAULT_MAX_CONSOLE_ENTRIES,
max_bytes: DEFAULT_MAX_CONSOLE_BYTES,
max_entry_bytes: DEFAULT_MAX_CONSOLE_ENTRY_BYTES,
sink: None,
}
}
}
impl std::fmt::Debug for ConsoleOptions {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ConsoleOptions")
.field("max_entries", &self.max_entries)
.field("max_bytes", &self.max_bytes)
.field("max_entry_bytes", &self.max_entry_bytes)
.field("sink", &self.sink.is_some())
.finish()
}
}
#[derive(Debug, Clone, Default)]
pub struct ProcessOptions {
pub cwd: Option<String>,
pub argv: Vec<String>,
}
pub struct Config {
pub limits: Limits,
pub console: ConsoleOptions,
pub realm: RealmOptions,
pub modules: ModulePolicy,
pub process: ProcessOptions,
pub identity: ferrijs_std::identity::Identity,
pub permissions: Arc<Container>,
pub redactor: Option<Arc<dyn Redactor>>,
pub pause_clock: Arc<dyn PauseClock>,
pub fs_global: bool,
pub timers: bool,
#[cfg(feature = "fetch")]
pub fetch: Option<Arc<dyn crate::fetch::FetchBackend>>,
pub extensions: Vec<Arc<dyn Extension>>,
}
impl std::fmt::Debug for Config {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Config")
.field("limits", &self.limits)
.field("console", &self.console)
.field("realm", &self.realm)
.field("modules", &self.modules)
.field("process", &self.process)
.field("identity", &self.identity)
.field("permissions", &self.permissions)
.field("fs_global", &self.fs_global)
.field("timers", &self.timers)
.field(
"extensions",
&self.extensions.iter().map(|e| e.name().to_string()).collect::<Vec<_>>(),
)
.finish_non_exhaustive()
}
}
pub struct Builder {
config: Config,
}
impl Default for Builder {
fn default() -> Self {
Self {
config: Config {
limits: Limits::default(),
console: ConsoleOptions::default(),
realm: RealmOptions::default(),
modules: ModulePolicy::default(),
process: ProcessOptions::default(),
identity: ferrijs_std::identity::Identity::default(),
permissions: Arc::new(Container::new(Permissions::none())),
redactor: None,
pause_clock: Arc::new(NeverParked),
fs_global: false,
timers: true,
#[cfg(feature = "fetch")]
fetch: Some(Arc::new(crate::fetch::Client::new())),
extensions: Vec::new(),
},
}
}
}
impl Builder {
#[must_use]
pub fn limits(mut self, limits: Limits) -> Self {
self.config.limits = limits;
self
}
#[must_use]
pub fn console(mut self, console: ConsoleOptions) -> Self {
self.config.console = console;
self
}
#[must_use]
pub fn realm(mut self, realm: RealmOptions) -> Self {
self.config.realm = realm;
self
}
#[must_use]
pub fn modules(mut self, policy: ModulePolicy) -> Self {
self.config.modules = policy;
self
}
#[must_use]
pub fn process(mut self, process: ProcessOptions) -> Self {
self.config.process = process;
self
}
#[must_use]
pub fn identity(mut self, identity: ferrijs_std::identity::Identity) -> Self {
self.config.identity = identity;
self
}
#[must_use]
pub fn permissions(mut self, permissions: Permissions) -> Self {
self.config.permissions = Arc::new(Container::new(permissions));
self
}
#[must_use]
pub fn permission_container(mut self, container: Container) -> Self {
self.config.permissions = Arc::new(container);
self
}
#[must_use]
pub fn redactor(mut self, redactor: Arc<dyn Redactor>) -> Self {
self.config.redactor = Some(redactor);
self
}
#[must_use]
pub fn pause_clock(mut self, clock: Arc<dyn PauseClock>) -> Self {
self.config.pause_clock = clock;
self
}
#[must_use]
pub fn fs_global(mut self, on: bool) -> Self {
self.config.fs_global = on;
self
}
#[must_use]
pub fn timers(mut self, on: bool) -> Self {
self.config.timers = on;
self
}
#[cfg(feature = "fetch")]
#[must_use]
pub fn fetch(mut self, backend: Arc<dyn crate::fetch::FetchBackend>) -> Self {
self.config.fetch = Some(backend);
self
}
#[cfg(feature = "fetch")]
#[must_use]
pub fn without_fetch(mut self) -> Self {
self.config.fetch = None;
self
}
#[must_use]
pub fn extension(mut self, extension: impl Extension + 'static) -> Self {
self.config.extensions.push(Arc::new(extension));
self
}
#[must_use]
pub fn extension_arc(mut self, extension: Arc<dyn Extension>) -> Self {
self.config.extensions.push(extension);
self
}
pub async fn build(self) -> Result<Runtime, ScriptError> {
Runtime::create(self.config).await
}
}
struct VmHandleUd(VmHandle);
#[allow(unsafe_code)]
unsafe impl rquickjs::JsLifetime<'_> for VmHandleUd {
type Changed<'to> = VmHandleUd;
}
#[must_use]
pub fn vm_handle(ctx: &Ctx<'_>) -> Option<VmHandle> {
ctx.userdata::<VmHandleUd>().map(|ud| ud.0.clone())
}
struct RegistryUd(Arc<ModuleRegistry>);
#[allow(unsafe_code)]
unsafe impl rquickjs::JsLifetime<'_> for RegistryUd {
type Changed<'to> = RegistryUd;
}
#[must_use]
pub fn registry(ctx: &Ctx<'_>) -> Option<Arc<ModuleRegistry>> {
ctx.userdata::<RegistryUd>().map(|ud| Arc::clone(&ud.0))
}
#[derive(Debug)]
pub struct Run<T> {
pub result: Result<T, ScriptError>,
pub duration_ms: u64,
pub console: Vec<ConsoleEntry>,
pub poisoned: bool,
}
impl<T> Run<T> {
#[must_use]
pub fn is_ok(&self) -> bool {
self.result.is_ok()
}
#[must_use]
pub fn err(&self) -> Option<&ScriptError> {
self.result.as_ref().err()
}
pub fn map<U>(self, f: impl FnOnce(T) -> U) -> Run<U> {
Run {
result: self.result.map(f),
duration_ms: self.duration_ms,
console: self.console,
poisoned: self.poisoned,
}
}
}
impl Run<serde_json::Value> {
#[must_use]
pub fn into_result(self) -> crate::result::ScriptResult {
match self.result {
Ok(value) => crate::result::ScriptResult::ok(value, self.duration_ms, self.console),
Err(error) => crate::result::ScriptResult::err(error, self.duration_ms, self.console),
}
}
}
pub type RunBody<T> =
Box<dyn for<'js> FnOnce(Ctx<'js>) -> Pin<Box<dyn Future<Output = Result<T, ScriptError>> + 'js>> + Send>;
pub struct Runtime {
engine: AsyncRuntime,
vm: VmHandle,
_vm_shutdown: VmShutdown,
config: Config,
registry: Arc<ModuleRegistry>,
applied: AppliedLimits,
timeout: Arc<TimeoutState>,
poisoned: AtomicBool,
base_console: Arc<ConsoleCapture>,
}
impl std::fmt::Debug for Runtime {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Runtime")
.field("config", &self.config)
.field("poisoned", &self.poisoned())
.finish_non_exhaustive()
}
}
impl Runtime {
#[must_use]
pub fn builder() -> Builder {
Builder::default()
}
async fn create(config: Config) -> Result<Self, ScriptError> {
let runtime = AsyncRuntime::new().map_err(|e| ScriptError::internal(format!("rquickjs runtime init: {e}")))?;
runtime.set_memory_limit(config.limits.memory).await;
runtime.set_max_stack_size(config.limits.stack).await;
runtime.set_gc_threshold(config.limits.gc_threshold).await;
let timeout = Arc::new(TimeoutState::new(Arc::clone(&config.pause_clock)));
{
let state = Arc::clone(&timeout);
runtime
.set_interrupt_handler(Some(Box::new(move || {
if state.expired() {
state.timed_out.store(true, Ordering::Relaxed);
true
} else {
false
}
})))
.await;
}
let mut registry = ModuleRegistry::with_std();
registry.retain(|specifier| config.modules.serves_builtin(specifier));
let mut resolvers: Vec<BoxResolver> = Vec::new();
let mut loaders: Vec<BoxLoader> = Vec::new();
let mut require_hooks: Vec<Arc<dyn RequireHook>> = Vec::new();
for extension in &config.extensions {
extension
.modules(&mut registry)
.map_err(|e| ScriptError::internal(format!("extension `{}`: {e}", extension.name())))?;
for (resolver, loader) in extension.loaders() {
resolvers.push(resolver);
loaders.push(loader);
}
if let Some(hook) = extension.require_hook() {
require_hooks.push(hook);
}
}
let registry = Arc::new(registry);
let (native_resolver, native_loader) = registry.loader();
runtime
.set_loader(
(
native_resolver,
ResolverChain(resolvers),
FileResolver::new(config.modules.clone()),
),
(native_loader, LoaderChain(loaders), FileLoader::new(&config.modules)),
)
.await;
let ctx = AsyncContext::full(&runtime)
.await
.map_err(|e| ScriptError::internal(format!("rquickjs context init: {e}")))?;
let (vm, vm_shutdown) = spawn_vm_loop(&ctx);
let base_console = Arc::new(Self::console_capture(&config));
let install_console_capture = Arc::clone(&base_console);
let kept_console = Arc::clone(&base_console);
let install_registry = Arc::clone(®istry);
let ud_vm = vm.clone();
let permissions = Arc::clone(&config.permissions);
let identity = config.identity.clone();
let process = ferrijs_std::node::process::ProcessOptions {
env: permissions.permissions().env_snapshot(),
cwd: config
.process
.cwd
.clone()
.unwrap_or_else(|| config.modules.root.to_string_lossy().into_owned()),
argv: config.process.argv.clone(),
};
let fs_global = config.fs_global;
let timers = config.timers;
#[cfg(feature = "fetch")]
let fetch_backend = config.fetch.clone();
let realm = config.realm.clone();
let extensions = config.extensions.clone();
let installed: Result<Result<(), ScriptError>, ScriptError> = vm_with!(vm => |ctx| {
let _ = ctx.store_userdata(VmHandleUd(ud_vm));
let _ = ctx.store_userdata(RegistryUd(Arc::clone(&install_registry)));
ferrijs_std::permissions::install(&ctx, permissions);
ferrijs_std::identity::set(&ctx, identity);
let fail = |what: &str, e: rquickjs::Error| ScriptError::internal(format!("failed to install {what}: {e}"));
ferrijs_std::init(&ctx).map_err(|e| fail("the standard library", e))?;
if timers {
crate::timers::install(&ctx).map_err(|e| fail("timers", e))?;
}
ferrijs_std::node::process::install(&ctx, &process).map_err(|e| fail("process", e))?;
if fs_global {
ferrijs_std::fs::init(&ctx).map_err(|e| fail("fs", e))?;
}
crate::modules::require::install(&ctx, install_registry, require_hooks).map_err(|e| fail("require", e))?;
#[cfg(feature = "fetch")]
if let Some(backend) = fetch_backend {
crate::fetch::install(&ctx, backend).map_err(|e| fail("fetch", e))?;
}
install_console(&ctx, base_console).map_err(|e| fail("console", e))?;
for extension in &extensions {
extension
.install_async(ctx.clone())
.await
.map_err(|e| ScriptError::internal(format!("extension `{}` failed to install: {e}", extension.name())))?;
}
for entry in install_console_capture.drain() {
tracing::info!(target: "ferrijs::extensions", "{}", entry.message);
}
crate::realm::lockdown(&ctx, &realm).map_err(|e| fail("the realm lockdown", e))?;
Ok(())
})
.await;
installed??;
let applied = AppliedLimits::new(&config.limits);
Ok(Self {
engine: runtime,
vm,
_vm_shutdown: vm_shutdown,
config,
registry,
applied,
timeout,
poisoned: AtomicBool::new(false),
base_console: kept_console,
})
}
fn console_capture(config: &Config) -> ConsoleCapture {
let capture = ConsoleCapture::new(
config.console.max_entries,
config.console.max_bytes,
config.console.max_entry_bytes,
);
let capture = match &config.redactor {
Some(redactor) => capture.with_redactor(Arc::clone(redactor)),
None => capture,
};
match &config.console.sink {
Some(sink) => capture.with_sink(Arc::clone(sink)),
None => capture,
}
}
#[must_use]
pub fn handle(&self) -> VmHandle {
self.vm.clone()
}
#[must_use]
pub fn config(&self) -> &Config {
&self.config
}
#[must_use]
pub fn registry(&self) -> &Arc<ModuleRegistry> {
&self.registry
}
#[must_use]
pub fn permissions(&self) -> &Arc<Container> {
&self.config.permissions
}
#[must_use]
pub fn poisoned(&self) -> bool {
self.poisoned.load(Ordering::Relaxed)
}
#[must_use]
pub fn deadline(&self) -> Deadline {
Deadline(Arc::clone(&self.timeout))
}
pub async fn with<R, F>(&self, f: F) -> Result<R, ScriptError>
where
R: Send + 'static,
F: for<'js> FnOnce(Ctx<'js>) -> Pin<Box<dyn Future<Output = R> + Send + 'js>> + Send + 'static,
{
self.vm.with(f).await
}
async fn apply_limits(&self, memory: usize, stack: usize, gc: usize) {
if self.applied.memory.swap(memory, Ordering::Relaxed) != memory {
self.engine.set_memory_limit(memory).await;
}
if self.applied.stack.swap(stack, Ordering::Relaxed) != stack {
self.engine.set_max_stack_size(stack).await;
}
if self.applied.gc.swap(gc, Ordering::Relaxed) != gc {
self.engine.set_gc_threshold(gc).await;
}
}
async fn apply_run_options(&self, options: &RunOptions) -> Duration {
let limits = &self.config.limits;
self
.apply_limits(
options.memory.unwrap_or(limits.memory),
options.stack.unwrap_or(limits.stack),
options.gc_threshold.unwrap_or(limits.gc_threshold),
)
.await;
options.timeout.unwrap_or(limits.timeout)
}
pub async fn run<T: Send + 'static>(&self, options: RunOptions, body: RunBody<T>) -> Run<T> {
let started = Instant::now();
let streaming = self.config.console.sink.is_some();
let console = if streaming {
Arc::clone(&self.base_console)
} else {
Arc::new(Self::console_capture(&self.config))
};
if self.poisoned() {
return Run {
result: Err(ScriptError::internal(
"this realm is poisoned by an earlier timeout or allocation fault; build a new one",
)),
duration_ms: 0,
console: Vec::new(),
poisoned: true,
};
}
let timeout = self.apply_run_options(&options).await;
let token = self.timeout.arm(started + timeout);
let run_console = Arc::clone(&console);
let fut = vm_with!(self.vm => |ctx| {
if !streaming
&& let Err(e) = install_console(&ctx, run_console)
{
return Err(ScriptError::internal(format!("failed to install console: {e}")));
}
body(ctx).await
});
let backstop = timeout.saturating_add(self.config.limits.backstop_grace);
let outcome = match run_within(self.timeout.clock(), backstop, fut).await {
Ok(r) => r.and_then(|inner| inner),
Err(_) => return self.finish_backstop(token, started, &console, timeout),
};
self.finish(token, outcome, started, &console, timeout)
}
fn finish<T>(
&self,
token: crate::limits::ArmToken,
outcome: Result<T, ScriptError>,
started: Instant,
console: &ConsoleCapture,
timeout: Duration,
) -> Run<T> {
self.timeout.disarm(token);
let duration_ms = elapsed_ms(started);
let drained = console.drain();
match outcome {
Ok(value) => Run {
result: Ok(value),
duration_ms,
console: drained,
poisoned: false,
},
Err(mut err) => {
let timed_out = self.timeout.timed_out.load(Ordering::Relaxed);
let oom = is_oom(&err);
let poisoned = timed_out || oom;
if timed_out {
err = ScriptError::timeout(duration_ms, timeout.as_millis().try_into().unwrap_or(u64::MAX));
} else if oom {
err.kind = ScriptErrorKind::MemoryLimit;
}
if let Some(redactor) = &self.config.redactor {
err.redact(redactor.as_ref());
}
if poisoned {
self.poisoned.store(true, Ordering::Relaxed);
}
Run {
result: Err(err),
duration_ms,
console: drained,
poisoned,
}
},
}
}
fn finish_backstop<T>(
&self,
token: crate::limits::ArmToken,
started: Instant,
console: &ConsoleCapture,
timeout: Duration,
) -> Run<T> {
self.timeout.disarm(token);
let poisoned = self.config.limits.backstop_poisons;
if poisoned {
self.poisoned.store(true, Ordering::Relaxed);
}
let duration_ms = elapsed_ms(started);
Run {
result: Err(ScriptError::timeout(
duration_ms,
timeout.as_millis().try_into().unwrap_or(u64::MAX),
)),
duration_ms,
console: console.drain(),
poisoned,
}
}
pub async fn eval_script(
&self,
source: &str,
args: &[serde_json::Value],
options: RunOptions,
) -> Run<serde_json::Value> {
let source = source.to_string();
let args = args.to_vec();
let run = self
.run(
options,
Box::new(move |ctx| Box::pin(async move { script_body(&ctx, &source, &args).await })),
)
.await;
self.redact_run(run)
}
pub async fn eval_module(
&self,
module: &CompiledModule,
args: &[serde_json::Value],
options: RunOptions,
) -> Run<serde_json::Value> {
let bytecode = Arc::clone(&module.bytecode);
let mapper = module.mapper();
let args = args.to_vec();
let run = self
.run(
options,
Box::new(move |ctx| Box::pin(async move { module_body(&ctx, &bytecode, mapper, &args).await })),
)
.await;
let run = run.map_err_pos(|e| {
if let Some(line) = e.line
&& let Some((src, sl, sc)) = module.remap(line, e.column.unwrap_or(1))
{
e.message = format!("{} (at {src}:{sl}:{sc})", e.message);
}
});
self.redact_run(run)
}
pub fn redact_value(&self, value: &mut serde_json::Value) {
if let Some(redactor) = &self.config.redactor {
crate::redact::redact_json(redactor.as_ref(), value);
}
}
fn redact_run(&self, mut run: Run<serde_json::Value>) -> Run<serde_json::Value> {
if let Ok(value) = &mut run.result {
self.redact_value(value);
}
run
}
pub async fn eval_module_source(
&self,
name: &str,
source: &str,
args: &[serde_json::Value],
options: RunOptions,
) -> Run<serde_json::Value> {
let name = name.to_string();
let source = source.to_string();
let args = args.to_vec();
let run = self
.run(
options,
Box::new(move |ctx| {
Box::pin(async move {
install_args(&ctx, &args)?;
let declared = match Module::declare(ctx.clone(), name.as_str(), source.as_bytes()).catch(&ctx) {
Ok(m) => m,
Err(e) => return Err(ScriptError::from_caught(&ctx, e, &source)),
};
let (evaluated, promise) = match declared.eval().catch(&ctx) {
Ok(v) => v,
Err(e) => return Err(ScriptError::from_caught(&ctx, e, &source)),
};
if let Err(e) = promise.into_future::<()>().await.catch(&ctx) {
return Err(ScriptError::from_caught(&ctx, e, &source));
}
let default = evaluated
.namespace()
.and_then(|ns| ns.get::<_, Value<'_>>("default"))
.unwrap_or_else(|_| Value::new_undefined(ctx.clone()));
Ok(crate::value::value_to_json(&ctx, default).unwrap_or(serde_json::Value::Null))
})
}),
)
.await;
self.redact_run(run)
}
}
pub async fn script_body(
ctx: &Ctx<'_>,
source: &str,
args: &[serde_json::Value],
) -> Result<serde_json::Value, ScriptError> {
install_args(ctx, args)?;
let wrapped = format!("(async () => {{\n{source}\n}})()");
let promise: rquickjs::Promise<'_> = ctx
.eval(wrapped.as_bytes())
.map_err(|e| ScriptError::from_caught_offset(ctx, rquickjs::CaughtError::from_error(ctx, e), source, 1))?;
let value: Value<'_> = promise
.into_future::<Value<'_>>()
.await
.map_err(|e| ScriptError::from_caught_offset(ctx, rquickjs::CaughtError::from_error(ctx, e), source, 1))?;
Ok(crate::value::value_to_json(ctx, value).unwrap_or(serde_json::Value::Null))
}
pub async fn module_body(
ctx: &Ctx<'_>,
bytecode: &[u8],
mapper: SourceMapper,
args: &[serde_json::Value],
) -> Result<serde_json::Value, ScriptError> {
let label = mapper.module_name.clone();
crate::source_map::register_bundle(ctx, mapper);
install_args(ctx, args)?;
let evaluated = eval_bytecode(ctx, bytecode, &label).await?;
let default = evaluated
.namespace()
.and_then(|ns| ns.get::<_, Value<'_>>("default"))
.unwrap_or_else(|_| Value::new_undefined(ctx.clone()));
Ok(crate::value::value_to_json(ctx, default).unwrap_or(serde_json::Value::Null))
}
pub async fn eval_bytecode<'js>(
ctx: &Ctx<'js>,
bytecode: &[u8],
label: &str,
) -> Result<Module<'js, rquickjs::module::Evaluated>, ScriptError> {
#[allow(unsafe_code)]
let declared = match (unsafe { Module::load(ctx.clone(), bytecode) }).catch(ctx) {
Ok(m) => m,
Err(e) => return Err(ScriptError::from_caught(ctx, e, label)),
};
let (evaluated, promise) = match declared.eval().catch(ctx) {
Ok(v) => v,
Err(e) => return Err(ScriptError::from_caught(ctx, e, label)),
};
if let Err(e) = promise.into_future::<()>().await.catch(ctx) {
return Err(ScriptError::from_caught(ctx, e, label));
}
Ok(evaluated)
}
impl<T> Run<T> {
fn map_err_pos(mut self, f: impl FnOnce(&mut ScriptError)) -> Self {
if let Err(e) = &mut self.result {
f(e);
}
self
}
}
pub fn install_args(ctx: &Ctx<'_>, args: &[serde_json::Value]) -> Result<(), ScriptError> {
let array = rquickjs::Array::new(ctx.clone()).map_err(|e| ScriptError::internal(format!("args: {e}")))?;
for (i, a) in args.iter().enumerate() {
let v = crate::value::json_to_js(ctx, a).map_err(|e| ScriptError::internal(format!("args[{i}]: {e}")))?;
array
.set(i, v)
.map_err(|e| ScriptError::internal(format!("args[{i}]: {e}")))?;
}
ctx
.globals()
.set("args", array)
.map_err(|e| ScriptError::internal(format!("args: {e}")))
}
fn is_oom(err: &ScriptError) -> bool {
err.kind == ScriptErrorKind::MemoryLimit || err.message.to_ascii_lowercase().contains("out of memory")
}
fn elapsed_ms(started: Instant) -> u64 {
u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX)
}