1use crate::{perf_scope, profile_log};
11
12use std::collections::HashMap;
13use std::rc::Rc;
14use std::sync::atomic::{AtomicU64, Ordering};
15use std::sync::{Arc, mpsc};
16use std::thread;
17
18use super::{
19 IframeContentSnapshot, JsDevToolsRequest, JsDynamicImageRequest, JsDynamicScriptRequest,
20 JsDynamicStyleRequest, JsFetchRequest, JsFetchResponse, JsIframeFetchRequest, JsLayoutMetrics,
21 JsRuntime, JsTraceBatch,
22};
23use crate::engine::layouter::dom_snapshot::DomSnapshot;
24use crate::engine::permissions::Permissions;
25
26#[derive(Debug)]
28pub enum JsTask {
29 SetDocumentUrl { url: String },
31 SetOrigin { origin: String },
33 SetPermissions { permissions: Permissions },
36 SetViewport { width: f32, height: f32 },
38 SetLanguage { language: String },
40 SetLayoutMetrics {
42 metrics: HashMap<u64, JsLayoutMetrics>,
43 computed_styles: HashMap<u64, Vec<(String, String)>>,
44 },
45 RunScript {
47 source: String,
48 label: Option<String>,
50 },
51 DispatchDomContentLoaded,
53 DispatchWindowLoad,
55 RunTimers,
57 Click { dom_id: u64 },
59 Scroll { dom_id: u64 },
61 DispatchElementEvent { dom_id: u64, event_type: String },
63 ResolveFetch { id: u64, response: JsFetchResponse },
65 RejectFetch { id: u64, reason: String },
67 ResolveDevTools { id: u64, result: String },
69 ResolveIframe {
72 dom_id: u64,
73 html: String,
74 url: String,
75 },
76 RejectIframe { dom_id: u64 },
79 SetTracing { enabled: bool },
81 ClearTrace,
83 EvaluateConsole { id: u64, source: String },
85 UpdateDom { snapshot: DomSnapshot },
87}
88
89#[derive(Debug)]
91enum JsCommand {
92 Task { task: JsTask, version: u64 },
93}
94
95#[derive(Debug)]
97pub struct JsTaskResult {
98 pub dom: Option<DomSnapshot>,
100 pub iframe_documents: Vec<IframeContentSnapshot>,
103 pub needs_redraw: bool,
105 pub fetch_requests: Vec<JsFetchRequest>,
107 pub iframe_fetch_requests: Vec<JsIframeFetchRequest>,
109 pub devtools_requests: Vec<JsDevToolsRequest>,
111 pub js_trace: JsTraceBatch,
113 pub console_results: Vec<(u64, String)>,
115 pub(crate) dynamic_script_requests: Vec<JsDynamicScriptRequest>,
117 pub(crate) dynamic_style_requests: Vec<JsDynamicStyleRequest>,
119 pub(crate) dynamic_image_requests: Vec<JsDynamicImageRequest>,
121 pub(crate) navigation_requests: Vec<String>,
123 pub version: u64,
125}
126
127pub struct JsProcessor {
130 cmd_tx: mpsc::Sender<JsCommand>,
131 result_rx: mpsc::Receiver<JsTaskResult>,
132 latest: Arc<AtomicU64>,
135}
136
137impl std::fmt::Debug for JsProcessor {
138 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139 f.write_str("JsProcessor")
140 }
141}
142
143impl JsProcessor {
144 pub fn new(initial: DomSnapshot) -> Self {
147 let (cmd_tx, cmd_rx) = mpsc::channel::<JsCommand>();
148 let (result_tx, result_rx) = mpsc::channel::<JsTaskResult>();
149
150 let latest = Arc::new(AtomicU64::new(0));
151 let thread_latest = Arc::clone(&latest);
152
153 thread::spawn(move || {
154 perf_scope!(init);
155 let (tree, _dom_ids) = initial.into_tree();
156 let mut runtime = JsRuntime::new(Rc::new(tree));
157 perf_scope!(apply_dom);
158 runtime.apply_dom(&initial);
159 profile_log!(
160 target: "JsProc",
161 log::Level::Info,
162 "[JsMetrics] runtime_init: {:?} | initial_apply_dom: {:?}",
163 init.elapsed(),
164 apply_dom.elapsed(),
165 );
166
167 for cmd in cmd_rx {
168 let JsCommand::Task { task, version } = cmd;
169 if matches!(task, JsTask::RunTimers)
171 && version < thread_latest.load(Ordering::SeqCst)
172 {
173 continue;
174 }
175
176 perf_scope!(total);
177 let tree_may_have_new_iframes = matches!(
181 &task,
182 JsTask::SetDocumentUrl { .. } | JsTask::UpdateDom { .. }
183 );
184 perf_scope!(run);
185 #[cfg_attr(not(feature = "profile"), allow(unused_variables))]
186 let did_work = run_task(&mut runtime, task);
187 if tree_may_have_new_iframes || did_work {
188 runtime.queue_markup_iframe_loads();
189 }
190 #[cfg(any(feature = "profile", debug_assertions))]
191 let run_time = run.elapsed();
192
193 perf_scope!(collect);
194 let needs_redraw = runtime.take_needs_redraw();
195 let fetch_requests = runtime.take_fetch_requests();
196 let iframe_fetch_requests = runtime.take_iframe_fetch_requests();
197 let devtools_requests = runtime.take_devtools_requests();
198 let js_trace = runtime.take_trace_batch();
199 let console_results = runtime.take_console_results();
200 let dynamic_script_requests = runtime.take_dynamic_script_requests();
201 let dynamic_style_requests = runtime.take_dynamic_style_requests();
202 let dynamic_image_requests = runtime.take_dynamic_image_requests();
203 let navigation_requests = runtime.take_navigation_requests();
204 let dom = if needs_redraw {
205 Some(runtime.snapshot())
206 } else {
207 None
208 };
209 let iframe_documents = if needs_redraw {
210 runtime.snapshot_iframe_documents()
211 } else {
212 Vec::new()
213 };
214 #[cfg(any(feature = "profile", debug_assertions))]
215 let collect_time = collect.elapsed();
216
217 let _ = result_tx.send(JsTaskResult {
218 dom,
219 iframe_documents,
220 needs_redraw,
221 fetch_requests,
222 iframe_fetch_requests,
223 devtools_requests,
224 js_trace,
225 console_results,
226 dynamic_script_requests,
227 dynamic_style_requests,
228 dynamic_image_requests,
229 navigation_requests,
230 version,
231 });
232 profile_log!(
233 target: "JsProc",
234 if did_work {
235 log::Level::Info
236 } else {
237 log::Level::Debug
238 },
239 "[JsMetrics] total: {:?} | run_task: {:?} | collect: {:?}",
240 total.elapsed(),
241 run_time,
242 collect_time,
243 );
244 }
245 });
246
247 Self {
248 cmd_tx,
249 result_rx,
250 latest,
251 }
252 }
253
254 pub fn send(&self, task: JsTask) -> u64 {
258 let version = self.latest.fetch_add(1, Ordering::SeqCst) + 1;
259 let _ = self.cmd_tx.send(JsCommand::Task { task, version });
260 version
261 }
262
263 pub fn try_receive(&self) -> Option<JsTaskResult> {
265 self.result_rx.try_recv().ok()
266 }
267}
268
269fn run_task(runtime: &mut JsRuntime, task: JsTask) -> bool {
274 match task {
275 JsTask::SetDocumentUrl { url } => {
276 runtime.set_document_url(&url);
277 false
278 }
279 JsTask::SetOrigin { origin } => {
280 runtime.set_page_origin(&origin);
281 false
282 }
283 JsTask::SetPermissions { permissions } => {
284 runtime.set_page_permissions(permissions);
285 false
286 }
287 JsTask::SetViewport { width, height } => {
288 runtime.set_viewport(width, height);
289 false
290 }
291 JsTask::SetLanguage { language } => {
292 runtime.set_language(&language);
293 false
294 }
295 JsTask::SetLayoutMetrics {
296 metrics,
297 computed_styles,
298 } => {
299 runtime.set_layout_metrics_by_dom_id(metrics);
300 runtime.set_computed_styles_by_dom_id(computed_styles);
301 false
302 }
303 JsTask::RunScript { source, label } => {
304 runtime.run_script(&source, label.as_deref());
305 true
306 }
307 JsTask::DispatchDomContentLoaded => runtime.dispatch_dom_content_loaded(),
308 JsTask::DispatchWindowLoad => runtime.dispatch_window_load(),
309 JsTask::RunTimers => runtime.run_due_timers(),
310 JsTask::Click { dom_id } => runtime.click_dom_id(dom_id),
311 JsTask::Scroll { dom_id } => runtime.scroll_dom_id(dom_id),
312 JsTask::DispatchElementEvent { dom_id, event_type } => {
313 runtime.dispatch_element_event(dom_id, &event_type);
314 true
315 }
316 JsTask::ResolveFetch { id, response } => {
317 runtime.resolve_fetch(id, response);
318 true
319 }
320 JsTask::RejectFetch { id, reason } => {
321 runtime.reject_fetch(id, reason);
322 true
323 }
324 JsTask::ResolveDevTools { id, result } => {
325 runtime.resolve_devtools(id, result);
326 true
327 }
328 JsTask::ResolveIframe { dom_id, html, url } => {
329 runtime.resolve_iframe_fetch(dom_id, html, url);
330 true
331 }
332 JsTask::RejectIframe { dom_id } => {
333 runtime.reject_iframe_fetch(dom_id);
334 true
335 }
336 JsTask::SetTracing { enabled } => {
337 runtime.set_js_tracing(enabled);
338 false
339 }
340 JsTask::ClearTrace => {
341 runtime.clear_js_trace();
342 false
343 }
344 JsTask::EvaluateConsole { id, source } => {
345 runtime.evaluate_console(id, &source);
346 true
347 }
348 JsTask::UpdateDom { snapshot } => {
349 runtime.apply_dom(&snapshot);
350 false
351 }
352 }
353}
354
355#[cfg(test)]
356mod tests {
357 use std::time::{Duration, Instant};
358
359 use super::*;
360 use crate::engine::html::parser::Parser as HtmlParser;
361 use crate::engine::layouter::NodeId;
362
363 fn snapshot_of(html: &str) -> DomSnapshot {
364 let dom = HtmlParser::new(html).parse();
365 let (snapshot, _) = DomSnapshot::from_tree(&dom.root);
366 snapshot
367 }
368
369 fn wait_for_result(processor: &JsProcessor) -> JsTaskResult {
370 let deadline = Instant::now() + Duration::from_secs(5);
371 loop {
372 if let Some(result) = processor.try_receive() {
373 return result;
374 }
375 assert!(
376 Instant::now() < deadline,
377 "JS result did not arrive before the timeout"
378 );
379 std::thread::sleep(Duration::from_millis(1));
380 }
381 }
382
383 fn find_element(snapshot: &DomSnapshot, id: NodeId, tag: &str) -> Option<NodeId> {
384 if snapshot.node(id).kind.tag_name() == Some(tag) {
385 return Some(id);
386 }
387 snapshot
388 .children(id)
389 .iter()
390 .find_map(|&c| find_element(snapshot, c, tag))
391 }
392
393 fn get_attr(snapshot: &DomSnapshot, id: NodeId, name: &str) -> Option<String> {
394 snapshot.node(id).kind.get_attr(name).map(str::to_string)
395 }
396
397 #[test]
398 fn script_dom_changes_are_reported_in_the_result_snapshot() {
399 let processor =
400 JsProcessor::new(snapshot_of("<html><body><div id='x'></div></body></html>"));
401 processor.send(JsTask::RunScript {
402 source: r#"document.getElementById("x").setAttribute("data-a", "1");"#.to_string(),
403 label: None,
404 });
405
406 let result = wait_for_result(&processor);
407 assert!(result.needs_redraw);
408 let snapshot = result.dom.expect("mutating script must commit a snapshot");
409 let root = snapshot.roots()[0];
410 let div = find_element(&snapshot, root, "div").unwrap();
411 assert_eq!(get_attr(&snapshot, div, "data-a").as_deref(), Some("1"));
412 }
413
414 #[test]
415 fn ordered_scripts_run_in_send_order() {
416 let processor =
417 JsProcessor::new(snapshot_of("<html><body><div id='x'></div></body></html>"));
418 processor.send(JsTask::RunScript {
419 source: r#"globalThis.__s = "a";"#.to_string(),
420 label: None,
421 });
422 processor.send(JsTask::RunScript {
423 source: r#"
424 globalThis.__s += "b";
425 document.getElementById("x").setAttribute("data-s", globalThis.__s);
426 "#
427 .to_string(),
428 label: None,
429 });
430
431 let mut result = None;
433 for _ in 0..2 {
434 let received = wait_for_result(&processor);
435 if received.needs_redraw {
436 result = Some(received);
437 }
438 }
439 let snapshot = result
440 .expect("the second script must commit a snapshot")
441 .dom
442 .unwrap();
443 let root = snapshot.roots()[0];
444 let div = find_element(&snapshot, root, "div").unwrap();
445 assert_eq!(get_attr(&snapshot, div, "data-s").as_deref(), Some("ab"));
446 }
447
448 #[test]
449 fn update_dom_replaces_the_mirror_before_later_tasks() {
450 let processor =
451 JsProcessor::new(snapshot_of("<html><body><div id='x'></div></body></html>"));
452 processor.send(JsTask::UpdateDom {
454 snapshot: snapshot_of("<html><body><div id='x' data-v='ui'></div></body></html>"),
455 });
456 processor.send(JsTask::RunScript {
457 source: r#"
458 const el = document.getElementById("x");
459 el.setAttribute("data-read", el.getAttribute("data-v"));
460 "#
461 .to_string(),
462 label: None,
463 });
464
465 let update = wait_for_result(&processor);
467 assert!(!update.needs_redraw);
468 assert!(update.dom.is_none());
469
470 let result = wait_for_result(&processor);
471 assert!(result.needs_redraw);
472 let snapshot = result.dom.unwrap();
473 let root = snapshot.roots()[0];
474 let div = find_element(&snapshot, root, "div").unwrap();
475 assert_eq!(get_attr(&snapshot, div, "data-read").as_deref(), Some("ui"));
476 }
477}