Skip to content

perf(promise_core): migrate timer queue to @priority_queue#433

Merged
dowdiness merged 2 commits into
mainfrom
perf/timer-queue-priority-queue
Jun 21, 2026
Merged

perf(promise_core): migrate timer queue to @priority_queue#433
dowdiness merged 2 commits into
mainfrom
perf/timer-queue-priority-queue

Conversation

@dowdiness

@dowdiness dowdiness commented Jun 21, 2026

Copy link
Copy Markdown
Owner

Closes #329.

What changed

Replaces Array[TimerTask] + sort_by + remove(0) in run_timers (O(n² log n) per invocation) with @priority_queue.PriorityQueue[TimerTask] (O(n log n) total drain).

Heap direction. @priority_queue is a max-heap. Compare is inverted — smaller (delay, insertion_order) compares as "greater" — so pop() always extracts the soonest-due timer. This preserves the WHATWG insertion-order tie-break that the existing equal-delay test pins.

Cancellation. The old code had a dual path: mutate timer.cancelled = true in-queue AND record in cancelled_timer_ids. With PriorityQueue, in-heap mutation is impossible. mut cancelled is removed from TimerTask; all cancellation goes through cancelled_timer_ids (lazy deletion). The drain loop skips any popped timer whose id is present in the set. The id set is cleared only after a full drain, so timers that survive the 10k-iteration safety limit are still correctly skipped on the next run_timers call.

Microtask queue deliberately unchanged. The head-pointer drain in run_microtasks is already O(n). Migrating to @deque would be API cleanup with no complexity benefit and is out of scope here.

Benchmark results

200-timer workload (event_loop/timer_drain_200):

Before After
timer_drain_200 4.19 ms ± 1.14 ms 1.95 ms ± 0.38 ms (2.15×)
microtask_drain_200 6.34 ms ± 1.36 ms 6.21 ms ± 1.22 ms (unchanged)

The microtask bench is the control: it has exactly one setTimeout sentinel, so array vs heap makes no O(n) difference — the flat result confirms only the timer path was affected.

Tests added

  • timers with equal delay fire in registration order — WHATWG tie-break invariant
  • clearTimeout cancels timer before it fires — lazy-deletion path for clearTimeout
  • clearInterval inside callback stops re-enqueue — cancellation during interval callback

Files changed

File Change
interpreter/runtime/interpreter.mbt Remove mut cancelled: Bool from TimerTask; change timer_queue field type to PriorityQueue[TimerTask]
interpreter/runtime/promise_core.mbt Add pub impl Eq/Compare for TimerTask; rewrite run_timers drain loop
interpreter/stdlib/builtins_promise.mbt Remove cancelled: false from struct literal; simplify cancel_timer to lazy-deletion-only
interpreter/runtime/moon.pkg Add @priority_queue import
benchmarks/ Add @priority_queue import, event_loop/* workload constants, two benchmark tests
interpreter/interpreter_test.mbt Add three timer tests
*.mbti Regenerated via moon info

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added new event-loop benchmarks for microtask and timer queue testing scenarios.
  • Bug Fixes

    • Fixed setTimeout callback execution order for equal-delay timers.
    • Fixed clearTimeout to properly prevent cancelled callbacks from executing.
    • Fixed setInterval to stop re-enqueueing when clearInterval is called from within the callback.
  • Tests

    • Added regression tests for timer ordering, cancellation, and interval behavior.



Replace the Array[TimerTask] + sort_by + remove(0) pattern (O(n² log n)
per run_timers call) with @priority_queue.PriorityQueue[TimerTask] (O(n
log n) total). The priority queue is a max-heap; Compare is reversed so
pop() always extracts the soonest-due timer first. Equal-delay tie-break
is by insertion_order, preserving WHATWG insertion-order semantics.

Cancellation moves to lazy deletion only: clearTimeout/clearInterval set
cancelled_timer_ids; the drain loop skips popped timers whose id is in
that set. The mut cancelled struct field is removed. The cancelled_ids map
is cleared only after a full drain so that timers surviving the 10k-
iteration safety limit are still correctly skipped on the next call.

Microtask queue is deliberately left unchanged: the head-pointer drain
is already O(n); @DeQue would be API cleanup only with no complexity win.

Benchmark results (200-timer/microtask workloads):
  event_loop/timer_drain_200:      4.19 ms → 1.95 ms  (2.15× speedup)
  event_loop/microtask_drain_200:  unchanged within noise (control)

New tests: equal-delay registration order, clearTimeout cancel, and
clearInterval-stops-after-N-fires cover all three cancellation paths.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@dowdiness, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 46 minutes and 19 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 65ace87b-c365-47e7-bedf-c19a0eeb6476

📥 Commits

Reviewing files that changed from the base of the PR and between f24707e and d613b17.

📒 Files selected for processing (4)
  • benchmarks/benchmarks.mbt
  • benchmarks/workloads.mbt
  • interpreter/interpreter_test.mbt
  • interpreter/stdlib/builtins_promise.mbt
📝 Walkthrough

Walkthrough

Migrates HostEnv.timer_queue from Array[TimerTask] to @priority_queue.PriorityQueue[TimerTask]. TimerTask gains Eq (by id) and Compare (by delay, then insertion_order) implementations and drops its mutable cancelled field. run_timers switches from sort-plus-remove to pop(), and cancel_timer moves from eager O(n) field mutation to lazy O(1) insertion into cancelled_timer_ids. Three regression tests and two event-loop benchmark workloads are added.

Changes

Timer Queue Priority Queue Migration

Layer / File(s) Summary
TimerTask Eq/Compare contract and field removal
interpreter/runtime/interpreter.mbt, interpreter/runtime/promise_core.mbt, interpreter/runtime/pkg.generated.mbti, interpreter/runtime/moon.pkg
Removes the mutable cancelled field from TimerTask and adds Eq (equality by id) and Compare (ordered by delay then insertion_order, negated for min-heap extraction) implementations, with matching package import and generated interface updates.
HostEnv timer_queue field type change and initialization
interpreter/runtime/interpreter.mbt, interpreter/runtime/pkg.generated.mbti, benchmarks/startup_new_interpreter_subphases.mbt
Changes HostEnv.timer_queue from Array[TimerTask] to @priority_queue.PriorityQueue[TimerTask] in the struct definition, constructor, benchmark startup host, and generated interface.
run_timers pop-based loop and lazy cancellation
interpreter/runtime/promise_core.mbt
Rewrites run_timers to extract the next due timer via timer_queue.pop() instead of sort-plus-remove(0), lazily skips timers whose id is in cancelled_timer_ids, conditionally re-enqueues setInterval timers based on cancelled_timer_ids, and clears cancelled_timer_ids only when the queue fully drains.
schedule_timer and cancel_timer lazy cancellation
interpreter/stdlib/builtins_promise.mbt
Removes cancelled field initialization from schedule_timer and rewrites cancel_timer from an O(n) scan that mutates a per-task flag to an O(1) insertion into cancelled_timer_ids.
Timer ordering and cancellation regression tests
interpreter/interpreter_test.mbt
Adds three tests: equal-delay setTimeout callbacks fire in registration order; clearTimeout before firing prevents execution; clearInterval from inside the callback stops re-enqueueing after exactly N runs.
Event-loop benchmark workloads and test cases
benchmarks/workloads.mbt, benchmarks/benchmarks.mbt, benchmarks/pkg.generated.mbti, benchmarks/moon.pkg
Defines MICROTASK_DRAIN_200_SRC/OUTPUT and TIMER_DRAIN_200_SRC/OUTPUT constants, adds benchmark test cases with correctness pre-checks that abort on output mismatch, and updates package imports and generated interface.

Sequence Diagram(s)

sequenceDiagram
  participant JS as JS Code
  participant builtins as builtins_promise
  participant host as HostEnv
  participant run as run_timers

  JS->>builtins: setTimeout(cb, delay)
  builtins->>host: timer_queue.push(TimerTask{id, delay, ...})

  JS->>builtins: clearTimeout(id)
  builtins->>host: cancelled_timer_ids[id] = true

  run->>host: timer_queue.pop() → TimerTask
  run->>host: cancelled_timer_ids.contains(timer.id)?
  alt cancelled
    run->>host: skip, pop next
  else not cancelled
    run->>run: execute callback
    run->>run: drain microtasks
    alt setInterval and not cancelled
      run->>host: timer_queue.push(re-enqueued TimerTask)
    end
  end
  alt queue empty
    run->>host: clear cancelled_timer_ids
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • dowdiness/js_engine#16: Modifies the interpreter's timer/microtask execution path including Interpreter::run_timers and timer queue data, which this PR further migrates to a @priority_queue-based structure.
  • dowdiness/js_engine#44: Introduces HostEnv.cancelled_timer_ids and timer scheduling/cancellation logic in builtins_promise and run_timers, the exact infrastructure this PR extends with lazy cancellation and PriorityQueue.

Poem

🐇 Hop, hop, the timers align,
No more O(n) scan down the line!
A priority heap pops the soonest due,
Lazy cancellation sees the old IDs through.
The rabbit sorts by delay with glee —
benchmarks and tests prove correctness, whee! 🎉

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: migration of the timer queue to @priority_queue for performance optimization.
Linked Issues check ✅ Passed The PR comprehensively addresses all coding requirements from issue #329: migrates timer queue to @priority_queue [#329], implements proper Compare/Eq for ordering [#329], adds correctness tests for timer ordering [#329], and includes performance benchmarks [#329].
Out of Scope Changes check ✅ Passed All changes are directly scoped to the timer queue migration. Microtask queue changes appropriately limited to dependency updates, and cancellation refactoring is integral to the timer queue rewrite.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/timer-queue-priority-queue

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f24707e240

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

None => break // unreachable: guarded by length() > 0 above
}
// Lazy deletion: skip timers cancelled via clearTimeout/clearInterval
if self.host.cancelled_timer_ids.contains(timer.id) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Prevent stale cancellation IDs from cancelling future timers

When clearTimeout/clearInterval is called with an ID that is not currently queued, cancel_timer still records that ID; with this new lazy-deletion check applied to every popped timer, a fresh script like clearTimeout(1); setTimeout(() => console.log("ran"), 0) skips the later timer because the first allocated ID is also 1. Clearing an unknown handle should be a no-op, so the cancellation set needs to be limited to pending/active IDs or stale IDs need to be removed before a newly scheduled timer can inherit them.

Useful? React with 👍 / 👎.

Comment thread benchmarks/benchmarks.mbt
)
}
b.bench(fn() {
let _ = @lib.run(TIMER_DRAIN_200_SRC) catch {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Measure timer drain without reparsing the workload

This benchmark loop calls @lib.run, and that facade constructs a new interpreter and parses the source before draining timers, so event_loop/timer_drain_200 measures startup/parse work along with the priority-queue drain despite being used as the queue-drain gate. A parser/startup change can mask or fake a timer-queue regression here; use a lower-level/pre-parsed setup or relabel the benchmark before relying on these numbers for the migration.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
benchmarks/benchmarks.mbt (1)

486-497: ⚡ Quick win

Use fail(...) for benchmark defect checks instead of abort(...).

Line 486-Line 497 and Line 513-Line 524 introduce new abort(...) checks; these should use fail(...) per project convention.

As per coding guidelines: "Use fail("msg") for defect detection instead of abort() — it is catchable and provides source location."

Also applies to: 513-524

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@benchmarks/benchmarks.mbt` around lines 486 - 497, Replace all `abort(...)`
calls with `fail(...)` throughout the benchmark defect checks in the
MICROTASK_DRAIN_200 benchmark test. This includes the pre-check error handling
at line 486 (the catch block after the pre-check), the correctness check failure
at line 489-492 (the abort call with the expected vs got comparison message),
and the bench loop error handling at line 496 (the catch block within b.bench).
Also apply the same change to the other benchmark test mentioned in the same
range (lines 513-524). The `fail(...)` function should be used instead of
`abort(...)` per project convention as it is catchable and provides source
location information.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@benchmarks/benchmarks.mbt`:
- Around line 504-510: The benchmark description for run_timers() still
documents the old sort_by + remove(0) implementation with O(n² log n)
complexity, but this has been migrated to use a priority queue. Update the
comment block starting at line 504 to describe the current priority_queue
behavior and complexity characteristics, optionally noting the old behavior as
historical context to help readers understand the migration context referenced
in issue `#329`.

In `@benchmarks/workloads.mbt`:
- Around line 745-748: The documentation comment for TIMER_DRAIN_200_SRC is
outdated and describes the old O(n² log n) complexity based on sort_by and
remove(0) operations, but the run_timers() implementation has been migrated to
use a priority queue. Update the comment block preceding the TIMER_DRAIN_200_SRC
constant to accurately reflect the current priority-queue based implementation
and its actual complexity characteristics, replacing references to sort_by and
remove(0) with the appropriate priority queue operations.

---

Nitpick comments:
In `@benchmarks/benchmarks.mbt`:
- Around line 486-497: Replace all `abort(...)` calls with `fail(...)`
throughout the benchmark defect checks in the MICROTASK_DRAIN_200 benchmark
test. This includes the pre-check error handling at line 486 (the catch block
after the pre-check), the correctness check failure at line 489-492 (the abort
call with the expected vs got comparison message), and the bench loop error
handling at line 496 (the catch block within b.bench). Also apply the same
change to the other benchmark test mentioned in the same range (lines 513-524).
The `fail(...)` function should be used instead of `abort(...)` per project
convention as it is catchable and provides source location information.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c167e684-acd2-486c-8bd7-87ea63389de9

📥 Commits

Reviewing files that changed from the base of the PR and between 1841205 and f24707e.

📒 Files selected for processing (11)
  • benchmarks/benchmarks.mbt
  • benchmarks/moon.pkg
  • benchmarks/pkg.generated.mbti
  • benchmarks/startup_new_interpreter_subphases.mbt
  • benchmarks/workloads.mbt
  • interpreter/interpreter_test.mbt
  • interpreter/runtime/interpreter.mbt
  • interpreter/runtime/moon.pkg
  • interpreter/runtime/pkg.generated.mbti
  • interpreter/runtime/promise_core.mbt
  • interpreter/stdlib/builtins_promise.mbt

Comment thread benchmarks/benchmarks.mbt Outdated
Comment thread benchmarks/workloads.mbt Outdated
@github-actions

github-actions Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Benchmark Results

Run: https://github.com/dowdiness/js_engine/actions/runs/27904310137

startup/tiny_program is the PR #153 / issue #141 guardrail for built-in realm-stamping startup cost.

Stage summary

stage benchmarks total mean slowest benchmark slowest mean noisy rows
startup 3 2.467 ms startup/tiny_program 1.244 ms 0
frontend 7 0.936 ms pipeline/parse_heavy 0.516 ms 2
execution 26 14589.832 ms exec/fibonacci_30 13277.024 ms 2

Focused bytecode base-vs-head comparison

Base-vs-head deltas are reporting-only. Negative delta and PR/base < 1.00x mean the PR is faster; interpret high-CV or noisy rows cautiously.

benchmark stage base mean PR mean delta PR/base base CV PR CV noisy
baseline/bytecode/closure_factory execution 12.668 ms 13.516 ms +6.7% 1.07x 6.9% 6.8% no
pipeline/bytecode/evaluate execution 8.864 ms 8.974 ms +1.2% 1.01x 2.6% 3.4% no
isolate/bytecode/call_frame execution 7.360 ms 7.173 ms -2.5% 0.97x 0.9% 0.6% no
isolate/bytecode/runtime_helpers execution 10.982 ms 10.699 ms -2.6% 0.97x 1.9% 0.7% no
isolate/bytecode/local_access execution 36.768 ms 37.594 ms +2.2% 1.02x 1.6% 1.7% no
isolate/bytecode/env_access execution 37.336 ms 37.318 ms -0.0% 1.00x 1.9% 1.0% no
isolate/bytecode/captured_access execution 41.901 ms 38.499 ms -8.1% 0.92x 1.7% 3.8% no
isolate/bytecode/dispatch_stack execution 23.492 ms 23.759 ms +1.1% 1.01x 2.1% 2.1% no

Base-vs-head comparison

benchmark stage base mean PR mean delta PR/base base CV PR CV noisy
startup/tiny_program startup 1.184 ms 1.244 ms +5.1% 1.05x 4.7% 5.4% no
lexer/small frontend 0.036 ms 0.034 ms -5.9% 0.94x 32.6% 22.4% base, PR
lexer/large frontend 0.302 ms 0.304 ms +0.4% 1.00x 2.5% 2.1% no
exec/fibonacci_30 execution 12921.970 ms 13277.024 ms +2.7% 1.03x 0.6% 0.4% no
exec/property_chain execution 13.331 ms 13.645 ms +2.4% 1.02x 9.5% 14.6% no
startup/phase/parse_tiny frontend 0.002 ms 0.002 ms +0.8% 1.01x 0.5% 0.6% no
startup/phase/new_interpreter startup 1.081 ms 1.223 ms +13.1% 1.13x 8.4% 10.4% no
startup/phase/execute_preparsed_tiny execution 0.001 ms 0.000 ms -20.6% 0.79x 2.2% 0.7% no
startup/phase/event_loop_drain_empty startup 0.000 ms 0.000 ms +0.6% 1.01x 0.9% 10.0% no
startup/phase/result_stringify_output execution 0.000 ms 0.000 ms +7.5% 1.07x 0.5% 1.4% no
exec/array_map_filter execution 19.513 ms 20.187 ms +3.5% 1.03x 18.3% 22.1% base, PR
exec/closure_factory execution 28.541 ms 29.565 ms +3.6% 1.04x 6.9% 6.9% no
baseline/closure_legacy/closure_factory execution 27.215 ms 28.231 ms +3.7% 1.04x 9.7% 10.1% no
baseline/bytecode/closure_factory execution 12.668 ms 13.516 ms +6.7% 1.07x 6.9% 6.8% no
isolate/bytecode/dispatch_stack execution 23.492 ms 23.759 ms +1.1% 1.01x 2.1% 2.1% no
isolate/bytecode/local_access execution 36.768 ms 37.594 ms +2.2% 1.02x 1.6% 1.7% no
isolate/bytecode/env_access execution 37.336 ms 37.318 ms -0.0% 1.00x 1.9% 1.0% no
isolate/bytecode/captured_access execution 41.901 ms 38.499 ms -8.1% 0.92x 1.7% 3.8% no
isolate/bytecode/call_frame execution 7.360 ms 7.173 ms -2.5% 0.97x 0.9% 0.6% no
isolate/bytecode/runtime_helpers execution 10.982 ms 10.699 ms -2.6% 0.97x 1.9% 0.7% no
isolate/bytecode/property_get execution 45.044 ms 43.906 ms -2.5% 0.97x 1.5% 1.1% no
isolate/bytecode/property_set execution 39.498 ms 38.977 ms -1.3% 0.99x 0.8% 2.6% no
isolate/bytecode/method_call execution 8.609 ms 8.316 ms -3.4% 0.97x 1.0% 0.6% no
isolate/bytecode/object_literal execution 13.959 ms 13.960 ms +0.0% 1.00x 1.4% 2.7% no
isolate/bytecode/array_literal execution 14.654 ms 14.798 ms +1.0% 1.01x 2.8% 1.1% no
exec/for_of execution 5.620 ms 5.599 ms -0.4% 1.00x 5.2% 7.6% no
exec/arithmetic_loop execution 799.360 ms 858.060 ms +7.3% 1.07x 0.8% 0.1% no
exec/object_construction execution 7.158 ms 7.077 ms -1.1% 0.99x 8.2% 6.1% no
exec/string_ops execution 1.953 ms 2.052 ms +5.1% 1.05x 18.9% 22.8% base, PR
pipeline/exec/lex frontend 0.030 ms 0.030 ms +1.3% 1.01x 0.5% 0.9% no
pipeline/exec/parse frontend 0.027 ms 0.028 ms +2.1% 1.02x 2.8% 3.4% no
pipeline/exec/evaluate execution 25.494 ms 26.195 ms +2.7% 1.03x 4.5% 12.5% no
pipeline/closure_legacy/evaluate execution 24.521 ms 24.706 ms +0.8% 1.01x 5.7% 4.4% no
pipeline/bytecode/compile frontend 0.022 ms 0.022 ms -0.7% 0.99x 30.4% 24.7% base, PR
pipeline/bytecode/evaluate execution 8.864 ms 8.974 ms +1.2% 1.01x 2.6% 3.4% no
pipeline/parse_heavy frontend 0.515 ms 0.516 ms +0.1% 1.00x 6.4% 4.9% no

Mean-time chart (log scale)

benchmark stage mean chart
startup/tiny_program startup 1.244 ms ##
lexer/small frontend 0.034 ms ⚠ #
lexer/large frontend 0.304 ms #
exec/fibonacci_30 execution 13277.024 ms ##############################
exec/property_chain execution 13.645 ms ########
startup/phase/parse_tiny frontend 0.002 ms #
startup/phase/new_interpreter startup 1.223 ms ##
startup/phase/execute_preparsed_tiny execution 0.000 ms #
startup/phase/event_loop_drain_empty startup 0.000 ms #
startup/phase/result_stringify_output execution 0.000 ms #
exec/array_map_filter execution 20.187 ms ⚠ #########
exec/closure_factory execution 29.565 ms ##########
baseline/closure_legacy/closure_factory execution 28.231 ms ##########
baseline/bytecode/closure_factory execution 13.516 ms ########
isolate/bytecode/dispatch_stack execution 23.759 ms ##########
isolate/bytecode/local_access execution 37.594 ms ###########
isolate/bytecode/env_access execution 37.318 ms ###########
isolate/bytecode/captured_access execution 38.499 ms ###########
isolate/bytecode/call_frame execution 7.173 ms ######
isolate/bytecode/runtime_helpers execution 10.699 ms #######
isolate/bytecode/property_get execution 43.906 ms ############
isolate/bytecode/property_set execution 38.977 ms ###########
isolate/bytecode/method_call execution 8.316 ms #######
isolate/bytecode/object_literal execution 13.960 ms ########
isolate/bytecode/array_literal execution 14.798 ms ########
exec/for_of execution 5.599 ms #####
exec/arithmetic_loop execution 858.060 ms #####################
exec/object_construction execution 7.077 ms ######
exec/string_ops execution 2.052 ms ⚠ ###
pipeline/exec/lex frontend 0.030 ms #
pipeline/exec/parse frontend 0.028 ms #
pipeline/exec/evaluate execution 26.195 ms ##########
pipeline/closure_legacy/evaluate execution 24.706 ms ##########
pipeline/bytecode/compile frontend 0.022 ms ⚠ #
pipeline/bytecode/evaluate execution 8.974 ms #######
pipeline/parse_heavy frontend 0.516 ms #

Closure-conversion comparison

  • unavailable

… review fixups

- schedule_timer now calls cancelled_timer_ids.remove(id) before pushing
  the new timer: clearTimeout/clearInterval called with IDs not yet in the
  queue (or never valid) can no longer silently cancel the first timer to
  inherit that ID. Test: "clearTimeout with unknown id is a no-op".

- Update event_loop/timer_drain_200 benchmark comment: was still describing
  the old sort_by + remove(0) / O(n² log n) path; now documents priority-
  queue pop() / O(n log n) and notes that @lib.run includes parse+startup
  overhead (so this is a throughput regression gate, not an isolated
  queue-op microbenchmark).

- Update TIMER_DRAIN_200_SRC workloads comment to match (same stale
  O(n² log n) language removed).

Skipped: abort→fail nitpick in benchmarks.mbt — the file has a deliberate
comment at line 41 ("Uses abort() (always fatal) so it works in any error
context") and all 15+ existing bench tests follow this pattern; changing
only the two new tests would be inconsistent.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d613b17715

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// Clear cancelled IDs only when the queue is fully drained.
// If the safety limit fires, surviving timers may fire on the next
// run_timers call; keeping cancelled IDs ensures they are still skipped.
if self.host.timer_queue.length() == 0 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reclaim consumed cancellation IDs

When an interval keeps timer_queue non-empty (or the 10k safety limit is hit), this conditional means the cancellation map may never be cleared. Since consumed IDs are also left in the map when a canceled timer is skipped or a self-canceled interval is not re-enqueued, long-lived hosts that repeatedly cancel timers while another interval remains active accumulate stale IDs unboundedly; remove IDs when their cancellation is consumed, or otherwise clear known-dead entries.

Useful? React with 👍 / 👎.

@dowdiness dowdiness merged commit faad350 into main Jun 21, 2026
15 checks passed
@dowdiness dowdiness deleted the perf/timer-queue-priority-queue branch June 21, 2026 12:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Gated perf: event-loop queues to @deque / @priority_queue (promise_core)

1 participant