rw_deno_core/runtime/
snapshot.rs

1// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
2
3use anyhow::Error;
4use serde::Deserialize;
5use serde::Serialize;
6use std::collections::HashMap;
7use std::path::Path;
8use std::path::PathBuf;
9use std::rc::Rc;
10use std::time::Instant;
11
12use crate::modules::ModuleMapSnapshotData;
13use crate::Extension;
14use crate::JsRuntimeForSnapshot;
15use crate::RuntimeOptions;
16
17use super::ExtensionTranspiler;
18
19pub type WithRuntimeCb = dyn Fn(&mut JsRuntimeForSnapshot);
20
21pub type SnapshotDataId = u32;
22
23/// We use this constant a few times
24const ULEN: usize = std::mem::size_of::<usize>();
25
26/// The v8 lifetime is different than the sidecar data, so we
27/// allow for it to be split out.
28pub(crate) struct V8Snapshot(pub(crate) &'static [u8]);
29
30pub(crate) fn deconstruct(
31  slice: &'static [u8],
32) -> (V8Snapshot, SerializableSnapshotSidecarData) {
33  let len =
34    usize::from_le_bytes(slice[slice.len() - ULEN..].try_into().unwrap());
35  let data = SerializableSnapshotSidecarData::from_slice(
36    &slice[len..slice.len() - ULEN],
37  );
38  (V8Snapshot(&slice[0..len]), data)
39}
40
41pub(crate) fn serialize(
42  v8_data: v8::StartupData,
43  sidecar_data: SerializableSnapshotSidecarData,
44) -> Box<[u8]> {
45  let v8_data_len = v8_data.len();
46  let sidecar_data = sidecar_data.into_bytes();
47  let mut data = Vec::with_capacity(v8_data_len + sidecar_data.len() + ULEN);
48
49  // add ulen
50  data.extend_from_slice(&v8_data);
51  data.extend_from_slice(&sidecar_data);
52  data.extend_from_slice(&v8_data_len.to_le_bytes());
53
54  data.into_boxed_slice()
55}
56
57#[derive(Default)]
58pub struct SnapshotLoadDataStore {
59  data: Vec<Option<v8::Global<v8::Data>>>,
60}
61
62impl SnapshotLoadDataStore {
63  pub fn get<'s, T>(
64    &mut self,
65    scope: &mut v8::HandleScope<'s>,
66    id: SnapshotDataId,
67  ) -> v8::Global<T>
68  where
69    v8::Local<'s, T>: TryFrom<v8::Local<'s, v8::Data>>,
70  {
71    let Some(data) = self.data.get_mut(id as usize) else {
72      panic!(
73        "Attempted to read snapshot data out of range: {id} (of {})",
74        self.data.len()
75      );
76    };
77    let Some(data) = data.take() else {
78      panic!("Attempted to read the snapshot data at index {id} twice");
79    };
80    let local = v8::Local::new(scope, data);
81    let local = v8::Local::<T>::try_from(local).unwrap_or_else(|_| {
82      panic!(
83        "Invalid data type at index {id}, expected '{}'",
84        std::any::type_name::<T>()
85      )
86    });
87    v8::Global::new(scope, local)
88  }
89}
90
91#[derive(Default)]
92pub struct SnapshotStoreDataStore {
93  data: Vec<v8::Global<v8::Data>>,
94}
95
96impl SnapshotStoreDataStore {
97  pub fn register<T>(&mut self, global: v8::Global<T>) -> SnapshotDataId
98  where
99    for<'s> v8::Local<'s, v8::Data>: From<v8::Local<'s, T>>,
100  {
101    let id = self.data.len();
102    // TODO(mmastrac): v8::Global needs From/Into
103    // SAFETY: Because we've tested that Local<Data>: From<Local<T>>, we can assume this is safe.
104    unsafe {
105      self.data.push(std::mem::transmute(global));
106    }
107    id as _
108  }
109}
110
111pub struct CreateSnapshotOptions {
112  pub cargo_manifest_dir: &'static str,
113  pub startup_snapshot: Option<&'static [u8]>,
114  pub skip_op_registration: bool,
115  pub extensions: Vec<Extension>,
116  pub extension_transpiler: Option<Rc<ExtensionTranspiler>>,
117  pub with_runtime_cb: Option<Box<WithRuntimeCb>>,
118}
119
120pub struct CreateSnapshotOutput {
121  /// Any files marked as LoadedFromFsDuringSnapshot are collected here and should be
122  /// printed as 'cargo:rerun-if-changed' lines from your build script.
123  pub files_loaded_during_snapshot: Vec<PathBuf>,
124  pub output: Box<[u8]>,
125}
126
127#[must_use = "The files listed by create_snapshot should be printed as 'cargo:rerun-if-changed' lines"]
128pub fn create_snapshot(
129  create_snapshot_options: CreateSnapshotOptions,
130  warmup_script: Option<&'static str>,
131) -> Result<CreateSnapshotOutput, Error> {
132  let mut mark = Instant::now();
133  println!("Creating a snapshot...",);
134
135  // Get the extensions for a second pass if we want to warm up the snapshot.
136  let warmup_exts = warmup_script.map(|_| {
137    create_snapshot_options
138      .extensions
139      .iter()
140      .map(|e| e.for_warmup())
141      .collect::<Vec<_>>()
142  });
143
144  let mut js_runtime = JsRuntimeForSnapshot::new(RuntimeOptions {
145    startup_snapshot: create_snapshot_options.startup_snapshot,
146    extensions: create_snapshot_options.extensions,
147    extension_transpiler: create_snapshot_options.extension_transpiler,
148    skip_op_registration: create_snapshot_options.skip_op_registration,
149    ..Default::default()
150  });
151
152  println!("JsRuntimeForSnapshot prepared, took {:#?}", mark.elapsed(),);
153  mark = Instant::now();
154
155  let files_loaded_during_snapshot = js_runtime
156    .files_loaded_from_fs_during_snapshot()
157    .iter()
158    .map(PathBuf::from)
159    .collect::<Vec<_>>();
160
161  if let Some(ref with_runtime_cb) = create_snapshot_options.with_runtime_cb {
162    with_runtime_cb(&mut js_runtime);
163  }
164
165  let mut snapshot = js_runtime.snapshot();
166  if let Some(warmup_script) = warmup_script {
167    let leaked_snapshot = Box::leak(snapshot);
168    let warmup_exts = warmup_exts.unwrap();
169
170    // Warm up the snapshot bootstrap.
171    //
172    // - Create a new isolate with cold snapshot blob.
173    // - Run warmup script in new context.
174    // - Serialize the new context into a new snapshot blob.
175    let mut js_runtime = JsRuntimeForSnapshot::new(RuntimeOptions {
176      startup_snapshot: Some(leaked_snapshot),
177      extensions: warmup_exts,
178      skip_op_registration: true,
179      ..Default::default()
180    });
181
182    if let Some(with_runtime_cb) = create_snapshot_options.with_runtime_cb {
183      with_runtime_cb(&mut js_runtime);
184    }
185
186    js_runtime.execute_script("warmup", warmup_script)?;
187
188    snapshot = js_runtime.snapshot();
189  }
190
191  println!(
192    "Snapshot size: {}, took {:#?}",
193    snapshot.len(),
194    mark.elapsed(),
195  );
196  mark = Instant::now();
197
198  println!(
199    "Snapshot written, took: {:#?}",
200    Instant::now().saturating_duration_since(mark),
201  );
202
203  Ok(CreateSnapshotOutput {
204    files_loaded_during_snapshot,
205    output: snapshot,
206  })
207}
208
209pub type FilterFn = Box<dyn Fn(&PathBuf) -> bool>;
210
211pub fn get_js_files(
212  cargo_manifest_dir: &'static str,
213  directory: &str,
214  filter: Option<FilterFn>,
215) -> Vec<PathBuf> {
216  let manifest_dir = Path::new(cargo_manifest_dir);
217  let mut js_files = std::fs::read_dir(directory)
218    .unwrap()
219    .map(|dir_entry| {
220      let file = dir_entry.unwrap();
221      manifest_dir.join(file.path())
222    })
223    .filter(|path| {
224      path.extension().unwrap_or_default() == "js"
225        && filter.as_ref().map(|filter| filter(path)).unwrap_or(true)
226    })
227    .collect::<Vec<PathBuf>>();
228  js_files.sort();
229  js_files
230}
231
232/// The data we intend to snapshot, separated from any V8 objects that
233/// are stored in the [`SnapshotLoadDataStore`]/[`SnapshotStoreDataStore`].
234#[derive(Serialize, Deserialize)]
235pub(crate) struct SnapshottedData<'snapshot> {
236  pub js_handled_promise_rejection_cb: Option<u32>,
237  pub module_map_data: ModuleMapSnapshotData,
238  pub externals_count: u32,
239  pub extension_count: usize,
240  pub op_count: usize,
241  pub source_count: usize,
242  pub addl_refs_count: usize,
243  #[serde(borrow)]
244  pub ext_source_maps: HashMap<String, &'snapshot [u8]>,
245  #[serde(borrow)]
246  pub external_strings: Vec<&'snapshot [u8]>,
247}
248
249/// Snapshot sidecar data, containing a [`SnapshottedData`] and the length of the
250/// associated data array. This is the final form of the [`SnapshottedData`] before
251/// we hand it off to serde.
252#[derive(Serialize, Deserialize)]
253pub(crate) struct SerializableSnapshotSidecarData<'snapshot> {
254  data_count: u32,
255  #[serde(borrow)]
256  pub snapshot_data: SnapshottedData<'snapshot>,
257}
258
259impl<'snapshot> SerializableSnapshotSidecarData<'snapshot> {
260  fn from_slice(slice: &'snapshot [u8]) -> Self {
261    bincode::deserialize(slice).expect("Failed to deserialize snapshot data")
262  }
263
264  fn into_bytes(self) -> Box<[u8]> {
265    bincode::serialize(&self).unwrap().into_boxed_slice()
266  }
267}
268
269/// Given the sidecar data and a scope to extract data from, reconstructs the
270/// `SnapshottedData` and `SnapshotLoadDataStore`.
271pub(crate) fn load_snapshotted_data_from_snapshot<'snapshot>(
272  scope: &mut v8::HandleScope<()>,
273  context: v8::Local<v8::Context>,
274  raw_data: SerializableSnapshotSidecarData<'snapshot>,
275) -> (SnapshottedData<'snapshot>, SnapshotLoadDataStore) {
276  let scope = &mut v8::ContextScope::new(scope, context);
277  let mut data = SnapshotLoadDataStore::default();
278  for i in 0..raw_data.data_count {
279    let item = scope
280      .get_context_data_from_snapshot_once::<v8::Data>(i as usize)
281      .unwrap();
282    let item = v8::Global::new(scope, item);
283    data.data.push(Some(item));
284  }
285
286  (raw_data.snapshot_data, data)
287}
288
289/// Given a `SnapshottedData` and `SnapshotStoreDataStore`, attaches the data to the
290/// context and returns the serialized sidecar data.
291pub(crate) fn store_snapshotted_data_for_snapshot<'snapshot>(
292  scope: &mut v8::HandleScope,
293  context: v8::Global<v8::Context>,
294  snapshotted_data: SnapshottedData<'snapshot>,
295  data_store: SnapshotStoreDataStore,
296) -> SerializableSnapshotSidecarData<'snapshot> {
297  let context = v8::Local::new(scope, context);
298  let raw_snapshot_data = SerializableSnapshotSidecarData {
299    data_count: data_store.data.len() as _,
300    snapshot_data: snapshotted_data,
301  };
302
303  for data in data_store.data {
304    let data = v8::Local::new(scope, data);
305    scope.add_context_data(context, data);
306  }
307
308  raw_snapshot_data
309}
310
311/// Returns an isolate set up for snapshotting.
312pub(crate) fn create_snapshot_creator(
313  external_refs: &'static v8::ExternalReferences,
314  maybe_startup_snapshot: Option<V8Snapshot>,
315) -> v8::OwnedIsolate {
316  if let Some(snapshot) = maybe_startup_snapshot {
317    v8::Isolate::snapshot_creator_from_existing_snapshot(
318      snapshot.0,
319      Some(external_refs),
320    )
321  } else {
322    v8::Isolate::snapshot_creator(Some(external_refs))
323  }
324}