orinium_browser/engine/js/
processor.rs1use 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,
22};
23use crate::engine::layouter::dom_snapshot::DomSnapshot;
24
25#[derive(Debug)]
27pub enum JsTask {
28 SetDocumentUrl { url: String },
30 SetOrigin { origin: String },
32 SetViewport { width: f32, height: f32 },
34 SetLanguage { language: String },
36 SetLayoutMetrics {
38 metrics: HashMap<u64, JsLayoutMetrics>,
39 },
40 RunScript { source: String },
42 DispatchDomContentLoaded,
44 DispatchWindowLoad,
46 RunTimers,
48 Click { dom_id: u64 },
50 Scroll { dom_id: u64 },
52 DispatchElementEvent { dom_id: u64, event_type: String },
54 ResolveFetch { id: u64, response: JsFetchResponse },
56 RejectFetch { id: u64, reason: String },
58 ResolveDevTools { id: u64, result: String },
60 ResolveIframe { dom_id: u64, html: String },
63 RejectIframe { dom_id: u64 },
66 UpdateDom { snapshot: DomSnapshot },
68}
69
70#[derive(Debug)]
72enum JsCommand {
73 Task { task: JsTask, version: u64 },
74}
75
76#[derive(Debug)]
78pub struct JsTaskResult {
79 pub dom: Option<DomSnapshot>,
81 pub iframe_documents: Vec<IframeContentSnapshot>,
84 pub needs_redraw: bool,
86 pub fetch_requests: Vec<JsFetchRequest>,
88 pub iframe_fetch_requests: Vec<JsIframeFetchRequest>,
90 pub devtools_requests: Vec<JsDevToolsRequest>,
92 pub(crate) dynamic_script_requests: Vec<JsDynamicScriptRequest>,
94 pub(crate) dynamic_style_requests: Vec<JsDynamicStyleRequest>,
96 pub(crate) dynamic_image_requests: Vec<JsDynamicImageRequest>,
98 pub version: u64,
100}
101
102pub struct JsProcessor {
105 cmd_tx: mpsc::Sender<JsCommand>,
106 result_rx: mpsc::Receiver<JsTaskResult>,
107 latest: Arc<AtomicU64>,
110}
111
112impl std::fmt::Debug for JsProcessor {
113 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114 f.write_str("JsProcessor")
115 }
116}
117
118impl JsProcessor {
119 pub fn new(initial: DomSnapshot) -> Self {
122 let (cmd_tx, cmd_rx) = mpsc::channel::<JsCommand>();
123 let (result_tx, result_rx) = mpsc::channel::<JsTaskResult>();
124
125 let latest = Arc::new(AtomicU64::new(0));
126 let thread_latest = Arc::clone(&latest);
127
128 thread::spawn(move || {
129 perf_scope!(init);
130 let (tree, _dom_ids) = initial.into_tree();
131 let mut runtime = JsRuntime::new(Rc::new(tree));
132 perf_scope!(apply_dom);
133 runtime.apply_dom(&initial);
134 profile_log!(
135 target: "JsProc",
136 log::Level::Info,
137 "[JsMetrics] runtime_init: {:?} | initial_apply_dom: {:?}",
138 init.elapsed(),
139 apply_dom.elapsed(),
140 );
141
142 for cmd in cmd_rx {
143 let JsCommand::Task { task, version } = cmd;
144 if matches!(task, JsTask::RunTimers)
146 && version < thread_latest.load(Ordering::SeqCst)
147 {
148 continue;
149 }
150
151 perf_scope!(total);
152 let tree_may_have_new_iframes = matches!(
156 &task,
157 JsTask::SetDocumentUrl { .. } | JsTask::UpdateDom { .. }
158 );
159 perf_scope!(run);
160 #[cfg_attr(not(feature = "profile"), allow(unused_variables))]
161 let did_work = run_task(&mut runtime, task);
162 if tree_may_have_new_iframes || did_work {
163 runtime.queue_markup_iframe_loads();
164 }
165 #[cfg(any(feature = "profile", debug_assertions))]
166 let run_time = run.elapsed();
167
168 perf_scope!(collect);
169 let needs_redraw = runtime.take_needs_redraw();
170 let fetch_requests = runtime.take_fetch_requests();
171 let iframe_fetch_requests = runtime.take_iframe_fetch_requests();
172 let devtools_requests = runtime.take_devtools_requests();
173 let dynamic_script_requests = runtime.take_dynamic_script_requests();
174 let dynamic_style_requests = runtime.take_dynamic_style_requests();
175 let dynamic_image_requests = runtime.take_dynamic_image_requests();
176 let dom = if needs_redraw {
177 Some(runtime.snapshot())
178 } else {
179 None
180 };
181 let iframe_documents = if needs_redraw {
182 runtime.snapshot_iframe_documents()
183 } else {
184 Vec::new()
185 };
186 #[cfg(any(feature = "profile", debug_assertions))]
187 let collect_time = collect.elapsed();
188
189 let _ = result_tx.send(JsTaskResult {
190 dom,
191 iframe_documents,
192 needs_redraw,
193 fetch_requests,
194 iframe_fetch_requests,
195 devtools_requests,
196 dynamic_script_requests,
197 dynamic_style_requests,
198 dynamic_image_requests,
199 version,
200 });
201 profile_log!(
202 target: "JsProc",
203 if did_work {
204 log::Level::Info
205 } else {
206 log::Level::Debug
207 },
208 "[JsMetrics] total: {:?} | run_task: {:?} | collect: {:?}",
209 total.elapsed(),
210 run_time,
211 collect_time,
212 );
213 }
214 });
215
216 Self {
217 cmd_tx,
218 result_rx,
219 latest,
220 }
221 }
222
223 pub fn send(&self, task: JsTask) -> u64 {
227 let version = self.latest.fetch_add(1, Ordering::SeqCst) + 1;
228 let _ = self.cmd_tx.send(JsCommand::Task { task, version });
229 version
230 }
231
232 pub fn try_receive(&self) -> Option<JsTaskResult> {
234 self.result_rx.try_recv().ok()
235 }
236}
237
238fn run_task(runtime: &mut JsRuntime, task: JsTask) -> bool {
243 match task {
244 JsTask::SetDocumentUrl { url } => {
245 runtime.set_document_url(&url);
246 false
247 }
248 JsTask::SetOrigin { origin } => {
249 runtime.set_page_origin(&origin);
250 false
251 }
252 JsTask::SetViewport { width, height } => {
253 runtime.set_viewport(width, height);
254 false
255 }
256 JsTask::SetLanguage { language } => {
257 runtime.set_language(&language);
258 false
259 }
260 JsTask::SetLayoutMetrics { metrics } => {
261 runtime.set_layout_metrics_by_dom_id(metrics);
262 false
263 }
264 JsTask::RunScript { source } => {
265 runtime.run_script(&source);
266 true
267 }
268 JsTask::DispatchDomContentLoaded => runtime.dispatch_dom_content_loaded(),
269 JsTask::DispatchWindowLoad => runtime.dispatch_window_load(),
270 JsTask::RunTimers => runtime.run_due_timers(),
271 JsTask::Click { dom_id } => runtime.click_dom_id(dom_id),
272 JsTask::Scroll { dom_id } => runtime.scroll_dom_id(dom_id),
273 JsTask::DispatchElementEvent { dom_id, event_type } => {
274 runtime.dispatch_element_event(dom_id, &event_type);
275 true
276 }
277 JsTask::ResolveFetch { id, response } => {
278 runtime.resolve_fetch(id, response);
279 true
280 }
281 JsTask::RejectFetch { id, reason } => {
282 runtime.reject_fetch(id, reason);
283 true
284 }
285 JsTask::ResolveDevTools { id, result } => {
286 runtime.resolve_devtools(id, result);
287 true
288 }
289 JsTask::ResolveIframe { dom_id, html } => {
290 runtime.resolve_iframe_fetch(dom_id, html);
291 true
292 }
293 JsTask::RejectIframe { dom_id } => {
294 runtime.reject_iframe_fetch(dom_id);
295 true
296 }
297 JsTask::UpdateDom { snapshot } => {
298 runtime.apply_dom(&snapshot);
299 false
300 }
301 }
302}
303
304#[cfg(test)]
305mod tests {
306 use std::time::{Duration, Instant};
307
308 use super::*;
309 use crate::engine::html::parser::Parser as HtmlParser;
310 use crate::engine::layouter::NodeId;
311
312 fn snapshot_of(html: &str) -> DomSnapshot {
313 let dom = HtmlParser::new(html).parse();
314 let (snapshot, _) = DomSnapshot::from_tree(&dom.root);
315 snapshot
316 }
317
318 fn wait_for_result(processor: &JsProcessor) -> JsTaskResult {
319 let deadline = Instant::now() + Duration::from_secs(5);
320 loop {
321 if let Some(result) = processor.try_receive() {
322 return result;
323 }
324 assert!(
325 Instant::now() < deadline,
326 "JS result did not arrive before the timeout"
327 );
328 std::thread::sleep(Duration::from_millis(1));
329 }
330 }
331
332 fn find_element<'i>(snapshot: &'i DomSnapshot, id: NodeId, tag: &str) -> Option<NodeId> {
333 if snapshot.node(id).kind.tag_name() == Some(tag) {
334 return Some(id);
335 }
336 snapshot
337 .children(id)
338 .iter()
339 .find_map(|&c| find_element(snapshot, c, tag))
340 }
341
342 fn get_attr(snapshot: &DomSnapshot, id: NodeId, name: &str) -> Option<String> {
343 snapshot.node(id).kind.get_attr(name).map(str::to_string)
344 }
345
346 #[test]
347 fn script_dom_changes_are_reported_in_the_result_snapshot() {
348 let processor =
349 JsProcessor::new(snapshot_of("<html><body><div id='x'></div></body></html>"));
350 processor.send(JsTask::RunScript {
351 source: r#"document.getElementById("x").setAttribute("data-a", "1");"#.to_string(),
352 });
353
354 let result = wait_for_result(&processor);
355 assert!(result.needs_redraw);
356 let snapshot = result.dom.expect("mutating script must commit a snapshot");
357 let root = snapshot.roots()[0];
358 let div = find_element(&snapshot, root, "div").unwrap();
359 assert_eq!(get_attr(&snapshot, div, "data-a").as_deref(), Some("1"));
360 }
361
362 #[test]
363 fn ordered_scripts_run_in_send_order() {
364 let processor =
365 JsProcessor::new(snapshot_of("<html><body><div id='x'></div></body></html>"));
366 processor.send(JsTask::RunScript {
367 source: r#"globalThis.__s = "a";"#.to_string(),
368 });
369 processor.send(JsTask::RunScript {
370 source: r#"
371 globalThis.__s += "b";
372 document.getElementById("x").setAttribute("data-s", globalThis.__s);
373 "#
374 .to_string(),
375 });
376
377 let mut result = None;
379 for _ in 0..2 {
380 let received = wait_for_result(&processor);
381 if received.needs_redraw {
382 result = Some(received);
383 }
384 }
385 let snapshot = result
386 .expect("the second script must commit a snapshot")
387 .dom
388 .unwrap();
389 let root = snapshot.roots()[0];
390 let div = find_element(&snapshot, root, "div").unwrap();
391 assert_eq!(get_attr(&snapshot, div, "data-s").as_deref(), Some("ab"));
392 }
393
394 #[test]
395 fn update_dom_replaces_the_mirror_before_later_tasks() {
396 let processor =
397 JsProcessor::new(snapshot_of("<html><body><div id='x'></div></body></html>"));
398 processor.send(JsTask::UpdateDom {
400 snapshot: snapshot_of("<html><body><div id='x' data-v='ui'></div></body></html>"),
401 });
402 processor.send(JsTask::RunScript {
403 source: r#"
404 const el = document.getElementById("x");
405 el.setAttribute("data-read", el.getAttribute("data-v"));
406 "#
407 .to_string(),
408 });
409
410 let update = wait_for_result(&processor);
412 assert!(!update.needs_redraw);
413 assert!(update.dom.is_none());
414
415 let result = wait_for_result(&processor);
416 assert!(result.needs_redraw);
417 let snapshot = result.dom.unwrap();
418 let root = snapshot.roots()[0];
419 let div = find_element(&snapshot, root, "div").unwrap();
420 assert_eq!(get_attr(&snapshot, div, "data-read").as_deref(), Some("ui"));
421 }
422}