1use std::cell::RefCell;
4use std::rc::Rc;
5use std::sync::mpsc::{Receiver, RecvTimeoutError, Sender, channel};
6use std::sync::{Arc, Mutex};
7use std::task::{Context as TaskContext, Waker};
8
9use blitz_dom::{
10 BaseDocument, DEFAULT_CSS, DocGuard, DocGuardMut, Document, DocumentConfig, EventDriver, NodeId,
11};
12use blitz_html::stream::StreamingParser;
13use blitz_html::{DocumentHtmlParser, HtmlProvider};
14use blitz_traits::events::{BlitzPointerEvent, DomEvent, UiEvent};
15use url::Url;
16use web_time::{Duration, Instant};
17
18use crate::event_handler::ScriptEventHandler;
19use crate::fetch::{DefaultScriptFetcher, ScriptFetcher};
20use crate::module::SharedFetcher;
21use crate::runtime::ScriptRuntime;
22
23type PollHook =
24 Box<dyn for<'a> FnMut(&mut ScriptDocument, Option<&TaskContext<'a>>) -> bool + 'static>;
25
26#[derive(Clone, Copy, PartialEq, Eq)]
28enum ScriptKind {
29 Classic,
30 Module,
33 ImportMap,
36}
37
38struct PendingScript {
40 node_id: NodeId,
41 src: Option<String>,
42 inline_text: String,
43 kind: ScriptKind,
44 deferred: bool,
50}
51
52pub struct ScriptDocument {
61 inner: Rc<RefCell<BaseDocument>>,
62 runtime: ScriptRuntime,
63 base_url: Option<Url>,
64 fetcher: SharedFetcher,
71 scripts_executed: bool,
72 startup_events_pending: bool,
73 load_event_pending: bool,
74 executed_scripts: std::collections::HashSet<NodeId>,
81 poll_hook: Option<PollHook>,
82
83 waker: Arc<Mutex<Option<Waker>>>,
86 timer_thread: Option<Sender<Instant>>,
87}
88
89impl ScriptDocument {
90 pub fn from_html(html: &str, mut config: DocumentConfig) -> Self {
99 if let Some(ss) = &mut config.ua_stylesheets {
100 if !ss.iter().any(|s| s == DEFAULT_CSS) {
101 ss.push(String::from(DEFAULT_CSS));
102 }
103 } else {
104 config.ua_stylesheets = Some(vec![String::from(DEFAULT_CSS)]);
105 }
106 if config.html_parser_provider.is_none() {
107 config.html_parser_provider = Some(Arc::new(HtmlProvider));
108 }
109
110 let base_url = config
111 .base_url
112 .as_deref()
113 .and_then(|url| Url::parse(url).ok());
114
115 let is_xml = DocumentHtmlParser::is_xhtml_document(html);
116 let mut doc = BaseDocument::new(config);
117 if is_xml {
118 let mut mutr = doc.mutate();
119 DocumentHtmlParser::parse_into_mutator(&mut mutr, html);
120 drop(mutr);
121 }
122
123 let inner = Rc::new(RefCell::new(doc));
124 let parser = if is_xml {
125 None
126 } else {
127 let mut parser = StreamingParser::new(Rc::clone(&inner), html);
128 if parser.next_script().is_some() {
129 Some(parser)
130 } else {
131 parser.finish();
132 None
133 }
134 };
135 let fetcher: SharedFetcher = Rc::new(RefCell::new(Rc::new(DefaultScriptFetcher)));
136 let runtime = ScriptRuntime::new(Rc::clone(&inner), base_url.as_ref(), Rc::clone(&fetcher));
137 crate::docwrite::prepare(&runtime.context, html, Rc::clone(&fetcher), parser);
138
139 Self {
140 inner,
141 runtime,
142 base_url,
143 fetcher,
144 scripts_executed: false,
145 startup_events_pending: false,
146 load_event_pending: false,
147 executed_scripts: std::collections::HashSet::new(),
148 poll_hook: None,
149 waker: Arc::new(Mutex::new(None)),
150 timer_thread: None,
151 }
152 }
153
154 pub fn set_job_budget(&mut self, duration: Duration) {
159 self.runtime.set_job_budget(duration);
160 }
161
162 pub fn job_budget(&self) -> Duration {
164 self.runtime.job_budget()
165 }
166
167 pub fn has_pending_jobs(&self) -> bool {
171 self.runtime.jobs_remain()
172 }
173
174 pub fn pending_task_count(&self) -> u64 {
177 self.runtime.pending_task_count()
178 }
179
180 pub fn run_until_idle(&mut self) -> Result<(), &'static str> {
205 const MODULE_TIMEOUT: Duration = Duration::from_secs(60);
206 const LOAD_POLL_INTERVAL: Duration = Duration::from_millis(5);
207 let module_deadline = Instant::now() + MODULE_TIMEOUT;
208
209 loop {
210 let progressed = self.poll(None);
211 let now = Instant::now();
212 let next_timer = self.runtime.next_timer_deadline();
213 let loads_pending = self.runtime.has_pending_module_loads();
214 let evaluations_pending = self.runtime.has_pending_module_evaluations();
215
216 if now >= module_deadline {
219 if loads_pending {
220 return Err("module loading did not complete within sixty seconds");
221 }
222 if evaluations_pending {
223 return Err("module evaluation did not complete within sixty seconds");
224 }
225 }
226
227 if self.runtime.jobs_remain()
228 || next_timer.is_some_and(|deadline| deadline <= now)
229 || progressed
230 {
231 continue;
232 }
233
234 if loads_pending || evaluations_pending {
235 let wait = if loads_pending {
239 LOAD_POLL_INTERVAL
240 } else {
241 next_timer.map_or(LOAD_POLL_INTERVAL, |deadline| {
242 deadline.saturating_duration_since(now)
243 })
244 };
245 let wait = next_timer.map_or(wait, |deadline| {
246 wait.min(deadline.saturating_duration_since(now))
247 });
248 std::thread::sleep(wait.min(module_deadline.saturating_duration_since(now)));
249 continue;
250 }
251
252 if crate::docwrite::active(&self.runtime.context)
253 || self.startup_events_pending
254 || self.collect_scripts().iter().any(|script| {
255 !self.executed_scripts.contains(&script.node_id)
256 && !crate::docwrite::was_executed(&self.runtime.context, script.node_id)
257 })
258 {
259 return Err("script startup cannot make progress");
260 }
261
262 return Ok(());
263 }
264 }
265
266 pub fn next_timer_deadline(&self) -> Option<std::time::Instant> {
270 self.runtime.next_timer_deadline()
271 }
272
273 pub fn with_fetcher(self, fetcher: impl ScriptFetcher) -> Self {
276 *self.fetcher.borrow_mut() = Rc::new(fetcher);
277 self
278 }
279
280 pub fn set_ipc_handler(&mut self, handler: impl Fn(String) + 'static) {
285 self.runtime.ctx.state.borrow_mut().ipc_handler = Some(Rc::new(handler));
286 }
287
288 pub fn set_poll_hook(
292 &mut self,
293 hook: impl for<'a> FnMut(&mut ScriptDocument, Option<&TaskContext<'a>>) -> bool + 'static,
294 ) {
295 self.poll_hook = Some(Box::new(hook));
296 }
297
298 pub fn add_poll_hook(
301 &mut self,
302 mut hook: impl for<'a> FnMut(&mut ScriptDocument, Option<&TaskContext<'a>>) -> bool + 'static,
303 ) {
304 let Some(mut existing) = self.poll_hook.take() else {
305 self.poll_hook = Some(Box::new(hook));
306 return;
307 };
308 self.poll_hook = Some(Box::new(move |document, task_context| {
309 let ran_existing = existing(document, task_context);
310 let ran_added = hook(document, task_context);
311 ran_existing | ran_added
312 }));
313 }
314
315 pub fn execute_scripts(&mut self) {
320 let _profiling = self.runtime.ctx.enter_profiling_boundary();
321 let _slice = self.runtime.begin_job_slice();
322 if self.scripts_executed {
323 return;
324 }
325 self.scripts_executed = true;
326 self.startup_events_pending = true;
327
328 crate::docwrite::begin(&mut self.runtime.context);
329 loop {
330 let progressed = self.run_pending_scripts();
331 if !progressed
332 || self.runtime.jobs_remain()
333 || !crate::docwrite::active(&self.runtime.context)
334 {
335 break;
336 }
337 }
338 self.finish_startup_events();
339
340 self.request_redraw();
341 self.arm_timer_thread();
342 }
343
344 pub fn external_script_urls(&self) -> Vec<Url> {
352 if let Some(original) = crate::docwrite::original(&self.runtime.context) {
353 return blitz_html::scan_script_tags(&original)
356 .into_iter()
357 .filter(|script| script.is_javascript())
358 .filter_map(|script| script.src)
359 .filter_map(|src| self.resolve_script_url(src.as_ref()))
360 .collect();
361 }
362 self.collect_scripts()
363 .iter()
364 .filter(|script| script.kind != ScriptKind::ImportMap)
367 .filter_map(|script| script.src.as_deref())
368 .filter_map(|src| self.resolve_script_url(src))
369 .collect()
370 }
371
372 fn resolve_script_url(&self, src: &str) -> Option<Url> {
374 match &self.base_url {
375 Some(base) => base.join(src).ok(),
376 None => Url::parse(src).ok(),
377 }
378 }
379
380 pub fn with_js_context<R>(
386 &mut self,
387 callback: impl FnOnce(&mut boa_engine::Context) -> R,
388 ) -> R {
389 callback(&mut self.runtime.context)
390 }
391
392 pub fn eval(&mut self, code: &str) {
394 let _profiling = self.runtime.ctx.enter_profiling_boundary();
395 self.runtime.eval(code, "<eval>");
396 self.request_redraw();
397 self.arm_timer_thread();
398 }
399
400 pub fn eval_named(&mut self, code: &str, name: &str) {
403 let _profiling = self.runtime.ctx.enter_profiling_boundary();
404 self.runtime.eval_internal(code, name);
405 self.runtime.run_jobs(name);
406 self.request_redraw();
407 self.arm_timer_thread();
408 }
409
410 pub fn eval_json(&mut self, code: &str) -> Result<serde_json::Value, String> {
416 let _profiling = self.runtime.ctx.enter_profiling_boundary();
417 let result = self.runtime.eval_json(code, "<eval with result>");
418 self.request_redraw();
419 self.arm_timer_thread();
420 result
421 }
422
423 #[cfg(feature = "debug-control")]
424 pub fn diagnostic_batch(
425 &self,
426 console_after: u64,
427 errors_after: u64,
428 limit: usize,
429 ) -> crate::DiagnosticBatch {
430 self.runtime
431 .diagnostic_batch(console_after, errors_after, limit)
432 }
433
434 #[cfg(feature = "debug-control")]
435 pub(crate) fn console_entries_after(
436 &self,
437 sequence: u64,
438 ) -> Vec<crate::runtime::DiagnosticEntry> {
439 self.runtime.console_entries_after(sequence)
440 }
441
442 #[cfg(feature = "debug-control")]
443 pub(crate) fn runtime_errors_after(
444 &self,
445 sequence: u64,
446 ) -> Vec<crate::runtime::DiagnosticEntry> {
447 self.runtime.runtime_errors_after(sequence)
448 }
449
450 pub fn current_url(&self) -> Option<&Url> {
452 self.base_url.as_ref()
453 }
454
455 pub fn page_source(&self) -> String {
457 self.inner.borrow().root_element().outer_html()
458 }
459
460 pub fn dispatch_dom_event(&mut self, event: DomEvent) {
465 let profiling_boundary = self.runtime.ctx.enter_profiling_boundary();
466 let profiling = profiling_boundary.enabled();
467 let handler = ScriptEventHandler {
468 runtime: &mut self.runtime,
469 profiling,
470 };
471 let mut driver = EventDriver::new(&mut self.inner, handler);
472 driver.handle_dom_event(event);
473
474 self.request_redraw();
475 self.arm_timer_thread();
476 }
477
478 fn run_pending_scripts(&mut self) -> bool {
491 let _slice = self.runtime.begin_job_slice();
492 if self.runtime.jobs_remain() {
493 return false;
494 }
495 if crate::docwrite::take_restart(&self.runtime.context) {
496 self.executed_scripts.clear();
497 self.startup_events_pending = true;
498 self.load_event_pending = false;
499 }
500 let parsed = crate::docwrite::advance(&mut self.runtime.context);
501 let generation = crate::docwrite::generation(&self.runtime.context);
502 if parsed {
503 self.runtime.ctx.mark_layout_dirty();
504 }
505 let pending: Vec<PendingScript> = self
508 .collect_scripts()
509 .into_iter()
510 .filter(|script| !self.executed_scripts.contains(&script.node_id))
511 .filter(|script| {
512 !crate::docwrite::was_executed(&self.runtime.context, script.node_id)
513 && (!script.deferred || !crate::docwrite::active(&self.runtime.context))
514 })
515 .collect();
516 let ran = parsed || !pending.is_empty();
517
518 for script in &pending {
523 if script.kind == ScriptKind::ImportMap && !script.inline_text.trim().is_empty() {
524 self.runtime
525 .set_import_map(&script.inline_text, self.base_url.as_ref());
526 }
527 }
528
529 let (deferred, immediate): (Vec<_>, Vec<_>) =
540 pending.into_iter().partition(|script| script.deferred);
541
542 for script in immediate.into_iter().chain(deferred) {
543 if generation != crate::docwrite::generation(&self.runtime.context) {
544 break;
545 }
546 if self.startup_events_pending && script.deferred {
547 self.runtime.set_document_ready_state("interactive");
548 if self.runtime.jobs_remain() {
549 break;
550 }
551 }
552 self.executed_scripts.insert(script.node_id);
556 if script.kind == ScriptKind::ImportMap {
557 continue;
558 }
559 match script.src {
560 Some(src) => {
561 let Some(url) = self.resolve_script_url(&src) else {
562 eprintln!("blitz-script: could not resolve script URL {src:?}");
563 self.runtime.dispatch_node_event(script.node_id, "error");
564 if self.runtime.jobs_remain() {
565 break;
566 }
567 continue;
568 };
569 let fetcher = Rc::clone(&self.fetcher.borrow());
570 match fetcher.fetch(&url) {
571 Ok(code) => {
572 if script.kind == ScriptKind::Module {
573 self.runtime.eval_module(&code, Some(&url), url.as_str());
574 } else {
575 crate::domc::set_current_script(
576 &self.runtime.context,
577 Some(script.node_id),
578 );
579 self.runtime.eval_at(&code, Some(&url), url.as_str());
580 }
581 self.runtime.dispatch_node_event(script.node_id, "load");
582 }
583 Err(error) => {
584 eprintln!("blitz-script: failed to fetch script {url}: {error}");
585 self.runtime.dispatch_node_event(script.node_id, "error");
586 }
587 }
588 }
589 None => {
590 if !script.inline_text.trim().is_empty() {
591 let base_url = self.base_url.clone();
595 if script.kind == ScriptKind::Module {
596 self.runtime.eval_module(
597 &script.inline_text,
598 base_url.as_ref(),
599 "<inline module>",
600 );
601 } else {
602 crate::domc::set_current_script(
603 &self.runtime.context,
604 Some(script.node_id),
605 );
606 self.runtime.eval_at(
607 &script.inline_text,
608 base_url.as_ref(),
609 "<inline script>",
610 );
611 }
612 }
613 }
614 }
615 if self.runtime.jobs_remain() {
618 break;
619 }
620 }
621 ran
622 }
623
624 fn finish_startup_events(&mut self) -> bool {
625 if !(self.startup_events_pending || self.load_event_pending)
626 || self.runtime.jobs_remain()
627 || crate::docwrite::active(&self.runtime.context)
628 {
629 return false;
630 }
631 if self.startup_events_pending
632 && (self.runtime.has_pending_module_evaluations()
633 || self.collect_scripts().iter().any(|script| {
634 !self.executed_scripts.contains(&script.node_id)
635 && !crate::docwrite::was_executed(&self.runtime.context, script.node_id)
636 }))
637 {
638 return false;
639 }
640
641 let mut ran = false;
642 if self.startup_events_pending {
643 ran |= self.runtime.set_document_ready_state("interactive");
644 if self.runtime.jobs_remain() {
645 return ran;
646 }
647 self.startup_events_pending = false;
648 self.load_event_pending = true;
649 self.runtime.dispatch_document_event("DOMContentLoaded");
650 ran = true;
651 }
652 let resources_pending = {
653 let doc = self.inner.borrow();
654 doc.has_pending_critical_resources() || doc.pending_image_count() != 0
655 };
656 if self.load_event_pending
657 && !self.runtime.jobs_remain()
658 && !resources_pending
659 && !self.runtime.has_pending_module_evaluations()
660 {
661 ran |= self.runtime.set_document_ready_state("complete");
662 if !self.runtime.jobs_remain() {
663 self.load_event_pending = false;
664 self.runtime.dispatch_window_event("load");
665 ran = true;
666 }
667 }
668 ran
669 }
670
671 fn collect_scripts(&self) -> Vec<PendingScript> {
673 let doc = self.inner.borrow();
674 let mut scripts = Vec::new();
675 let mut stack = vec![doc.root_node().id];
676
677 while let Some(node_id) = stack.pop() {
678 let Some(node) = doc.get_node(node_id) else {
679 continue;
680 };
681
682 if let Some(element) = node.element_data() {
683 if element.name.local == blitz_dom::local_name!("script") {
684 if node
685 .flags
686 .contains(blitz_dom::node::NodeFlags::IS_PARSER_INERT_SCRIPT)
687 {
688 continue;
689 }
690 let script_type = element
692 .attr(blitz_dom::local_name!("type"))
693 .unwrap_or("")
694 .trim()
695 .to_ascii_lowercase();
696 let kind = match script_type.as_str() {
697 "module" => Some(ScriptKind::Module),
698 "importmap" => Some(ScriptKind::ImportMap),
699 "" | "text/javascript" | "application/javascript" => {
700 if element.attr(blitz_dom::local_name!("nomodule")).is_some() {
705 None
706 } else {
707 Some(ScriptKind::Classic)
708 }
709 }
710 _ => None,
711 };
712
713 if let Some(kind) = kind {
714 let src = element
715 .attr(blitz_dom::local_name!("src"))
716 .map(str::to_string);
717 let is_async = element.attr(blitz_dom::local_name!("async")).is_some();
718
719 let deferred = !is_async
723 && match kind {
724 ScriptKind::Module => true,
725 ScriptKind::Classic => {
726 src.is_some()
727 && element.attr(blitz_dom::local_name!("defer")).is_some()
728 }
729 ScriptKind::ImportMap => false,
730 };
731
732 scripts.push(PendingScript {
733 node_id,
734 src,
735 inline_text: node.text_content(),
736 kind,
737 deferred,
738 });
739 }
740 continue;
741 }
742 }
743
744 stack.extend(node.children.iter().rev().copied());
745 }
746
747 scripts
748 }
749
750 fn request_redraw(&self) {
751 self.inner.borrow().shell_provider.request_redraw();
752 }
753
754 fn arm_timer_thread(&mut self) {
757 let Some(deadline) = self.runtime.next_timer_deadline() else {
758 return;
759 };
760
761 let sender = self.timer_thread.get_or_insert_with(|| {
762 let (tx, rx) = channel::<Instant>();
763 let waker = Arc::clone(&self.waker);
764 std::thread::Builder::new()
765 .name("blitz-script-timers".to_string())
766 .spawn(move || timer_thread_main(rx, waker))
767 .expect("failed to spawn timer thread");
768 tx
769 });
770
771 if sender.send(deadline).is_err() {
774 self.timer_thread = None;
775 }
776 }
777}
778
779fn timer_thread_main(rx: Receiver<Instant>, waker: Arc<Mutex<Option<Waker>>>) {
781 let mut deadline: Option<Instant> = None;
782
783 loop {
784 match deadline {
785 None => match rx.recv() {
786 Ok(new_deadline) => deadline = Some(new_deadline),
787 Err(_) => return,
788 },
789 Some(current) => {
790 let now = Instant::now();
791 if current <= now {
792 if let Some(waker) = waker.lock().unwrap().as_ref() {
793 waker.wake_by_ref();
794 }
795 deadline = None;
796 continue;
797 }
798 match rx.recv_timeout(current - now) {
799 Ok(new_deadline) => deadline = Some(new_deadline.min(current)),
800 Err(RecvTimeoutError::Timeout) => {
801 if let Some(waker) = waker.lock().unwrap().as_ref() {
802 waker.wake_by_ref();
803 }
804 deadline = None;
805 }
806 Err(RecvTimeoutError::Disconnected) => return,
807 }
808 }
809 }
810 }
811}
812
813impl Document for ScriptDocument {
814 fn inner(&self) -> DocGuard<'_> {
815 DocGuard::RefCell(self.inner.borrow())
816 }
817
818 fn inner_mut(&mut self) -> DocGuardMut<'_> {
819 DocGuardMut::RefCell(self.inner.borrow_mut())
820 }
821
822 fn handle_ui_event(&mut self, event: UiEvent) {
823 let profiling_boundary = self.runtime.ctx.enter_profiling_boundary();
824 let profiling = profiling_boundary.enabled();
825 let handler = ScriptEventHandler {
826 runtime: &mut self.runtime,
827 profiling,
828 };
829 let mut driver = EventDriver::new(&mut self.inner, handler);
830 driver.handle_ui_event(event);
831
832 self.request_redraw();
834 self.arm_timer_thread();
835 }
836
837 fn poll(&mut self, task_context: Option<TaskContext>) -> bool {
838 self.poll_with_timer_policy(task_context, None)
839 }
840
841 fn poll_for_settle(&mut self, task_context: Option<TaskContext>, run_timers: bool) -> bool {
842 self.poll_with_timer_policy(task_context, Some(run_timers))
843 }
844}
845
846impl ScriptDocument {
847 fn poll_with_timer_policy(
849 &mut self,
850 task_context: Option<TaskContext>,
851 settle_timers: Option<bool>,
852 ) -> bool {
853 let profiling_boundary = self.runtime.ctx.enter_profiling_boundary();
854 let profiling = profiling_boundary.enabled();
855 let poll_started = profiling.then(std::time::Instant::now);
856 let _slice = self.runtime.begin_job_slice();
857 let ran = self.poll_inner(task_context, profiling, settle_timers);
858 if let Some(started) = poll_started {
859 crate::script_stats::record_poll(started.elapsed(), ran);
860 }
861 ran
862 }
863
864 pub fn handle_pointer_move_to_node(&mut self, event: BlitzPointerEvent, node_id: NodeId) {
871 let profiling_boundary = self.runtime.ctx.enter_profiling_boundary();
872 let profiling = profiling_boundary.enabled();
873 let handler = ScriptEventHandler {
874 runtime: &mut self.runtime,
875 profiling,
876 };
877 let mut driver = EventDriver::new(&mut self.inner, handler);
878 driver.handle_pointer_move_to_node(&event, node_id);
879
880 self.request_redraw();
881 self.arm_timer_thread();
882 }
883
884 pub fn handle_ui_event_to_node(&mut self, event: UiEvent, node_id: NodeId) {
888 let profiling_boundary = self.runtime.ctx.enter_profiling_boundary();
889 let profiling = profiling_boundary.enabled();
890 let handler = ScriptEventHandler {
891 runtime: &mut self.runtime,
892 profiling,
893 };
894 let mut driver = EventDriver::new(&mut self.inner, handler);
895 driver.handle_ui_event_to_node(event, node_id);
896
897 self.request_redraw();
898 self.arm_timer_thread();
899 }
900
901 fn poll_inner(
904 &mut self,
905 task_context: Option<TaskContext>,
906 profiling: bool,
907 settle_timers: Option<bool>,
908 ) -> bool {
909 if let Some(cx) = &task_context {
911 let mut waker = self.waker.lock().unwrap();
912 let stale = waker
913 .as_ref()
914 .map(|old| !old.will_wake(cx.waker()))
915 .unwrap_or(true);
916 if stale {
917 *waker = Some(cx.waker().clone());
918 }
919 }
920
921 let subdocument_changes = {
926 let mut inner = self.inner.borrow_mut();
927 let waker = task_context.as_ref().map(TaskContext::waker);
928 match settle_timers {
929 Some(run_timers) => inner.poll_subdocuments_for_settle(waker, run_timers),
930 None => inner.poll_subdocuments(waker),
931 }
932 };
933
934 let mut ran = subdocument_changes;
935 ran |= self.runtime.poll_domc();
936 ran |= self.runtime.run_jobs_with_progress("poll microtasks");
939
940 if !self.scripts_executed {
942 let started = profiling.then(std::time::Instant::now);
946 self.execute_scripts();
947 if let Some(started) = started {
948 crate::script_stats::record_work("startup:execute_scripts", started.elapsed());
949 }
950 ran = true;
951 } else {
952 ran |= self.run_pending_scripts();
956 }
957 ran |= self.finish_startup_events();
958
959 if settle_timers.unwrap_or(true) {
960 ran |= self.runtime.run_due_timers(profiling);
961 }
962
963 self.runtime.poll_module_evaluations();
967
968 if let Some(mut hook) = self.poll_hook.take() {
969 let started = profiling.then(std::time::Instant::now);
973 ran |= hook(self, task_context.as_ref());
974 if let Some(started) = started {
975 crate::script_stats::record_work("poll_hook", started.elapsed());
976 }
977 self.poll_hook = Some(hook);
978 }
979
980 self.runtime.sweep_detached_nodes();
985
986 if self.runtime.jobs_remain() {
987 ran = true;
988 self.request_redraw();
989 if let Some(cx) = &task_context {
990 cx.waker().wake_by_ref();
991 }
992 }
993 self.arm_timer_thread();
994 ran
995 }
996}