jstime_core/
lib.rs

1mod buffered_random;
2mod builtins;
3mod error;
4mod event_loop;
5mod isolate_state;
6mod js_loading;
7mod module;
8mod pool;
9mod script;
10mod sourcemap;
11
12pub(crate) use isolate_state::IsolateState;
13
14pub fn init(v8_flags: Option<Vec<String>>) {
15    // Initialize ICU data before V8 initialization
16    // This is required for locale-specific operations like toLocaleString()
17    static ICU_INIT: std::sync::Once = std::sync::Once::new();
18    ICU_INIT.call_once(|| {
19        let icu_data =
20            align_data::include_aligned!(align_data::Align16, "../third_party/icu/icudtl.dat");
21        // Ignore errors - ICU data initialization is best-effort
22        let _ = v8::icu::set_common_data_74(icu_data);
23        // Set default locale to en_US
24        v8::icu::set_default_locale("en_US");
25    });
26
27    let mut flags = v8_flags.unwrap_or_default();
28
29    // Add performance-oriented V8 flags if not already present
30    // Only add flags that don't conflict with user-provided flags
31    let perf_flags = [
32        "--turbofan", // Enable TurboFan optimizing compiler (usually on by default)
33        "--opt",      // Enable optimizations
34    ];
35
36    for flag in &perf_flags {
37        if !flags
38            .iter()
39            .any(|f| f.starts_with(flag) || f.starts_with(&format!("--no-{}", &flag[2..])))
40        {
41            flags.push(flag.to_string());
42        }
43    }
44
45    flags.push("jstime".to_owned());
46    flags.rotate_right(1);
47
48    v8::V8::set_flags_from_command_line(flags);
49
50    static V8_INIT: std::sync::Once = std::sync::Once::new();
51    V8_INIT.call_once(|| {
52        let platform = v8::new_default_platform(0, false).make_shared();
53        v8::V8::initialize_platform(platform);
54        v8::V8::initialize();
55    });
56}
57
58/// Options for `JSTime::new`.
59#[derive(Default)]
60pub struct Options {
61    pub snapshot: Option<&'static [u8]>,
62    taking_snapshot: bool,
63    pub process_argv: Vec<String>,
64    /// Number of warmup iterations to run before actual execution.
65    /// This allows V8's TurboFan JIT compiler to optimize the code.
66    /// Default is 0 (no warmup).
67    pub warmup_iterations: usize,
68}
69
70impl Options {
71    pub fn new(snapshot: Option<&'static [u8]>) -> Options {
72        Options {
73            snapshot,
74            taking_snapshot: false,
75            process_argv: Vec::new(),
76            warmup_iterations: 0,
77        }
78    }
79
80    pub fn with_process_argv(mut self, argv: Vec<String>) -> Self {
81        self.process_argv = argv;
82        self
83    }
84
85    pub fn with_warmup(mut self, iterations: usize) -> Self {
86        self.warmup_iterations = iterations;
87        self
88    }
89}
90
91/// JSTime Instance.
92#[allow(clippy::all)]
93pub struct JSTime {
94    isolate: Option<v8::OwnedIsolate>,
95    taking_snapshot: bool,
96    warmup_iterations: usize,
97}
98
99impl JSTime {
100    /// Create a new JSTime instance from `options`.
101    pub fn new(options: Options) -> JSTime {
102        let mut create_params = v8::Isolate::create_params()
103            .external_references(builtins::get_external_references().into_vec().into())
104            .heap_limits(0, 1024 * 1024 * 1024); // 1GB max heap size
105        if let Some(snapshot) = options.snapshot {
106            create_params = create_params.snapshot_blob(snapshot.into());
107        }
108        let isolate = v8::Isolate::new(create_params);
109        JSTime::create(options, isolate)
110    }
111
112    pub fn create_snapshot(mut options: Options) -> Vec<u8> {
113        assert!(
114            options.snapshot.is_none(),
115            "Cannot pass snapshot data while creating snapshot"
116        );
117        options.taking_snapshot = true;
118
119        let external_refs = builtins::get_external_references();
120        let external_refs_cow: std::borrow::Cow<'static, [v8::ExternalReference]> =
121            std::borrow::Cow::Owned(external_refs.into_vec());
122        let mut isolate = v8::Isolate::snapshot_creator(Some(external_refs_cow), None);
123
124        // Set up import.meta callback before creating context
125        isolate.set_host_initialize_import_meta_object_callback(
126            module::host_initialize_import_meta_object_callback,
127        );
128
129        // Set up dynamic import callback
130        isolate.set_host_import_module_dynamically_callback(
131            module::host_import_module_dynamically_callback,
132        );
133
134        let global_context = {
135            v8::scope!(let scope, &mut isolate);
136            let context = v8::Context::new(scope, Default::default());
137            let isolate_ref: &v8::Isolate = scope;
138            v8::Global::new(isolate_ref, context)
139        };
140
141        isolate.set_slot(IsolateState::new(global_context, options.process_argv));
142
143        // Create builtins in the snapshot context and set default context
144        {
145            let context = IsolateState::get(&mut isolate).borrow().context();
146            v8::scope!(let scope, &mut isolate);
147            let context_local = v8::Local::new(scope, context);
148            let scope = &mut v8::ContextScope::new(scope, context_local);
149
150            // Create builtins
151            builtins::Builtins::create(scope);
152
153            // Set the default context for the snapshot
154            scope.set_default_context(context_local);
155        }
156
157        // Drop the context before creating the blob
158        IsolateState::get(&mut isolate).borrow_mut().drop_context();
159
160        match isolate.create_blob(v8::FunctionCodeHandling::Keep) {
161            Some(data) => data.to_vec(),
162            None => {
163                panic!("Unable to create snapshot");
164            }
165        }
166    }
167
168    fn create(options: Options, mut isolate: v8::OwnedIsolate) -> JSTime {
169        // Set up import.meta callback before creating context
170        isolate.set_host_initialize_import_meta_object_callback(
171            module::host_initialize_import_meta_object_callback,
172        );
173
174        // Set up dynamic import callback
175        isolate.set_host_import_module_dynamically_callback(
176            module::host_import_module_dynamically_callback,
177        );
178
179        let global_context = {
180            v8::scope!(let scope, &mut isolate);
181            let context = v8::Context::new(scope, Default::default());
182            let isolate_ref: &v8::Isolate = scope;
183            v8::Global::new(isolate_ref, context)
184        };
185
186        isolate.set_slot(IsolateState::new(global_context, options.process_argv));
187
188        // If snapshot data was provided, the builtins already exist within it.
189        if options.snapshot.is_none() {
190            let context = IsolateState::get(&mut isolate).borrow().context();
191            v8::scope!(let scope, &mut isolate);
192            let context_local = v8::Local::new(scope, context);
193            let mut scope = v8::ContextScope::new(scope, context_local);
194            builtins::Builtins::create(&mut scope);
195        }
196
197        JSTime {
198            isolate: Some(isolate),
199            taking_snapshot: options.taking_snapshot,
200            warmup_iterations: options.warmup_iterations,
201        }
202    }
203
204    fn isolate(&mut self) -> &mut v8::Isolate {
205        match self.isolate.as_mut() {
206            Some(i) => i,
207            None => unsafe {
208                std::hint::unreachable_unchecked();
209            },
210        }
211    }
212
213    /// Import a module by filename.
214    pub fn import(&mut self, filename: &str) -> Result<(), String> {
215        // Perform JIT warmup if configured
216        if self.warmup_iterations > 0 {
217            self.warmup_import(filename)?;
218        }
219
220        let result = {
221            let context = IsolateState::get(self.isolate()).borrow().context();
222            v8::scope!(let scope, self.isolate());
223            let context_local = v8::Local::new(scope, context);
224            let mut scope = v8::ContextScope::new(scope, context_local);
225
226            // Use a TryCatch scope to properly capture error details
227            v8::tc_scope!(let tc, &mut scope);
228            let loader = module::Loader::new();
229
230            let mut cwd = std::env::current_dir().unwrap();
231            cwd.push("jstime");
232            let cwd = cwd.into_os_string().into_string().unwrap();
233            match loader.import(tc, &cwd, filename) {
234                Ok(_) => Ok(()),
235                Err(exception) => {
236                    // If we have caught exception details, format them properly
237                    if tc.has_caught() {
238                        Err(crate::error::format_exception(tc))
239                    } else {
240                        // Fallback: Format the exception value directly with enhanced formatting
241                        Err(crate::error::format_exception_value(tc, exception))
242                    }
243                }
244            }
245        };
246
247        // Run the event loop to process any pending timers
248        self.run_event_loop();
249
250        result
251    }
252
253    /// Warm up the JIT compiler by importing the module multiple times.
254    /// This allows V8's TurboFan compiler to optimize the module code.
255    fn warmup_import(&mut self, filename: &str) -> Result<(), String> {
256        for _ in 0..self.warmup_iterations {
257            let context = IsolateState::get(self.isolate()).borrow().context();
258            v8::scope!(let scope, self.isolate());
259            let context_local = v8::Local::new(scope, context);
260            let mut scope = v8::ContextScope::new(scope, context_local);
261            v8::tc_scope!(let tc, &mut scope);
262            let loader = module::Loader::new();
263            let mut cwd = std::env::current_dir().unwrap();
264            cwd.push("jstime");
265            let cwd = cwd.into_os_string().into_string().unwrap();
266            // Import but ignore result during warmup
267            match loader.import(tc, &cwd, filename) {
268                Ok(_) => {}
269                Err(exception) => {
270                    if tc.has_caught() {
271                        return Err(crate::error::format_exception(tc));
272                    } else {
273                        return Err(crate::error::format_exception_value(tc, exception));
274                    }
275                }
276            }
277        }
278        Ok(())
279    }
280
281    /// Run a script and get a string representation of the result.
282    /// This version runs the event loop after execution, which is suitable for file execution.
283    /// For REPL usage, use `run_script_no_event_loop` instead.
284    pub fn run_script(&mut self, source: &str, filename: &str) -> Result<String, String> {
285        // Perform JIT warmup if configured
286        if self.warmup_iterations > 0 {
287            self.warmup_script(source, filename)?;
288        }
289
290        let result = self.run_script_no_event_loop(source, filename);
291
292        // Run the event loop to process any pending timers
293        self.run_event_loop();
294
295        result
296    }
297
298    /// Warm up the JIT compiler by running the script multiple times.
299    /// This allows V8's TurboFan compiler to optimize the code before the actual execution.
300    fn warmup_script(&mut self, source: &str, filename: &str) -> Result<(), String> {
301        for _ in 0..self.warmup_iterations {
302            let context = IsolateState::get(self.isolate()).borrow().context();
303            v8::scope!(let scope, self.isolate());
304            let context_local = v8::Local::new(scope, context);
305            let mut scope = v8::ContextScope::new(scope, context_local);
306            // Run script but ignore the result during warmup
307            script::run(&mut scope, source, filename)?;
308        }
309        Ok(())
310    }
311
312    /// Run a script and get a string representation of the result without running the event loop.
313    /// This is suitable for REPL usage where the event loop should not block between commands.
314    pub fn run_script_no_event_loop(
315        &mut self,
316        source: &str,
317        filename: &str,
318    ) -> Result<String, String> {
319        let context = IsolateState::get(self.isolate()).borrow().context();
320        v8::scope!(let scope, self.isolate());
321        let context_local = v8::Local::new(scope, context);
322        let mut scope = v8::ContextScope::new(scope, context_local);
323        match script::run(&mut scope, source, filename) {
324            Ok(v) => {
325                let isolate: &v8::Isolate = &scope;
326                Ok(v.to_string(&scope).unwrap().to_rust_string_lossy(isolate))
327            }
328            Err(e) => Err(e),
329        }
330    }
331
332    /// Tick the event loop to execute ready timers without blocking.
333    /// This is suitable for REPL usage to allow timers to execute in the background.
334    pub fn tick_event_loop(&mut self) {
335        let context = IsolateState::get(self.isolate()).borrow().context();
336        v8::scope!(let scope, self.isolate());
337        let context_local = v8::Local::new(scope, context);
338        let mut scope = v8::ContextScope::new(scope, context_local);
339        let event_loop = event_loop::get_event_loop(&mut scope);
340        event_loop.borrow_mut().tick(&mut scope);
341    }
342
343    /// Run the event loop until all pending operations are complete
344    fn run_event_loop(&mut self) {
345        let context = IsolateState::get(self.isolate()).borrow().context();
346        v8::scope!(let scope, self.isolate());
347        let context_local = v8::Local::new(scope, context);
348        let mut scope = v8::ContextScope::new(scope, context_local);
349        let event_loop = event_loop::get_event_loop(&mut scope);
350        event_loop.borrow_mut().run(&mut scope);
351    }
352}
353
354impl Drop for JSTime {
355    fn drop(&mut self) {
356        if self.taking_snapshot {
357            // The isolate is not actually owned by JSTime if we're
358            // snapshotting, it's owned by the SnapshotCreator.
359            std::mem::forget(self.isolate.take().unwrap())
360        }
361    }
362}