diff --git a/src/browser/ScriptManagerBase.zig b/src/browser/ScriptManagerBase.zig index 509b4523f..277332a18 100644 --- a/src/browser/ScriptManagerBase.zig +++ b/src/browser/ScriptManagerBase.zig @@ -306,18 +306,19 @@ pub fn preloadModuleHint(self: *ScriptManagerBase, element: *Element.Html, url: } pub fn waitForImport(self: *ScriptManagerBase, url: [:0]const u8) !ModuleSource { - const entry = self.imported_modules.getEntry(url) orelse { - // It shouldn't be possible for v8 to ask for a module that we didn't - // `preloadImport` above. - return error.UnknownModule; - }; - const was_evaluating = self.is_evaluating; self.is_evaluating = true; defer self.endEvaluationWindow(was_evaluating); var client = self.client; while (true) { + // imported_modules can be mutated by client.tick, so we need to lookup + // the entry on each iteration. + const entry = self.imported_modules.getEntry(url) orelse { + // It shouldn't be possible for v8 to ask for a module that we + // didn't `preloadImport` above. + return error.UnknownModule; + }; switch (entry.value_ptr.state) { .loading => { _ = try client.tick(200, .sync_wait); diff --git a/src/browser/js/Context.zig b/src/browser/js/Context.zig index 296eaa2d1..da55f56b8 100644 --- a/src/browser/js/Context.zig +++ b/src/browser/js/Context.zig @@ -746,8 +746,7 @@ fn _resolveModuleCallback(self: *Context, referrer: js.Module, specifier: [:0]co specifier, ); - const entry = self.module_cache.getPtr(normalized_specifier).?; - if (entry.module) |m| { + if (self.module_cache.getPtr(normalized_specifier).?.module) |m| { // This import registered a waiter via preloadImport when it was discovered // but the compiled module is already cached so we don't have to call // waitForImport. Release our waiter so we no longer hold on waiter on @@ -773,6 +772,9 @@ fn _resolveModuleCallback(self: *Context, referrer: js.Module, specifier: [:0]co const mod = try compileModule(local, source.src(), normalized_specifier); try self.postCompileModule(mod, normalized_specifier, local); + // waitForImport can cause module_cache to be mutated (via HttpClient.tick), + // so we need to refetch this incase the hashmap changed + const entry = self.module_cache.getPtr(normalized_specifier).?; entry.module = try mod.persist(); // Note: We don't instantiate/evaluate here - V8 will handle instantiation // as part of the parent module's dependency chain. If there's a resolver diff --git a/src/browser/webapi/Timers.zig b/src/browser/webapi/Timers.zig index c92f7973b..ac1c3c169 100644 --- a/src/browser/webapi/Timers.zig +++ b/src/browser/webapi/Timers.zig @@ -29,11 +29,21 @@ const js = @import("../js/js.zig"); const log = lp.log; const Allocator = std.mem.Allocator; +const CLAMP_MS = 4; +const CLAMP_NESTING = 5; + const Timers = @This(); _timer_id: u30 = 0, _callbacks: CallbackHashMap = .{}, +// We keep the depth of the timers (a setTimeout calling a setTimeout). When +// the depth reaches CLAMP_NESTING, the minimum timeout is 4ms. This is per- +// spec and it's necessary to prevent some sites from virtually breaking because +// they repeatedly do heavy work in endlessly looping setTimeout with a short +// timeout (often of 0ms) +_nesting_level: u8 = 0, + const Key = u32; const CallbackHashMap = std.HashMapUnmanaged( Key, @@ -82,6 +92,9 @@ pub fn schedule( const timer_id = self._timer_id +% 1; self._timer_id = timer_id; + const nesting = @min(self._nesting_level + 1, CLAMP_NESTING + 1); + const delay = if (nesting > CLAMP_NESTING and delay_ms < CLAMP_MS) CLAMP_MS else delay_ms; + var persisted_params: []js.Value.Temp = &.{}; if (opts.params.len > 0) { persisted_params = try arena.dupe(js.Value.Temp, opts.params); @@ -102,13 +115,14 @@ pub fn schedule( .arena = arena, .mode = opts.mode, .name = opts.name, + .nesting = nesting, .timer_id = timer_id, .params = persisted_params, - .repeat_ms = if (opts.repeat) if (delay_ms == 0) 1 else delay_ms else null, + .repeat_ms = if (opts.repeat) if (delay == 0) 1 else delay else null, }; gop.value_ptr.* = callback; - try exec.js.scheduler.add(callback, ScheduleCallback.run, delay_ms, .{ + try exec.js.scheduler.add(callback, ScheduleCallback.run, delay, .{ .name = opts.name, .low_priority = opts.low_priority, .finalizer = ScheduleCallback.cancelled, @@ -152,6 +166,10 @@ const ScheduleCallback = struct { // delay, in ms, to repeat. When null, removed after first invocation. repeat_ms: ?u32, + // The nesting of this task. When it executes, this nesting will become + // the Timer's _nesting_level so that any new timers will become nesting + 1 + nesting: u8, + cb: js.Function.Temp, mode: Mode, @@ -185,6 +203,11 @@ const ScheduleCallback = struct { self.exec.js.localScope(&ls); defer ls.deinit(); + const timers = self.timers; + const prev_nesting = timers._nesting_level; + timers._nesting_level = self.nesting; + defer timers._nesting_level = prev_nesting; + switch (self.mode) { .idle => { const IdleDeadline = @import("IdleDeadline.zig"); @@ -210,6 +233,12 @@ const ScheduleCallback = struct { ls.local.runMicrotasks(); if (self.repeat_ms) |ms| { + // each repeat re-enters the timer initialization steps, so the + // nesting level keeps growing and sub-4ms intervals get clamped. + self.nesting = @min(self.nesting + 1, CLAMP_NESTING + 1); + if (self.nesting > CLAMP_NESTING and ms < CLAMP_MS) { + return CLAMP_MS; + } return ms; } defer self.deinit();