diff --git a/boa_cli/src/debug/snapshot.rs b/boa_cli/src/debug/snapshot.rs index 8883ea718b..83d22412e7 100644 --- a/boa_cli/src/debug/snapshot.rs +++ b/boa_cli/src/debug/snapshot.rs @@ -1,29 +1,59 @@ -use std::{fs::OpenOptions, io::Write}; +use std::{ + fs::{File, OpenOptions}, + io::{Read, Write}, +}; use boa_engine::{ - object::ObjectInitializer, snapshot::SnapshotSerializer, Context, JsNativeError, JsObject, - JsResult, JsValue, NativeFunction, + object::ObjectInitializer, + snapshot::{Snapshot, SnapshotSerializer}, + Context, JsNativeError, JsObject, JsResult, JsValue, NativeFunction, }; const SNAPSHOT_PATH: &str = "./snapshot.bin"; +fn get_file_as_byte_vec(filename: &str) -> Vec { + let mut f = File::open(&filename).expect("no file found"); + let metadata = std::fs::metadata(&filename).expect("unable to read metadata"); + let mut buffer = vec![0; metadata.len() as usize]; + f.read(&mut buffer).expect("buffer overflow"); + + buffer +} + fn create(_: &JsValue, _: &[JsValue], context: &mut Context<'_>) -> JsResult { let Ok(mut file) = OpenOptions::new().write(true).create(true).open(SNAPSHOT_PATH) else { return Err(JsNativeError::error().with_message("could not create snapshot.bin file").into()); }; - let mut serializer = SnapshotSerializer::new(); + let serializer = SnapshotSerializer::new(); - serializer.serialize(context).unwrap(); + let snapshot = serializer.serialize(context).unwrap(); - file.write_all(serializer.bytes()).unwrap(); + file.write_all(snapshot.bytes()).unwrap(); file.flush().unwrap(); Ok(JsValue::undefined()) } +fn load(_: &JsValue, _: &[JsValue], context: &mut Context<'_>) -> JsResult { + // let Ok(mut file) = OpenOptions::new().read(true).open(SNAPSHOT_PATH) else { + // return Err(JsNativeError::error().with_message("could not open snapshot.bin file").into()); + // }; + + let bytes = get_file_as_byte_vec(SNAPSHOT_PATH); + + let snapshot = Snapshot::new(bytes); + + let deser_context = snapshot.deserialize().unwrap(); + + *context = deser_context; + + Ok(JsValue::undefined()) +} + pub(super) fn create_object(context: &mut Context<'_>) -> JsObject { ObjectInitializer::new(context) .function(NativeFunction::from_fn_ptr(create), "create", 0) + .function(NativeFunction::from_fn_ptr(load), "load", 1) .build() } diff --git a/boa_cli/src/main.rs b/boa_cli/src/main.rs index da705e54b1..7954c42ed3 100644 --- a/boa_cli/src/main.rs +++ b/boa_cli/src/main.rs @@ -72,6 +72,7 @@ use boa_engine::{ optimizer::OptimizerOptions, property::Attribute, script::Script, + snapshot::Snapshot, vm::flowgraph::{Direction, Graph}, Context, JsError, JsNativeError, JsResult, Source, }; @@ -81,7 +82,13 @@ use colored::Colorize; use debug::init_boa_debug_object; use rustyline::{config::Config, error::ReadlineError, EditMode, Editor}; use std::{ - cell::RefCell, collections::VecDeque, eprintln, fs::read, fs::OpenOptions, io, path::PathBuf, + cell::RefCell, + collections::VecDeque, + eprintln, + fs::OpenOptions, + fs::{read, File}, + io::{self, Read}, + path::{Path, PathBuf}, println, }; @@ -168,6 +175,9 @@ struct Opt { /// Root path from where the module resolver will try to load the modules. #[arg(long, short = 'r', default_value_os_t = PathBuf::from("."), requires = "mod")] root: PathBuf, + + #[arg(long)] + snapshot: Option, } impl Opt { @@ -363,37 +373,60 @@ fn evaluate_files( Ok(()) } +fn get_file_as_byte_vec(filename: &Path) -> Vec { + let mut f = File::open(&filename).expect("no file found"); + let metadata = std::fs::metadata(&filename).expect("unable to read metadata"); + let mut buffer = vec![0; metadata.len() as usize]; + f.read(&mut buffer).expect("buffer overflow"); + + buffer +} + fn main() -> Result<(), io::Error> { let args = Opt::parse(); - let queue: &dyn JobQueue = &Jobs::default(); let loader = &SimpleModuleLoader::new(&args.root) .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; + + let queue: &dyn JobQueue = &Jobs::default(); let dyn_loader: &dyn ModuleLoader = loader; - let mut context = ContextBuilder::new() - .job_queue(queue) - .module_loader(dyn_loader) - .build() - .expect("cannot fail with default global object"); - // Strict mode - context.strict(args.strict); + let mut context = if let Some(path) = &args.snapshot { + let bytes = get_file_as_byte_vec(&path); - // Add `console`. - add_runtime(&mut context); + let snapshot = Snapshot::new(bytes); - // Trace Output - context.set_trace(args.trace); + let deser_context = snapshot.deserialize().unwrap(); - if args.debug_object { - init_boa_debug_object(&mut context); - } + deser_context + } else { + let mut context = ContextBuilder::new() + .job_queue(queue) + .module_loader(dyn_loader) + .build() + .expect("cannot fail with default global object"); + + // Strict mode + context.strict(args.strict); + + // Add `console`. + add_runtime(&mut context); + + // Trace Output + context.set_trace(args.trace); - // Configure optimizer options - let mut optimizer_options = OptimizerOptions::empty(); - optimizer_options.set(OptimizerOptions::STATISTICS, args.optimizer_statistics); - optimizer_options.set(OptimizerOptions::OPTIMIZE_ALL, args.optimize); - context.set_optimizer_options(optimizer_options); + if args.debug_object { + init_boa_debug_object(&mut context); + } + + // Configure optimizer options + let mut optimizer_options = OptimizerOptions::empty(); + optimizer_options.set(OptimizerOptions::STATISTICS, args.optimizer_statistics); + optimizer_options.set(OptimizerOptions::OPTIMIZE_ALL, args.optimize); + context.set_optimizer_options(optimizer_options); + + context + }; if args.files.is_empty() { let config = Config::builder() diff --git a/boa_engine/src/context/mod.rs b/boa_engine/src/context/mod.rs index c1c0b4be66..96e950d2ea 100644 --- a/boa_engine/src/context/mod.rs +++ b/boa_engine/src/context/mod.rs @@ -996,3 +996,16 @@ impl crate::snapshot::Serialize for Context<'_> { Ok(()) } } + +impl crate::snapshot::Deserialize for Context<'_> { + fn deserialize( + d: &mut crate::snapshot::SnapshotDeserializer<'_>, + ) -> Result { + let strict = d.read_bool()?; + let mut context = Context::default(); + + context.strict(strict); + + Ok(context) + } +} diff --git a/boa_engine/src/snapshot/mod.rs b/boa_engine/src/snapshot/mod.rs index 417ebc59d1..cbcbe4bf70 100644 --- a/boa_engine/src/snapshot/mod.rs +++ b/boa_engine/src/snapshot/mod.rs @@ -7,6 +7,18 @@ use crate::{Context, JsBigInt, JsObject, JsString, JsSymbol}; use indexmap::{IndexMap, IndexSet}; use std::fmt::{Debug, Display}; +/// TODO: doc +pub trait Serialize { + /// Serialize type + fn serialize(&self, s: &mut SnapshotSerializer) -> Result<(), SnapshotError>; +} + +/// TODO: doc +pub trait Deserialize: Sized { + /// TODO: doc + fn deserialize(d: &mut SnapshotDeserializer<'_>) -> Result; +} + /// TODO: doc #[derive(Debug, Clone, Copy)] pub struct Header { @@ -15,12 +27,6 @@ pub struct Header { // checksum: u64, } -/// TODO: doc -pub trait Serialize { - /// Serialize type - fn serialize(&self, s: &mut SnapshotSerializer) -> Result<(), SnapshotError>; -} - impl Serialize for Header { fn serialize(&self, s: &mut SnapshotSerializer) -> Result<(), SnapshotError> { s.write_bytes(&self.signature)?; @@ -29,6 +35,17 @@ impl Serialize for Header { } } +impl Deserialize for Header { + fn deserialize(d: &mut SnapshotDeserializer<'_>) -> Result { + let signature = d.read_bytes(4)?; + let signature = [signature[0], signature[1], signature[2], signature[3]]; + + let version = d.read_u32()?; + + Ok(Self { signature, version }) + } +} + impl Serialize for JsObject { fn serialize(&self, s: &mut SnapshotSerializer) -> Result<(), SnapshotError> { let value = s.objects.insert_full(self.clone()).0; @@ -40,8 +57,149 @@ impl Serialize for JsObject { /// TODO: doc pub struct Snapshot { - header: Header, bytes: Vec, + external_references: IndexSet, +} + +impl Snapshot { + /// TODO: doc + pub fn new(bytes: Vec) -> Self { + Self { + bytes, + external_references: IndexSet::default(), + } + } + + /// TODO: doc + pub fn bytes(&self) -> &[u8] { + &self.bytes + } + + /// TODO: doc + pub fn deserialize<'a>(&self) -> Result, SnapshotError> { + let mut deserializer = SnapshotDeserializer { + index: 0, + bytes: &self.bytes, + external_references: &self.external_references, + }; + + let header = Header::deserialize(&mut deserializer)?; + + // TODO: Do error handling and snapshot integrity checks. + assert_eq!(&header.signature, b".boa"); + assert_eq!(header.version, 42); + + let context = Context::deserialize(&mut deserializer)?; + + // Assert that all bytes are consumed. + // assert_eq!(deserializer.index, deserializer.bytes.len()); + + Ok(context) + } +} + +/// TODO: doc +pub struct SnapshotDeserializer<'snapshot> { + bytes: &'snapshot [u8], + index: usize, + external_references: &'snapshot IndexSet, +} + +impl SnapshotDeserializer<'_> { + /// TODO: doc + pub fn read_bool(&mut self) -> Result { + let byte = self.read_u8()?; + assert!(byte == 0 || byte == 1); + Ok(byte == 1) + } + /// TODO: doc + pub fn read_u8(&mut self) -> Result { + let byte = self.bytes[self.index]; + self.index += 1; + Ok(byte) + } + /// TODO: doc + pub fn read_i8(&mut self) -> Result { + let byte = self.bytes[self.index]; + self.index += 1; + Ok(byte as i8) + } + + /// TODO: doc + pub fn read_u16(&mut self) -> Result { + let bytes = self.read_bytes(std::mem::size_of::())?; + let value = u16::from_le_bytes([bytes[0], bytes[1]]); + Ok(value) + } + /// TODO: doc + pub fn read_i16(&mut self) -> Result { + let value = self.read_u16()?; + Ok(value as i16) + } + + /// TODO: doc + pub fn read_u32(&mut self) -> Result { + let bytes = self.read_bytes(4)?; + let value = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]); + Ok(value) + } + /// TODO: doc + pub fn read_i32(&mut self) -> Result { + let value = self.read_u32()?; + Ok(value as i32) + } + + /// TODO: doc + pub fn read_f32(&mut self) -> Result { + let value = self.read_u32()?; + Ok(f32::from_bits(value)) + } + /// TODO: doc + pub fn read_f64(&mut self) -> Result { + let value = self.read_u64()?; + Ok(f64::from_bits(value)) + } + + /// TODO: doc + pub fn read_u64(&mut self) -> Result { + let bytes = self.read_bytes(std::mem::size_of::())?; + let value = u64::from_le_bytes([ + bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], + ]); + Ok(value) + } + /// TODO: doc + pub fn read_i64(&mut self) -> Result { + let value = self.read_u64()?; + Ok(value as i64) + } + + /// TODO: doc + pub fn read_usize(&mut self) -> Result { + let value = self.read_u64()?; + // TODO: handle error. + Ok(usize::try_from(value).unwrap()) + } + /// TODO: doc + pub fn read_isize(&mut self) -> Result { + let value = self.read_usize()?; + Ok(value as isize) + } + /// TODO: doc + pub fn read_string(&mut self) -> Result<&str, SnapshotError> { + let len = self.read_usize()?; + let bytes = self.read_bytes(len)?; + // TODO: handle error + Ok(std::str::from_utf8(bytes).unwrap()) + } + /// TODO: doc + pub fn read_bytes(&mut self, count: usize) -> Result<&[u8], SnapshotError> { + let index = self.index; + self.index += count; + // TODO: use .get() so we can handle the error. + let bytes = &self.bytes[index..(index + count)]; + Ok(bytes) + } } /// TODO: doc @@ -91,18 +249,12 @@ impl SnapshotSerializer { } } - /// TODO: doc - pub fn bytes(&self) -> &[u8] { - &self.bytes - } - /// Serialize the given [`Context`]. - pub fn serialize(&mut self, context: &mut Context<'_>) -> Result<(), SnapshotError> { + pub fn serialize(mut self, context: &mut Context<'_>) -> Result { // Remove any garbage objects before serialization. boa_gc::force_collect(); // boa_gc::walk_gc_alloc_pointers(|address| { - // }); let header = Header { @@ -110,8 +262,8 @@ impl SnapshotSerializer { version: 42, }; - header.serialize(self)?; - context.serialize(self)?; + header.serialize(&mut self)?; + context.serialize(&mut self)?; for i in 0..self.objects.len() { let object = self @@ -119,7 +271,7 @@ impl SnapshotSerializer { .get_index(i) .expect("There should be an object") .clone(); - object.inner().serialize(self)?; + object.inner().serialize(&mut self)?; } for i in 0..self.symbols.len() { @@ -132,7 +284,7 @@ impl SnapshotSerializer { self.write_u64(hash)?; if let Some(desc) = symbol.description() { self.write_bool(true)?; - desc.serialize(self)?; + desc.serialize(&mut self)?; } else { self.write_bool(false)?; } @@ -146,7 +298,7 @@ impl SnapshotSerializer { .1 .clone(); // string. - string.serialize(self)?; + string.serialize(&mut self)?; self.write_bool(string.is_static())?; self.write_usize(string.len())?; @@ -155,7 +307,10 @@ impl SnapshotSerializer { } } - Ok(()) + Ok(Snapshot { + bytes: self.bytes, + external_references: self.external_references, + }) } /// TODO: doc diff --git a/docs/snapshot.md b/docs/snapshot.md index 1d9c1803e2..df0a1621a0 100644 --- a/docs/snapshot.md +++ b/docs/snapshot.md @@ -61,6 +61,6 @@ If it's a static string then it's elements comprise the index into the `STATIC_S ## JsValue Encoding -type `: u8` (JsValue discriminant, Boolean, Null, etc) followed by the value if it exits. +type `: u8` (JsValue discriminant, Boolean, Null, etc) followed by the value if it applied for the type (`Null` and `Undefined` does not have a value part). If following the `JsValue` is an `JsString`, `JsSymbol`, `JsBigInt`, `JsObject` then the following value will be an index into the appropriate tables.