Repository: denoland/deno_core
Pull request #603 (2024-02-24 14:31:18)

doc(core): Update deno_core README.md and update the link to the code examples


Patch:
diff --git a/.github/workflows/ci-coverage/action.yml b/.github/workflows/ci-coverage/action.yml
new file mode 100644
index 0000000..6414a61
--- /dev/null
+++ b/.github/workflows/ci-coverage/action.yml
@@ -0,0 +1,15 @@
+name: Coverage
+runs:
+  using: "composite"
+  steps:
+    - name: Install cargo-llvm-cov
+      uses: taiki-e/install-action@cargo-llvm-cov
+    - name: Generate code coverage
+      shell: bash
+      run: |
+        cargo llvm-cov --workspace --release --all-features --bins --tests --examples --exclude deno_ops_compile_test_runner --lcov --output-path lcov.info
+    - name: Upload coverage to Codecov
+      uses: codecov/codecov-action@v3
+      with:
+        files: lcov.info
+        fail_ci_if_error: true
diff --git a/.github/workflows/ci-job.yml b/.github/workflows/ci-job.yml
index 4753ec1..358df2f 100644
--- a/.github/workflows/ci-job.yml
+++ b/.github/workflows/ci-job.yml
@@ -70,6 +70,11 @@ jobs:
         if: inputs.job == 'nop'
         uses: ./.github/workflows/ci-nop
 
+      - name: Coverage
+        id: coverage
+        if: inputs.job == 'coverage'
+        uses: ./.github/workflows/ci-coverage
+
       - name: Lint
         id: lint
         if: inputs.job == 'lint'
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 5b05262..d140180 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -56,7 +56,7 @@ jobs:
           config: |-
             linux:
               os: ubuntu-latest
-              jobs: [lint, lint-deps, test, test-miri, test-ops, test-publish]
+              jobs: [coverage, lint, lint-deps, test, test-miri, test-ops, test-publish]
             linux-arm:
               os: ubuntu-22.04-arm64
               jobs: [lint, test, test-miri]
diff --git a/Cargo.lock b/Cargo.lock
index 66128c3..12ab759 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -418,7 +418,7 @@ dependencies = [
 
 [[package]]
 name = "deno_core"
-version = "0.264.0"
+version = "0.266.0"
 dependencies = [
  "anyhow",
  "bencher",
@@ -486,7 +486,7 @@ dependencies = [
 
 [[package]]
 name = "deno_ops"
-version = "0.140.0"
+version = "0.142.0"
 dependencies = [
  "pretty_assertions",
  "prettyplease",
@@ -1478,7 +1478,7 @@ dependencies = [
 
 [[package]]
 name = "serde_v8"
-version = "0.173.0"
+version = "0.175.0"
 dependencies = [
  "bencher",
  "bytes",
diff --git a/Cargo.toml b/Cargo.toml
index bf6c69a..18b745f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -18,9 +18,9 @@ repository = "https://github.com/denoland/deno_core"
 
 [workspace.dependencies]
 # Local dependencies
-deno_core = { version = "0.264.0", path = "./core" }
-deno_ops = { version = "0.140.0", path = "./ops" }
-serde_v8 = { version = "0.173.0", path = "./serde_v8" }
+deno_core = { version = "0.266.0", path = "./core" }
+deno_ops = { version = "0.142.0", path = "./ops" }
+serde_v8 = { version = "0.175.0", path = "./serde_v8" }
 
 v8 = { version = "0.83.2", default-features = false }
 deno_ast = { version = "=0.32.0", features = ["transpiling"] }
diff --git a/core/00_infra.js b/core/00_infra.js
index 4fc40f4..944fcc0 100644
--- a/core/00_infra.js
+++ b/core/00_infra.js
@@ -33,21 +33,25 @@
   const RING_SIZE = 4 * 1024;
   const NO_PROMISE = null; // Alias to null is faster than plain nulls
   const promiseRing = ArrayPrototypeFill(new Array(RING_SIZE), NO_PROMISE);
-  // TODO(bartlomieju): it future use `v8::Private` so it's not visible
+  // TODO(bartlomieju): in the future use `v8::Private` so it's not visible
   // to users. Currently missing bindings.
   const promiseIdSymbol = SymbolFor("Deno.core.internalPromiseId");
 
-  let isOpCallTracingEnabled = false;
-  let submitOpCallTrace;
+  let isLeakTracingEnabled = false;
+  let submitLeakTrace;
   let eventLoopTick;
 
-  function __setOpCallTracingEnabled(enabled) {
-    isOpCallTracingEnabled = enabled;
+  function __setLeakTracingEnabled(enabled) {
+    isLeakTracingEnabled = enabled;
   }
 
-  function __initializeCoreMethods(eventLoopTick_, submitOpCallTrace_) {
+  function __isLeakTracingEnabled() {
+    return isLeakTracingEnabled;
+  }
+
+  function __initializeCoreMethods(eventLoopTick_, submitLeakTrace_) {
     eventLoopTick = eventLoopTick_;
-    submitOpCallTrace = submitOpCallTrace_;
+    submitLeakTrace = submitLeakTrace_;
   }
 
   const build = {
@@ -204,8 +208,8 @@
             ErrorCaptureStackTrace(err, async_op_0);
             return PromiseReject(err);
           }
-          if (isOpCallTracingEnabled) {
-            submitOpCallTrace(id);
+          if (isLeakTracingEnabled) {
+            submitLeakTrace(id);
           }
           nextPromiseId = (id + 1) & 0xffffffff;
           return setPromise(id);
@@ -223,8 +227,8 @@
             ErrorCaptureStackTrace(err, async_op_1);
             return PromiseReject(err);
           }
-          if (isOpCallTracingEnabled) {
-            submitOpCallTrace(id);
+          if (isLeakTracingEnabled) {
+            submitLeakTrace(id);
           }
           nextPromiseId = (id + 1) & 0xffffffff;
           return setPromise(id);
@@ -242,8 +246,8 @@
             ErrorCaptureStackTrace(err, async_op_2);
             return PromiseReject(err);
           }
-          if (isOpCallTracingEnabled) {
-            submitOpCallTrace(id);
+          if (isLeakTracingEnabled) {
+            submitLeakTrace(id);
           }
           nextPromiseId = (id + 1) & 0xffffffff;
           return setPromise(id);
@@ -261,8 +265,8 @@
             ErrorCaptureStackTrace(err, async_op_3);
             return PromiseReject(err);
           }
-          if (isOpCallTracingEnabled) {
-            submitOpCallTrace(id);
+          if (isLeakTracingEnabled) {
+            submitLeakTrace(id);
           }
           nextPromiseId = (id + 1) & 0xffffffff;
           return setPromise(id);
@@ -280,8 +284,8 @@
             ErrorCaptureStackTrace(err, async_op_4);
             return PromiseReject(err);
           }
-          if (isOpCallTracingEnabled) {
-            submitOpCallTrace(id);
+          if (isLeakTracingEnabled) {
+            submitLeakTrace(id);
           }
           nextPromiseId = (id + 1) & 0xffffffff;
           return setPromise(id);
@@ -299,8 +303,8 @@
             ErrorCaptureStackTrace(err, async_op_5);
             return PromiseReject(err);
           }
-          if (isOpCallTracingEnabled) {
-            submitOpCallTrace(id);
+          if (isLeakTracingEnabled) {
+            submitLeakTrace(id);
           }
           nextPromiseId = (id + 1) & 0xffffffff;
           return setPromise(id);
@@ -318,8 +322,8 @@
             ErrorCaptureStackTrace(err, async_op_6);
             return PromiseReject(err);
           }
-          if (isOpCallTracingEnabled) {
-            submitOpCallTrace(id);
+          if (isLeakTracingEnabled) {
+            submitLeakTrace(id);
           }
           nextPromiseId = (id + 1) & 0xffffffff;
           return setPromise(id);
@@ -337,8 +341,8 @@
             ErrorCaptureStackTrace(err, async_op_7);
             return PromiseReject(err);
           }
-          if (isOpCallTracingEnabled) {
-            submitOpCallTrace(id);
+          if (isLeakTracingEnabled) {
+            submitLeakTrace(id);
           }
           nextPromiseId = (id + 1) & 0xffffffff;
           return setPromise(id);
@@ -356,8 +360,8 @@
             ErrorCaptureStackTrace(err, async_op_8);
             return PromiseReject(err);
           }
-          if (isOpCallTracingEnabled) {
-            submitOpCallTrace(id);
+          if (isLeakTracingEnabled) {
+            submitLeakTrace(id);
           }
           nextPromiseId = (id + 1) & 0xffffffff;
           return setPromise(id);
@@ -375,8 +379,8 @@
             ErrorCaptureStackTrace(err, async_op_9);
             return PromiseReject(err);
           }
-          if (isOpCallTracingEnabled) {
-            submitOpCallTrace(id);
+          if (isLeakTracingEnabled) {
+            submitLeakTrace(id);
           }
           nextPromiseId = (id + 1) & 0xffffffff;
           return setPromise(id);
@@ -409,11 +413,16 @@
     setUpAsyncStub,
     hasPromise,
     promiseIdSymbol,
+  });
+
+  const infra = {
     __resolvePromise,
-    __setOpCallTracingEnabled,
+    __setLeakTracingEnabled,
+    __isLeakTracingEnabled,
     __initializeCoreMethods,
-  });
+  };
 
+  ObjectAssign(globalThis, { __infra: infra });
   ObjectAssign(globalThis.__bootstrap, { core });
   ObjectAssign(globalThis.Deno, { core });
 })(globalThis);
diff --git a/core/01_core.js b/core/01_core.js
index 6a1b8dd..b65f811 100644
--- a/core/01_core.js
+++ b/core/01_core.js
@@ -16,6 +16,7 @@
     Proxy,
     setQueueMicrotask,
     SafeMap,
+    Set,
     StringPrototypeSlice,
     Symbol,
     SymbolFor,
@@ -30,10 +31,13 @@
     hasPromise,
     promiseIdSymbol,
     registerErrorClass,
-    __setOpCallTracingEnabled,
+  } = window.Deno.core;
+  const {
+    __setLeakTracingEnabled,
+    __isLeakTracingEnabled,
     __initializeCoreMethods,
     __resolvePromise,
-  } = window.Deno.core;
+  } = window.__infra;
   const {
     op_abort_wasm_streaming,
     op_current_user_call_site,
@@ -65,14 +69,16 @@
     op_str_byte_length,
     op_timer_cancel,
     op_timer_queue,
+    op_timer_queue_system,
+    op_timer_queue_immediate,
     op_timer_ref,
     op_timer_unref,
     op_unref_op,
     op_cancel_handle,
-    op_opcall_tracing_enable,
-    op_opcall_tracing_submit,
-    op_opcall_tracing_get_all,
-    op_opcall_tracing_get,
+    op_leak_tracing_enable,
+    op_leak_tracing_submit,
+    op_leak_tracing_get_all,
+    op_leak_tracing_get,
 
     op_is_any_array_buffer,
     op_is_arguments_object,
@@ -105,22 +111,32 @@
   } = ensureFastOps();
 
   // core/infra collaborative code
-  delete window.Deno.core.__setOpCallTracingEnabled;
-  delete window.Deno.core.__initializeCoreMethods;
-  delete window.Deno.core.__resolvePromise;
+  delete window.__infra;
+
   __initializeCoreMethods(
     eventLoopTick,
-    submitOpCallTrace,
+    submitLeakTrace,
   );
 
-  function submitOpCallTrace(id) {
+  function submitLeakTrace(id) {
+    const error = new Error();
+    ErrorCaptureStackTrace(error, submitLeakTrace);
+    // "Error\n".length == 6
+    op_leak_tracing_submit(0, id, StringPrototypeSlice(error.stack, 6));
+  }
+
+  function submitTimerTrace(id) {
     const error = new Error();
-    ErrorCaptureStackTrace(error, submitOpCallTrace);
-    op_opcall_tracing_submit(id, StringPrototypeSlice(error.stack, 6));
+    ErrorCaptureStackTrace(error, submitTimerTrace);
+    // We submit interval and timer traces as type "Timer"
+    // "Error\n".length == 6
+    op_leak_tracing_submit(2, id, StringPrototypeSlice(error.stack, 6));
   }
 
   let unhandledPromiseRejectionHandler = () => false;
   let timerDepth = 0;
+  let timersRunning = false;
+  const cancelledTimers = new Set();
 
   const macrotaskCallbacks = [];
   const nextTickCallbacks = [];
@@ -176,17 +192,24 @@
 
     const timers = arguments[arguments.length - 2];
     if (timers) {
-      for (let i = 0; i < timers.length; i += 2) {
+      timersRunning = true;
+      for (let i = 0; i < timers.length; i += 3) {
         timerDepth = timers[i];
+        const id = timers[i + 1];
+        if (cancelledTimers.has(id)) {
+          continue;
+        }
         try {
-          const f = timers[i + 1];
+          const f = timers[i + 2];
           f.call(window);
         } catch (e) {
           reportExceptionCallback(e);
         }
         op_run_microtasks();
       }
+      timersRunning = false;
       timerDepth = 0;
+      cancelledTimers.clear();
     }
 
     // If we have any rejections for this tick, attempt to process them
@@ -618,17 +641,17 @@
     writeSync,
     shutdown,
     print: (msg, isErr) => op_print(msg, isErr),
-    setOpCallTracingEnabled: (enabled) => {
-      __setOpCallTracingEnabled(enabled);
-      op_opcall_tracing_enable(enabled);
+    setLeakTracingEnabled: (enabled) => {
+      __setLeakTracingEnabled(enabled);
+      op_leak_tracing_enable(enabled);
     },
-    isOpCallTracingEnabled: () => false,
-    getAllOpCallTraces: () => {
-      const traces = op_opcall_tracing_get_all();
+    isLeakTracingEnabled: () => __isLeakTracingEnabled(),
+    getAllLeakTraces: () => {
+      const traces = op_leak_tracing_get_all();
       return new SafeMap(traces);
     },
-    getOpCallTraceForPromise: (promise) =>
-      op_opcall_tracing_get(promise[promiseIdSymbol]),
+    getLeakTraceForPromise: (promise) =>
+      op_leak_tracing_get(0, promise[promiseIdSymbol]),
     setMacrotaskCallback,
     setNextTickCallback,
     runMicrotasks: () => op_run_microtasks(),
@@ -694,9 +717,23 @@
       unhandledPromiseRejectionHandler = handler,
     reportUnhandledException: (e) => op_dispatch_exception(e, false),
     reportUnhandledPromiseRejection: (e) => op_dispatch_exception(e, true),
-    queueTimer: (depth, repeat, timeout, task) =>
-      op_timer_queue(depth, repeat, timeout, task),
-    cancelTimer: (id) => op_timer_cancel(id),
+    queueUserTimer: (depth, repeat, timeout, task) => {
+      const id = op_timer_queue(depth, repeat, timeout, task);
+      if (__isLeakTracingEnabled()) {
+        submitTimerTrace(id);
+      }
+      return id;
+    },
+    // TODO(mmastrac): Hook up associatedOp to tracing
+    queueSystemTimer: (_associatedOp, repeat, timeout, task) =>
+      op_timer_queue_system(repeat, timeout, task),
+    queueImmediate: (task) => op_timer_queue_immediate(task),
+    cancelTimer: (id) => {
+      if (timersRunning) {
+        cancelledTimers.add(id);
+      }
+      op_timer_cancel(id);
+    },
     refTimer: (id) => op_timer_ref(id),
     unrefTimer: (id) => op_timer_unref(id),
     getTimerDepth: () => timerDepth,
diff --git a/core/Cargo.toml b/core/Cargo.toml
index caff0c0..3e5d4dc 100644
--- a/core/Cargo.toml
+++ b/core/Cargo.toml
@@ -2,7 +2,7 @@
 
 [package]
 name = "deno_core"
-version = "0.264.0"
+version = "0.266.0"
 authors.workspace = true
 edition.workspace = true
 license.workspace = true
diff --git a/core/README.md b/core/README.md
index 06f395c..180d307 100644
--- a/core/README.md
+++ b/core/README.md
@@ -16,15 +16,13 @@ keeps track of all pending tasks (async ops, dynamic module loads). It is user's
 responsibility to drive that loop by using `JsRuntime::run_event_loop` method -
 it must be executed in the context of Rust's future executor (eg. tokio, smol).
 
-Rust functions can be registered in JavaScript using `deno_core::Extension`. A
-conventional way to write ops is using the
-[`deno_ops`](https://github.com/denoland/deno/blob/main/ops) crate.
+Rust functions can be registered in JavaScript using the `deno_core::extension!`
+macro. A conventional way to write ops is using the
+[`deno_ops`](../ops/README.md) crate.
 
 Documentation for this crate is thin at the moment. Please see
-[hello_world.rs](https://github.com/denoland/deno_core/blob/main/core/examples/hello_world.rs)
-and
-[http_bench_json_ops/main.rs](https://github.com/denoland/deno_core/blob/main/core/examples/http_bench_json_ops/main.rs)
-as examples of usage.
+[hello_world.rs](./examples/hello_world.rs) and
+[eval_js_value.rs](./examples/eval_js_value.rs) as examples of usage.
 
 TypeScript support and lots of other functionality are not available at this
 layer. See the [CLI](https://github.com/denoland/deno/tree/main/cli) for that.
diff --git a/core/lib.deno_core.d.ts b/core/lib.deno_core.d.ts
index 7252002..b61e1ec 100644
--- a/core/lib.deno_core.d.ts
+++ b/core/lib.deno_core.d.ts
@@ -19,28 +19,28 @@ declare namespace Deno {
     function unrefOpPromise<T>(promise: Promise<T>): void;
 
     /**
-     * Enables collection of stack traces of all async ops. This allows for
+     * Enables collection of stack traces for sanitizers. This allows for
      * debugging of where a given async op was started. Deno CLI uses this for
-     * improving error message in op sanitizer errors for `deno test`.
+     * improving error message in sanitizer errors for `deno test`.
      *
      * **NOTE:** enabling tracing has a significant negative performance impact.
      */
-    function setOpCallTracingEnabled(enabled: boolean);
+    function setLeakTracingEnabled(enabled: boolean);
 
-    function isOpCallTracingEnabled(): boolean;
+    function isLeakTracingEnabled(): boolean;
 
     /**
      * Returns the origin stack trace of the given async op promise. The promise
      * must be ongoing.
      */
-    function getOpCallTraceForPromise<T>(promise: Promise<T>): string | null;
+    function getLeakTraceForPromise<T>(promise: Promise<T>): string | null;
 
     /**
      * Returns a map containing traces for all ongoing async ops. The key is the promise id.
-     * Tracing only occurs when `Deno.core.setOpCallTracingEnabled()` was previously
+     * Tracing only occurs when `Deno.core.setLeakTracingEnabled()` was previously
      * enabled.
      */
-    function getAllOpCallTraces(): Map<number, string>;
+    function getAllLeakTraces(): Map<number, string>;
 
     /**
      * List of all registered ops, in the form of a map that maps op
@@ -142,14 +142,37 @@ declare namespace Deno {
     /** Set a value telling the runtime if there are "next ticks" scheduled */
     function setHasNextTickScheduled(value: boolean): void;
 
-    /** Enqueue a timer at the given depth, optionally repeating. */
-    function queueTimer(
+    /** Enqueue an immediate callback. Immediate callbacks always execute in
+     * the next timer phase.
+     */
+    function queueImmediate(
+      depth: number,
+      repeat: boolean,
+      delay: number,
+      callback: () => void,
+    ): number;
+
+    /** Enqueue a user timer at the given depth, optionally repeating. User
+     * timers may generate call traces for sanitization, and may be clamped
+     * depending on the depth of nesting. */
+    function queueUserTimer(
       depth: number,
       repeat: boolean,
       delay: number,
       callback: () => void,
     ): number;
 
+    /** Enqueue a system timer at the given depth, optionally repeating. System
+     * timers do not generate call traces, and are never clamped at any nesting
+     * depth. System timers are also associated with an op to provide contextual
+     * information. */
+    function queueSystemTimer(
+      associatedOp: Function,
+      repeat: boolean,
+      delay: number,
+      callback: () => void,
+    ): number;
+
     /** Cancel a timer with a given ID. */
     function cancelTimer(id: number);
 
diff --git a/core/ops_builtin.rs b/core/ops_builtin.rs
index 74a0de8..9e7ca27 100644
--- a/core/ops_builtin.rs
+++ b/core/ops_builtin.rs
@@ -88,6 +88,8 @@ builtin_ops! {
   ops_builtin_types::op_is_weak_set,
   ops_builtin_v8::op_set_handled_promise_rejection_handler,
   ops_builtin_v8::op_timer_queue,
+  ops_builtin_v8::op_timer_queue_system,
+  ops_builtin_v8::op_timer_queue_immediate,
   ops_builtin_v8::op_timer_cancel,
   ops_builtin_v8::op_timer_ref,
   ops_builtin_v8::op_timer_unref,
@@ -120,10 +122,10 @@ builtin_ops! {
   ops_builtin_v8::op_set_format_exception_callback,
   ops_builtin_v8::op_event_loop_has_more_work,
   ops_builtin_v8::op_arraybuffer_was_detached,
-  ops_builtin_v8::op_opcall_tracing_enable,
-  ops_builtin_v8::op_opcall_tracing_submit,
-  ops_builtin_v8::op_opcall_tracing_get_all,
-  ops_builtin_v8::op_opcall_tracing_get
+  ops_builtin_v8::op_leak_tracing_enable,
+  ops_builtin_v8::op_leak_tracing_submit,
+  ops_builtin_v8::op_leak_tracing_get_all,
+  ops_builtin_v8::op_leak_tracing_get
 }
 
 #[op2(fast)]
diff --git a/core/ops_builtin_v8.rs b/core/ops_builtin_v8.rs
index 8f83b40..795a6e6 100644
--- a/core/ops_builtin_v8.rs
+++ b/core/ops_builtin_v8.rs
@@ -11,10 +11,10 @@ use crate::runtime::script_origin;
 use crate::runtime::JsRealm;
 use crate::runtime::JsRuntimeState;
 use crate::source_map::SourceMapApplication;
+use crate::stats::RuntimeActivityType;
 use crate::JsBuffer;
 use crate::JsRuntime;
 use crate::OpState;
-use crate::PromiseId;
 use anyhow::Error;
 use serde::Deserialize;
 use serde::Serialize;
@@ -45,32 +45,37 @@ pub fn op_unref_op(scope: &mut v8::HandleScope, promise_id: i32) {
 }
 
 #[op2]
-pub fn op_opcall_tracing_enable(scope: &mut v8::HandleScope, enabled: bool) {
+pub fn op_leak_tracing_enable(scope: &mut v8::HandleScope, enabled: bool) {
   let context_state = JsRealm::state_from_scope(scope);
-  context_state.opcall_traces.set_enabled(enabled);
+  context_state.activity_traces.set_enabled(enabled);
 }
 
 #[op2]
-pub fn op_opcall_tracing_submit(
+pub fn op_leak_tracing_submit(
   scope: &mut v8::HandleScope,
-  #[smi] promise: PromiseId,
+  #[smi] kind: u8,
+  #[smi] id: i32,
   #[string] trace: &str,
 ) {
   let context_state = JsRealm::state_from_scope(scope);
-  context_state.opcall_traces.submit(promise, trace);
+  context_state.activity_traces.submit(
+    RuntimeActivityType::from_u8(kind),
+    id as _,
+    trace,
+  );
 }
 
 #[op2]
 #[serde]
-pub fn op_opcall_tracing_get_all<'s>(
+pub fn op_leak_tracing_get_all<'s>(
   scope: &mut v8::HandleScope<'s>,
 ) -> Vec<serde_v8::Value<'s>> {
   let context_state = JsRealm::state_from_scope(scope);
-  // This is relatively inefficient, but so is opcall tracing
-  let mut out = Vec::with_capacity(context_state.opcall_traces.count());
-  context_state.opcall_traces.get_all(|promise, trace| {
+  // This is relatively inefficient, but so is leak tracing
+  let mut out = Vec::with_capacity(context_state.activity_traces.count());
+  context_state.activity_traces.get_all(|kind, id, trace| {
     out.push(
-      serde_v8::to_v8(scope, (promise.to_string(), trace.to_owned()))
+      serde_v8::to_v8(scope, (kind as u8, id.to_string(), trace.to_owned()))
         .unwrap()
         .into(),
     );
@@ -79,19 +84,23 @@ pub fn op_opcall_tracing_get_all<'s>(
 }
 
 #[op2]
-pub fn op_opcall_tracing_get<'s>(
+pub fn op_leak_tracing_get<'s>(
   scope: &mut v8::HandleScope<'s>,
-  #[smi] promise: PromiseId,
+  #[smi] kind: u8,
+  #[smi] id: i32,
 ) -> v8::Local<'s, v8::Value> {
   use serde_v8::Serializable;
   let context_state = JsRealm::state_from_scope(scope);
-  context_state
-    .opcall_traces
-    .get(promise, |mut x| x.to_v8(scope).unwrap())
+  context_state.activity_traces.get(
+    RuntimeActivityType::from_u8(kind),
+    id as _,
+    |mut x| x.to_v8(scope).unwrap(),
+  )
 }
 
-/// Queue a timer, returning a "large" integer in an f64 (allowing up to `MAX_SAFE_INTEGER`
-/// timers to exist).
+/// Queue a timer. We return a "large integer" timer ID in an f64 which allows for up
+/// to `MAX_SAFE_INTEGER` (2^53) timers to exist, versus 2^32 timers if we used
+/// `u32`.
 #[op2]
 pub fn op_timer_queue(
   scope: &mut v8::HandleScope,
@@ -112,10 +121,45 @@ pub fn op_timer_queue(
   }
 }
 
+/// Queue a timer. We return a "large integer" timer ID in an f64 which allows for up
+/// to `MAX_SAFE_INTEGER` (2^53) timers to exist, versus 2^32 timers if we used
+/// `u32`.
+#[op2]
+pub fn op_timer_queue_system(
+  scope: &mut v8::HandleScope,
+  repeat: bool,
+  timeout_ms: f64,
+  #[global] task: v8::Global<v8::Function>,
+) -> f64 {
+  let context_state = JsRealm::state_from_scope(scope);
+  if repeat {
+    context_state
+      .timers
+      .queue_timer_repeat(timeout_ms as _, (task, 0)) as _
+  } else {
+    context_state.timers.queue_timer(timeout_ms as _, (task, 0)) as _
+  }
+}
+
+/// Queue a timer. We return a "large integer" timer ID in an f64 which allows for up
+/// to `MAX_SAFE_INTEGER` (2^53) timers to exist, versus 2^32 timers if we used
+/// `u32`.
+#[op2]
+pub fn op_timer_queue_immediate(
+  scope: &mut v8::HandleScope,
+  #[global] task: v8::Global<v8::Function>,
+) -> f64 {
+  let context_state = JsRealm::state_from_scope(scope);
+  context_state.timers.queue_timer(0, (task, 0)) as _
+}
+
 #[op2]
 pub fn op_timer_cancel(scope: &mut v8::HandleScope, id: f64) {
   let context_state = JsRealm::state_from_scope(scope);
   context_state.timers.cancel_timer(id as _);
+  context_state
+    .activity_traces
+    .complete(RuntimeActivityType::Timer, id as _);
 }
 
 #[op2]
diff --git a/core/rebuild_async_stubs.js b/core/rebuild_async_stubs.js
index 37b8bad..aaafc44 100755
--- a/core/rebuild_async_stubs.js
+++ b/core/rebuild_async_stubs.js
@@ -17,8 +17,8 @@ function __TEMPLATE__(__ARGS_PARAM__) {
     ErrorCaptureStackTrace(err, __TEMPLATE__);
     return PromiseReject(err);
   }
-  if (isOpCallTracingEnabled) {
-    submitOpCallTrace(id);
+  if (isLeakTracingEnabled) {
+    submitLeakTrace(id);
   }
   nextPromiseId = (id + 1) & 0xffffffff;
   return setPromise(id);
diff --git a/core/runtime/bindings.rs b/core/runtime/bindings.rs
index 32ef479..672f1dc 100644
--- a/core/runtime/bindings.rs
+++ b/core/runtime/bindings.rs
@@ -58,17 +58,19 @@ pub(crate) fn create_external_references(
     pointer: syn_module_eval_fn as *mut c_void,
   });
 
-  // Using v8::OneByteConst and passing external references to the source code
+  // Using v8::OneByteConst and passing external references to it
   // allows V8 to take an optimized path when deserializing the snapshot.
   for source_file in &CONTEXT_SETUP_SOURCES {
     references.push(v8::ExternalReference {
-      pointer: source_file.source.as_ptr() as *mut c_void,
+      pointer: source_file.source_onebyte_const as *const v8::OneByteConst
+        as *mut c_void,
     });
   }
 
   for source_file in &BUILTIN_SOURCES {
     references.push(v8::ExternalReference {
-      pointer: source_file.source.as_ptr() as *mut c_void,
+      pointer: source_file.source_onebyte_const as *const v8::OneByteConst
+        as *mut c_void,
     });
   }
 
@@ -164,35 +166,31 @@ pub mod v8_static_strings {
   }
 
   macro_rules! onebyte_const {
-    ($str_:literal) => {
-      v8::String::create_external_onebyte_const($str_.as_bytes())
+    ($ident:ident, $str_:literal) => {
+      pub static $ident: v8::OneByteConst =
+        v8::String::create_external_onebyte_const($str_.as_bytes());
     };
   }
 
-  pub static DENO: v8::OneByteConst = onebyte_const!("Deno");
-  pub static CORE: v8::OneByteConst = onebyte_const!("core");
-  pub static OPS: v8::OneByteConst = onebyte_const!("ops");
-  pub static URL: v8::OneByteConst = onebyte_const!("url");
-  pub static MAIN: v8::OneByteConst = onebyte_const!("main");
-  pub static RESOLVE: v8::OneByteConst = onebyte_const!("resolve");
-  pub static MESSAGE: v8::OneByteConst = onebyte_const!("message");
-  pub static CODE: v8::OneByteConst = onebyte_const!("code");
-  pub static ERR_MODULE_NOT_FOUND: v8::OneByteConst =
-    onebyte_const!("ERR_MODULE_NOT_FOUND");
-  pub static EVENT_LOOP_TICK: v8::OneByteConst =
-    onebyte_const!("eventLoopTick");
-  pub static BUILD_CUSTOM_ERROR: v8::OneByteConst =
-    onebyte_const!("buildCustomError");
-  pub static CONSOLE: v8::OneByteConst = onebyte_const!("console");
-  pub static CALL_CONSOLE: v8::OneByteConst = onebyte_const!("callConsole");
-  pub static FILENAME: v8::OneByteConst = onebyte_const!("filename");
-  pub static DIRNAME: v8::OneByteConst = onebyte_const!("dirname");
-  pub static SET_UP_ASYNC_STUB: v8::OneByteConst =
-    onebyte_const!("setUpAsyncStub");
-  pub static WEBASSEMBLY: v8::OneByteConst = onebyte_const!("WebAssembly");
-  pub static INSTANTIATE: v8::OneByteConst = onebyte_const!("instantiate");
-  pub static WASM_INSTANTIATE: v8::OneByteConst =
-    onebyte_const!("wasmInstantiate");
+  onebyte_const!(DENO, "Deno");
+  onebyte_const!(CORE, "core");
+  onebyte_const!(OPS, "ops");
+  onebyte_const!(URL, "url");
+  onebyte_const!(MAIN, "main");
+  onebyte_const!(RESOLVE, "resolve");
+  onebyte_const!(MESSAGE, "message");
+  onebyte_const!(CODE, "code");
+  onebyte_const!(ERR_MODULE_NOT_FOUND, "ERR_MODULE_NOT_FOUND");
+  onebyte_const!(EVENT_LOOP_TICK, "eventLoopTick");
+  onebyte_const!(BUILD_CUSTOM_ERROR, "buildCustomError");
+  onebyte_const!(CONSOLE, "console");
+  onebyte_const!(CALL_CONSOLE, "callConsole");
+  onebyte_const!(FILENAME, "filename");
+  onebyte_const!(DIRNAME, "dirname");
+  onebyte_const!(SET_UP_ASYNC_STUB, "setUpAsyncStub");
+  onebyte_const!(WEBASSEMBLY, "WebAssembly");
+  onebyte_const!(INSTANTIATE, "instantiate");
+  onebyte_const!(WASM_INSTANTIATE, "wasmInstantiate");
 }
 
 /// Create an object on the `globalThis` that looks like this:
@@ -269,12 +267,12 @@ pub(crate) fn initialize_primordials_and_infra(
   for source_file in &CONTEXT_SETUP_SOURCES {
     let name = v8::String::new_from_onebyte_const(
       scope,
-      &source_file.specifier_onebyte_const,
+      source_file.specifier_onebyte_const,
     )
     .unwrap();
     let source_str = v8::String::new_from_onebyte_const(
       scope,
-      &source_file.source_onebyte_const,
+      source_file.source_onebyte_const,
     )
     .unwrap();
 
diff --git a/core/runtime/exception_state.rs b/core/runtime/exception_state.rs
index 782fc1b..1acbad0 100644
--- a/core/runtime/exception_state.rs
+++ b/core/runtime/exception_state.rs
@@ -144,6 +144,8 @@ impl ExceptionState {
         let previous_len = rejections.len();
         rejections.retain(|(key, _)| key != &promise_global);
         if rejections.len() == previous_len {
+          // Don't hold the lock while we go back into v8
+          drop(rejections);
           // The unhandled rejection was already delivered, so this means we need to deliver a
           // "rejectionhandled" event if anyone cares.
           if self.js_handled_promise_rejection_cb.borrow().is_some() {
diff --git a/core/runtime/jsrealm.rs b/core/runtime/jsrealm.rs
index d8448d6..1f5693e 100644
--- a/core/runtime/jsrealm.rs
+++ b/core/runtime/jsrealm.rs
@@ -9,7 +9,7 @@ use crate::modules::ModuleCodeString;
 use crate::modules::ModuleId;
 use crate::modules::ModuleMap;
 use crate::ops::OpCtx;
-use crate::stats::OpCallTraces;
+use crate::stats::RuntimeActivityTraces;
 use crate::tasks::V8TaskSpawnerFactory;
 use crate::web_timeout::WebTimers;
 use crate::GetErrorClassFn;
@@ -56,7 +56,7 @@ pub(crate) struct ContextState {
   pub(crate) wasm_instantiate_fn: RefCell<Option<Rc<v8::Global<v8::Function>>>>,
   pub(crate) unrefed_ops:
     RefCell<HashSet<i32, BuildHasherDefault<IdentityHasher>>>,
-  pub(crate) opcall_traces: OpCallTraces,
+  pub(crate) activity_traces: RuntimeActivityTraces,
   pub(crate) pending_ops: Rc<OpDriverImpl>,
   // We don't explicitly re-read this prop but need the slice to live alongside
   // the context
@@ -82,7 +82,7 @@ impl ContextState {
       js_event_loop_tick_cb: Default::default(),
       js_wasm_streaming_cb: Default::default(),
       wasm_instantiate_fn: Default::default(),
-      opcall_traces: Default::default(),
+      activity_traces: Default::default(),
       op_ctxs,
       pending_ops: op_driver,
       task_spawner_factory: Default::default(),
diff --git a/core/runtime/jsruntime.rs b/core/runtime/jsruntime.rs
index 4e1617e..ccf73b5 100644
--- a/core/runtime/jsruntime.rs
+++ b/core/runtime/jsruntime.rs
@@ -38,6 +38,7 @@ use crate::runtime::JsRealm;
 use crate::runtime::OpDriverImpl;
 use crate::source_map::SourceMapGetter;
 use crate::source_map::SourceMapper;
+use crate::stats::RuntimeActivityType;
 use crate::Extension;
 use crate::ExtensionFileSource;
 use crate::ExtensionFileSourceCode;
@@ -59,6 +60,7 @@ use std::any::Any;
 use std::cell::Cell;
 use std::cell::RefCell;
 use std::collections::HashMap;
+use std::collections::VecDeque;
 use std::ffi::c_void;
 use std::mem::ManuallyDrop;
 use std::ops::Deref;
@@ -260,23 +262,23 @@ const VIRTUAL_OPS_MODULE_NAME: &str = "ext:core/ops";
 
 pub(crate) struct InternalSourceFile {
   pub specifier: &'static str,
-  pub specifier_onebyte_const: v8::OneByteConst,
-  pub source: &'static [u8],
-  pub source_onebyte_const: v8::OneByteConst,
+  pub specifier_onebyte_const: &'static v8::OneByteConst,
+  pub source_onebyte_const: &'static v8::OneByteConst,
 }
 
 macro_rules! internal_source_file {
   ($str_:literal) => {{
-    let specifier = concat!("ext:core/", $str_);
-    let source = include_bytes!(concat!("../", $str_));
+    static SPECIFIER: &str = concat!("ext:core/", $str_);
+    static SOURCE: &[u8] = include_bytes!(concat!("../", $str_));
 
+    static SPECIFIER_OBC: v8::OneByteConst =
+      v8::String::create_external_onebyte_const(SPECIFIER.as_bytes());
+    static SOURCE_OBC: v8::OneByteConst =
+      v8::String::create_external_onebyte_const(SOURCE);
     InternalSourceFile {
-      specifier,
-      specifier_onebyte_const: v8::String::create_external_onebyte_const(
-        specifier.as_bytes(),
-      ),
-      source,
-      source_onebyte_const: v8::String::create_external_onebyte_const(source),
+      specifier: SPECIFIER,
+      specifier_onebyte_const: &SPECIFIER_OBC,
+      source_onebyte_const: &SOURCE_OBC,
     }
   }};
 }
@@ -1029,12 +1031,12 @@ impl JsRuntime {
     for source_file in &BUILTIN_SOURCES {
       let name = v8::String::new_from_onebyte_const(
         scope,
-        &source_file.specifier_onebyte_const,
+        source_file.specifier_onebyte_const,
       )
       .unwrap();
       let source_str = v8::String::new_from_onebyte_const(
         scope,
-        &source_file.source_onebyte_const,
+        source_file.source_onebyte_const,
       )
       .unwrap();
 
@@ -2212,7 +2214,9 @@ impl JsRuntime {
       }
 
       context_state.unrefed_ops.borrow_mut().remove(&promise_id);
-      context_state.opcall_traces.complete(promise_id);
+      context_state
+        .activity_traces
+        .complete(RuntimeActivityType::AsyncOp, promise_id as _);
       dispatched_ops |= true;
       args.push(v8::Integer::new(scope, promise_id).into());
       args.push(res.unwrap_or_else(std::convert::identity));
@@ -2247,11 +2251,16 @@ impl JsRuntime {
       .borrow_mut()
       .is_empty()
     {
-      let mut rejections =
+      // Avoid holding the pending rejection lock longer than necessary
+      let mut pending_rejections =
         exception_state.pending_promise_rejections.borrow_mut();
+      let mut rejections = VecDeque::default();
+      std::mem::swap(&mut *pending_rejections, &mut rejections);
+      drop(pending_rejections);
+
       let arr = v8::Array::new(scope, (rejections.len() * 2) as i32);
       let mut index = 0;
-      for rejection in rejections.drain(..) {
+      for rejection in rejections.into_iter() {
         let value = v8::Local::new(scope, rejection.0);
         arr.set_index(scope, index, value.into());
         index += 1;
@@ -2259,7 +2268,6 @@ impl JsRuntime {
         arr.set_index(scope, index, value);
         index += 1;
       }
-      drop(rejections);
       arr.into()
     } else {
       undefined
@@ -2267,15 +2275,27 @@ impl JsRuntime {
 
     args.push(rejections);
 
+    // TODO(mmastrac): timer dispatch should be done via direct function call, but we will have to start
+    // storing the exception-reporting callback.
     let timers =
       if let Poll::Ready(timers) = context_state.timers.poll_timers(cx) {
-        let arr = v8::Array::new(scope, (timers.len() * 2) as _);
+        let traces_enabled = context_state.activity_traces.is_enabled();
+        let arr = v8::Array::new(scope, (timers.len() * 3) as _);
         #[allow(clippy::needless_range_loop)]
         for i in 0..timers.len() {
+          if traces_enabled {
+            // Timer and interval traces both use RuntimeActivityType::Timer
+            context_state
+              .activity_traces
+              .complete(RuntimeActivityType::Timer, timers[i].0 as _);
+          }
+          // depth, id, function
           let value = v8::Integer::new(scope, timers[i].1 .1 as _);
-          arr.set_index(scope, (i * 2) as _, value.into());
+          arr.set_index(scope, (i * 3) as _, value.into());
+          let value = v8::Number::new(scope, timers[i].0 as _);
+          arr.set_index(scope, (i * 3 + 1) as _, value.into());
           let value = v8::Local::new(scope, timers[i].1 .0.clone());
-          arr.set_index(scope, (i * 2 + 1) as _, value.into());
+          arr.set_index(scope, (i * 3 + 2) as _, value.into());
         }
         arr.into()
       } else {
diff --git a/core/runtime/stats.rs b/core/runtime/stats.rs
index e2a1eec..cb985ae 100644
--- a/core/runtime/stats.rs
+++ b/core/runtime/stats.rs
@@ -11,31 +11,58 @@ use serde::Serialize;
 use serde::Serializer;
 use std::cell::Cell;
 use std::cell::RefCell;
-use std::collections::HashMap;
+use std::collections::BTreeMap;
 use std::fmt::Display;
 use std::ops::Deref;
 use std::rc::Rc;
 
+type ActivityId = usize;
+
+/// Fast, const no-trace collection of hashes.
+const NO_TRACES: [BTreeMap<ActivityId, Rc<str>>;
+  RuntimeActivityType::MAX_TYPE as usize] = [
+  BTreeMap::new(),
+  BTreeMap::new(),
+  BTreeMap::new(),
+  BTreeMap::new(),
+];
+
 #[derive(Default)]
-pub struct OpCallTraces {
+pub struct RuntimeActivityTraces {
   enabled: Cell<bool>,
-  traces: RefCell<HashMap<PromiseId, Rc<str>>>,
+  traces: RefCell<
+    [BTreeMap<ActivityId, Rc<str>>; RuntimeActivityType::MAX_TYPE as usize],
+  >,
 }
 
-impl OpCallTraces {
+impl RuntimeActivityTraces {
   pub(crate) fn set_enabled(&self, enabled: bool) {
     self.enabled.set(enabled);
     if !enabled {
-      self.traces.borrow_mut().clear();
+      *self.traces.borrow_mut() = Default::default();
     }
   }
 
-  pub(crate) fn submit(&self, promise_id: PromiseId, trace: &str) {
-    self.traces.borrow_mut().insert(promise_id, trace.into());
+  pub(crate) fn submit(
+    &self,
+    activity_type: RuntimeActivityType,
+    id: ActivityId,
+    trace: &str,
+  ) {
+    debug_assert_ne!(
+      activity_type,
+      RuntimeActivityType::Interval,
+      "Use Timer for for timers and intervals"
+    );
+    self.traces.borrow_mut()[activity_type as usize].insert(id, trace.into());
   }
 
-  pub(crate) fn complete(&self, promise_id: PromiseId) {
-    self.traces.borrow_mut().remove(&promise_id);
+  pub(crate) fn complete(
+    &self,
+    activity_type: RuntimeActivityType,
+    id: ActivityId,
+  ) {
+    self.traces.borrow_mut()[activity_type as usize].remove(&id);
   }
 
   pub fn is_enabled(&self) -> bool {
@@ -46,22 +73,38 @@ impl OpCallTraces {
     self.traces.borrow().len()
   }
 
-  pub fn get_all(&self, mut f: impl FnMut(PromiseId, &str)) {
-    for (key, value) in self.traces.borrow().iter() {
-      f(*key, value.as_ref())
+  pub fn get_all(
+    &self,
+    mut f: impl FnMut(RuntimeActivityType, ActivityId, &str),
+  ) {
+    let traces = self.traces.borrow();
+    for i in 0..RuntimeActivityType::MAX_TYPE {
+      for (key, value) in traces[i as usize].iter() {
+        f(RuntimeActivityType::from_u8(i), *key, value.as_ref())
+      }
     }
   }
 
-  pub fn capture(&self) -> HashMap<PromiseId, Rc<str>> {
-    self.traces.borrow().clone()
+  pub fn capture(
+    &self,
+  ) -> [BTreeMap<ActivityId, Rc<str>>; RuntimeActivityType::MAX_TYPE as usize]
+  {
+    if self.is_enabled() {
+      self.traces.borrow().clone()
+    } else {
+      NO_TRACES
+    }
   }
 
   pub fn get<T>(
     &self,
-    promise_id: PromiseId,
+    activity_type: RuntimeActivityType,
+    id: ActivityId,
     f: impl FnOnce(Option<&str>) -> T,
   ) -> T {
-    f(self.traces.borrow().get(&promise_id).map(|x| x.as_ref()))
+    f(self.traces.borrow()[activity_type as u8 as usize]
+      .get(&id)
+      .map(|x| x.as_ref()))
   }
 }
 
@@ -160,18 +203,18 @@ impl RuntimeActivityStatsFactory {
       TimerStats::default()
     };
 
-    let (ops, opcall_traces) = if filter.include_ops {
+    let (ops, activity_traces) = if filter.include_ops {
       let ops = self.context_state.pending_ops.stats(&filter.op_filter);
-      let opcall_traces = self.context_state.opcall_traces.capture();
-      (ops, opcall_traces)
+      let activity_traces = self.context_state.activity_traces.capture();
+      (ops, activity_traces)
     } else {
-      (OpInflightStats::default(), HashMap::default())
+      (Default::default(), Default::default())
     };
 
     RuntimeActivityStats {
       context_state: self.context_state.clone(),
       ops,
-      opcall_traces,
+      activity_traces,
       resources,
       timers,
     }
@@ -196,17 +239,17 @@ pub struct TimerStats {
 pub struct RuntimeActivityStats {
   context_state: Rc<ContextState>,
   pub(super) ops: OpInflightStats,
-  pub(super) opcall_traces: HashMap<PromiseId, Rc<str>>,
+  pub(super) activity_traces: [BTreeMap<ActivityId, Rc<str>>; 4],
   pub(super) resources: ResourceOpenStats,
   pub(super) timers: TimerStats,
 }
 
-/// Contains an opcall stack trace.
+/// Contains a runtime activity (op, timer, resource, etc.) stack trace.
 #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
 #[repr(transparent)]
-pub struct OpCallTrace(Rc<str>);
+pub struct RuntimeActivityTrace(Rc<str>);
 
-impl Serialize for OpCallTrace {
+impl Serialize for RuntimeActivityTrace {
   fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
   where
     S: Serializer,
@@ -215,20 +258,20 @@ impl Serialize for OpCallTrace {
   }
 }
 
-impl Display for OpCallTrace {
+impl Display for RuntimeActivityTrace {
   fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
     f.write_str(self.0.as_ref())
   }
 }
 
-impl Deref for OpCallTrace {
+impl Deref for RuntimeActivityTrace {
   type Target = str;
   fn deref(&self) -> &Self::Target {
     self.0.as_ref()
   }
 }
 
-impl From<&Rc<str>> for OpCallTrace {
+impl From<&Rc<str>> for RuntimeActivityTrace {
   fn from(value: &Rc<str>) -> Self {
     Self(value.clone())
   }
@@ -237,14 +280,14 @@ impl From<&Rc<str>> for OpCallTrace {
 /// The type of runtime activity being tracked.
 #[derive(Debug, Serialize)]
 pub enum RuntimeActivity {
-  /// An async op, including the promise ID and op name, with an optional opcall trace.
-  AsyncOp(PromiseId, &'static str, Option<OpCallTrace>),
-  /// A resource, including the resource ID and name.
-  Resource(ResourceId, String),
-  /// A timer, including the timer ID.
-  Timer(usize),
-  /// An interval, including the interval ID.
-  Interval(usize),
+  /// An async op, including the promise ID and op name, with an optional trace.
+  AsyncOp(PromiseId, Option<RuntimeActivityTrace>, &'static str),
+  /// A resource, including the resource ID and name, with an optional trace.
+  Resource(ResourceId, Option<RuntimeActivityTrace>, String),
+  /// A timer, including the timer ID, with an optional trace.
+  Timer(usize, Option<RuntimeActivityTrace>),
+  /// An interval, including the interval ID, with an optional trace.
+  Interval(usize, Option<RuntimeActivityTrace>),
 }
 
 impl RuntimeActivity {
@@ -262,6 +305,7 @@ impl RuntimeActivity {
 #[derive(
   Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize,
 )]
+#[repr(u8)]
 pub enum RuntimeActivityType {
   AsyncOp,
   Resource,
@@ -269,11 +313,40 @@ pub enum RuntimeActivityType {
   Interval,
 }
 
+impl RuntimeActivityType {
+  const MAX_TYPE: u8 = 4;
+
+  pub(crate) fn from_u8(value: u8) -> Self {
+    match value {
+      0 => Self::AsyncOp,
+      1 => Self::Resource,
+      2 => Self::Timer,
+      3 => Self::Interval,
+      _ => unreachable!(),
+    }
+  }
+}
+
 impl RuntimeActivityStats {
+  fn trace_for(
+    &self,
+    activity_type: RuntimeActivityType,
+    id: ActivityId,
+  ) -> Option<RuntimeActivityTrace> {
+    debug_assert_ne!(
+      activity_type,
+      RuntimeActivityType::Interval,
+      "Use Timer for for timers and intervals"
+    );
+    self.activity_traces[activity_type as u8 as usize]
+      .get(&id)
+      .map(|x| x.into())
+  }
+
   /// Capture the data within this [`RuntimeActivityStats`] as a [`RuntimeActivitySnapshot`]
   /// with details of activity.
   pub fn dump(&self) -> RuntimeActivitySnapshot {
-    let has_traces = !self.opcall_traces.is_empty();
+    let has_traces = !self.activity_traces.is_empty();
     let mut v = Vec::with_capacity(
       self.ops.ops.len()
         + self.resources.resources.len()
@@ -284,27 +357,48 @@ impl RuntimeActivityStats {
       for op in self.ops.ops.iter() {
         v.push(RuntimeActivity::AsyncOp(
           op.0,
+          self.trace_for(RuntimeActivityType::AsyncOp, op.0 as _),
           ops[op.1 as usize].decl.name,
-          self.opcall_traces.get(&op.0).map(|x| x.into()),
         ));
       }
     } else {
       for op in self.ops.ops.iter() {
         v.push(RuntimeActivity::AsyncOp(
           op.0,
-          ops[op.1 as usize].decl.name,
           None,
+          ops[op.1 as usize].decl.name,
         ));
       }
     }
     for resource in self.resources.resources.iter() {
-      v.push(RuntimeActivity::Resource(resource.0, resource.1.clone()))
+      v.push(RuntimeActivity::Resource(
+        resource.0,
+        None,
+        resource.1.clone(),
+      ))
     }
-    for i in 0..self.timers.timers.len() {
-      if self.timers.repeats.contains(i) {
-        v.push(RuntimeActivity::Interval(self.timers.timers[i]));
-      } else {
-        v.push(RuntimeActivity::Timer(self.timers.timers[i]));
+    if has_traces {
+      for i in 0..self.timers.timers.len() {
+        let id = self.timers.timers[i];
+        if self.timers.repeats.contains(i) {
+          v.push(RuntimeActivity::Interval(
+            id,
+            self.trace_for(RuntimeActivityType::Timer, id),
+          ));
+        } else {
+          v.push(RuntimeActivity::Timer(
+            id,
+            self.trace_for(RuntimeActivityType::Timer, id),
+          ));
+        }
+      }
+    } else {
+      for i in 0..self.timers.timers.len() {
+        if self.timers.repeats.contains(i) {
+          v.push(RuntimeActivity::Interval(self.timers.timers[i], None));
+        } else {
+          v.push(RuntimeActivity::Timer(self.timers.timers[i], None));
+        }
       }
     }
     RuntimeActivitySnapshot { active: v }
@@ -326,8 +420,8 @@ impl RuntimeActivityStats {
         // before, but not after
         disappeared.push(RuntimeActivity::AsyncOp(
           op.0,
+          before.trace_for(RuntimeActivityType::AsyncOp, op.0 as _),
           ops[op.1 as usize].decl.name,
-          before.opcall_traces.get(&op.0).map(|x| x.into()),
         ));
       }
     }
@@ -336,8 +430,8 @@ impl RuntimeActivityStats {
         // after but not before
         appeared.push(RuntimeActivity::AsyncOp(
           op.0,
+          after.trace_for(RuntimeActivityType::AsyncOp, op.0 as _),
           ops[op.1 as usize].decl.name,
-          after.opcall_traces.get(&op.0).map(|x| x.into()),
         ));
       }
     }
@@ -351,13 +445,13 @@ impl RuntimeActivityStats {
         // continuing op
       } else {
         // before, but not after
-        disappeared.push(RuntimeActivity::Resource(op.0, op.1.clone()));
+        disappeared.push(RuntimeActivity::Resource(op.0, None, op.1.clone()));
       }
     }
     for op in after.resources.resources.iter() {
       if a.contains(op.0 as usize) {
         // after but not before
-        appeared.push(RuntimeActivity::Resource(op.0, op.1.clone()));
+        appeared.push(RuntimeActivity::Resource(op.0, None, op.1.clone()));
       }
     }
 
@@ -372,9 +466,15 @@ impl RuntimeActivityStats {
       } else {
         // before, but not after
         if before.timers.repeats.contains(index) {
-          disappeared.push(RuntimeActivity::Interval(timer));
+          disappeared.push(RuntimeActivity::Interval(
+            timer,
+            before.trace_for(RuntimeActivityType::Timer, timer),
+          ));
         } else {
-          disappeared.push(RuntimeActivity::Timer(timer));
+          disappeared.push(RuntimeActivity::Timer(
+            timer,
+            before.trace_for(RuntimeActivityType::Timer, timer),
+          ));
         }
       }
     }
@@ -383,9 +483,15 @@ impl RuntimeActivityStats {
       if a.contains(timer) {
         // after but not before
         if after.timers.repeats.contains(index) {
-          appeared.push(RuntimeActivity::Interval(timer));
+          appeared.push(RuntimeActivity::Interval(
+            timer,
+            after.trace_for(RuntimeActivityType::Timer, timer),
+          ));
         } else {
-          appeared.push(RuntimeActivity::Timer(timer));
+          appeared.push(RuntimeActivity::Timer(
+            timer,
+            after.trace_for(RuntimeActivityType::Timer, timer),
+          ));
         }
       }
     }
diff --git a/core/web_timeout.rs b/core/web_timeout.rs
index b34e9a3..84c7c38 100644
--- a/core/web_timeout.rs
+++ b/core/web_timeout.rs
@@ -33,6 +33,15 @@ enum TimerType {
 #[derive(PartialEq, Eq, PartialOrd, Ord)]
 struct TimerKey(Instant, u64, TimerType);
 
+struct TimerData<T> {
+  data: T,
+  unrefd: bool,
+  #[cfg(any(windows, test))]
+  high_res: bool,
+  #[cfg(not(any(windows, test)))]
+  high_res: (),
+}
+
 /// Implements much of the specification described by https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html.
 ///
 /// To ensure that we perform well in the face of large numbers of timers, this implementation assumes
@@ -67,7 +76,7 @@ pub(crate) struct WebTimers<T> {
   next_id: Cell<WebTimerId>,
   timers: RefCell<BTreeSet<TimerKey>>,
   /// We choose a `BTreeMap` over `HashMap` because of memory performance.
-  data_map: RefCell<BTreeMap<WebTimerId, (T, bool)>>,
+  data_map: RefCell<BTreeMap<WebTimerId, TimerData<T>>>,
   /// How may deleted entries are in the timers BTreeMap? We use this to determine
   /// when there's too much garbage and need to rebuild the map.
   tombstone_count: Cell<usize>,
@@ -76,6 +85,8 @@ pub(crate) struct WebTimers<T> {
   /// A boxed MutableSleep that will allow us to change the Tokio sleep timeout as needed.
   /// Because this is boxed, we can "safely" unsafely poll it.
   sleep: Box<MutableSleep>,
+  /// The high-res timer lock. No-op on platforms other than Windows.
+  high_res_timer_lock: HighResTimerLock,
 }
 
 impl<T> Default for WebTimers<T> {
@@ -87,12 +98,13 @@ impl<T> Default for WebTimers<T> {
       tombstone_count: Default::default(),
       unrefd_count: Default::default(),
       sleep: MutableSleep::new(),
+      high_res_timer_lock: Default::default(),
     }
   }
 }
 
 pub(crate) struct WebTimersIterator<'a, T> {
-  data: Ref<'a, BTreeMap<WebTimerId, (T, bool)>>,
+  data: Ref<'a, BTreeMap<WebTimerId, TimerData<T>>>,
   timers: Ref<'a, BTreeSet<TimerKey>>,
 }
 
@@ -109,7 +121,7 @@ impl<'a, T> IntoIterator for &'a WebTimersIterator<'a, T> {
 }
 
 pub(crate) struct WebTimersIteratorImpl<'a, T> {
-  data: &'a BTreeMap<WebTimerId, (T, bool)>,
+  data: &'a BTreeMap<WebTimerId, TimerData<T>>,
   timers: btree_set::Iter<'a, TimerKey>,
 }
 
@@ -257,7 +269,9 @@ impl<T: Clone> WebTimers<T> {
 
   /// Refs a timer by ID. Invalid IDs are ignored.
   pub fn ref_timer(&self, id: WebTimerId) {
-    if let Some((_, unrefd)) = self.data_map.borrow_mut().get_mut(&id) {
+    if let Some(TimerData { unrefd, .. }) =
+      self.data_map.borrow_mut().get_mut(&id)
+    {
       if std::mem::replace(unrefd, false) {
         self.unrefd_count.set(self.unrefd_count.get() - 1);
       }
@@ -266,7 +280,9 @@ impl<T: Clone> WebTimers<T> {
 
   /// Unrefs a timer by ID. Invalid IDs are ignored.
   pub fn unref_timer(&self, id: WebTimerId) {
-    if let Some((_, unrefd)) = self.data_map.borrow_mut().get_mut(&id) {
+    if let Some(TimerData { unrefd, .. }) =
+      self.data_map.borrow_mut().get_mut(&id)
+    {
       if !std::mem::replace(unrefd, true) {
         self.unrefd_count.set(self.unrefd_count.get() + 1);
       }
@@ -289,6 +305,9 @@ impl<T: Clone> WebTimers<T> {
     timeout_ms: u64,
     data: T,
   ) -> WebTimerId {
+    #[allow(clippy::let_unit_value)]
+    let high_res = self.high_res_timer_lock.maybe_lock(timeout_ms);
+
     let id = self.next_id.get() + 1;
     self.next_id.set(id);
 
@@ -314,7 +333,14 @@ impl<T: Clone> WebTimers<T> {
     timers.insert(TimerKey(deadline, id, timer_type));
 
     let mut data_map = self.data_map.borrow_mut();
-    data_map.insert(id, (data, false));
+    data_map.insert(
+      id,
+      TimerData {
+        data,
+        unrefd: false,
+        high_res,
+      },
+    );
     id
   }
 
@@ -322,15 +348,30 @@ impl<T: Clone> WebTimers<T> {
   /// with the given ID was found.
   pub fn cancel_timer(&self, timer: u64) -> Option<T> {
     let mut data_map = self.data_map.borrow_mut();
-    if let Some((data, unrefd)) = data_map.remove(&timer) {
+    if let Some(TimerData {
+      data,
+      unrefd,
+      high_res,
+    }) = data_map.remove(&timer)
+    {
       if data_map.is_empty() {
         // When the # of running timers hits zero, clear the timer tree and
-        // tombstone count.
+        // tombstone count. When debug assertions are enabled, we do a
+        // consistency check.
+        debug_assert_eq!(self.unrefd_count.get(), if unrefd { 1 } else { 0 });
+        debug_assert_eq!(
+          self.tombstone_count.get() + 1,
+          self.timers.borrow().len()
+        );
+        #[cfg(any(windows, test))]
+        debug_assert_eq!(self.high_res_timer_lock.is_locked(), high_res);
+        self.high_res_timer_lock.clear();
         self.unrefd_count.set(0);
         self.timers.borrow_mut().clear();
         self.tombstone_count.set(0);
         self.sleep.clear();
       } else {
+        self.high_res_timer_lock.maybe_unlock(high_res);
         if unrefd {
           self.unrefd_count.set(self.unrefd_count.get() - 1);
         }
@@ -353,9 +394,10 @@ impl<T: Clone> WebTimers<T> {
 
     let mut split = timers.split_off(&TimerKey(now, 0, TimerType::Once));
     std::mem::swap(&mut split, &mut timers);
+    let mut tombstones = self.tombstone_count.get();
     for TimerKey(_, id, timer_type) in split {
       if let TimerType::Repeat(interval) = timer_type {
-        if let Some((data, _)) = data.get(&id) {
+        if let Some(TimerData { data, .. }) = data.get(&id) {
           output.push((id, data.clone()));
           timers.insert(TimerKey(
             now
@@ -365,15 +407,21 @@ impl<T: Clone> WebTimers<T> {
             timer_type,
           ));
         } else {
-          self.tombstone_count.set(self.tombstone_count.get() - 1);
+          tombstones -= 1;
         }
-      } else if let Some((data, unrefd)) = data.remove(&id) {
+      } else if let Some(TimerData {
+        data,
+        unrefd,
+        high_res,
+      }) = data.remove(&id)
+      {
+        self.high_res_timer_lock.maybe_unlock(high_res);
         if unrefd {
           self.unrefd_count.set(self.unrefd_count.get() - 1);
         }
         output.push((id, data));
       } else {
-        self.tombstone_count.set(self.tombstone_count.get() - 1);
+        tombstones -= 1;
       }
     }
 
@@ -387,14 +435,18 @@ impl<T: Clone> WebTimers<T> {
           break;
         } else {
           timers.pop_first();
+          tombstones -= 1;
         }
       }
       if let Some(TimerKey(k, ..)) = timers.first() {
         self.sleep.change(*k);
       }
+      self.tombstone_count.set(tombstones);
       return Poll::Pending;
     }
 
+    self.tombstone_count.set(tombstones);
+
     if data.is_empty() {
       // When the # of running timers hits zero, clear the timer tree and
       // tombstone count.
@@ -408,6 +460,10 @@ impl<T: Clone> WebTimers<T> {
       if self.tombstone_count.get() > data.len()
         && self.tombstone_count.get() > COMPACTION_MINIMUM
       {
+        // Run a consistency check on tombstone count before we zero it out. Note that
+        // we aren't changing unref or high-res timer lock counts here, so we don't
+        // check consistency of those.
+        debug_assert_eq!(self.tombstone_count.get() + data.len(), timers.len());
         self.tombstone_count.set(0);
         timers.retain(|k| data.contains_key(&k.1));
       }
@@ -438,16 +494,19 @@ impl<T: Clone> WebTimers<T> {
   #[cfg(test)]
   pub fn assert_consistent(&self) {
     if self.data_map.borrow().is_empty() {
-      // If the data map is empty, we should have no timers, no tombstones and no unref'd count
+      // If the data map is empty, we should have no timers, no tombstones, no unref'd count, no high-res lock
       assert_eq!(self.timers.borrow().len(), 0);
       assert_eq!(self.tombstone_count.get(), 0);
       assert_eq!(self.unrefd_count.get(), 0);
+      assert!(!self.high_res_timer_lock.is_locked());
     } else {
       assert_eq!(
         self.timers.borrow().len(),
         self.data_map.borrow().len() + self.tombstone_count.get()
       );
       assert!(self.unrefd_count.get() <= self.data_map.borrow().len());
+      // The high-res lock count must be <= the number of remaining timers
+      assert!(self.high_res_timer_lock.lock_count.get() <= self.len());
     }
   }
 
@@ -456,6 +515,99 @@ impl<T: Clone> WebTimers<T> {
   }
 }
 
+#[cfg(windows)]
+#[link(name = "winmm")]
+extern "C" {
+  fn timeBeginPeriod(n: u32);
+  fn timeEndPeriod(n: u32);
+}
+
+#[derive(Default)]
+struct HighResTimerLock {
+  #[cfg(any(windows, test))]
+  lock_count: Cell<usize>,
+}
+
+impl HighResTimerLock {
+  /// If a timer is requested with <=100ms resolution, request the high-res timer. Since the default
+  /// Windows timer period is 15ms, this means a 100ms timer could fire at 115ms (15% late). We assume that
+  /// timers longer than 100ms are a reasonable cutoff here.
+  ///
+  /// The high-res timers on Windows are still limited. Unfortunately this means that our shortest duration 4ms timers
+  /// can still be 25% late, but without a more complex timer system or spinning on the clock itself, we're somewhat
+  /// bounded by the OS' scheduler itself.
+  #[cfg(any(windows, test))]
+  const LOW_RES_TIMER_RESOLUTION: u64 = 100;
+
+  #[cfg(any(windows, test))]
+  #[inline(always)]
+  fn maybe_unlock(&self, high_res: bool) {
+    if high_res {
+      let old = self.lock_count.get();
+      debug_assert!(old > 0);
+      let new = old - 1;
+      self.lock_count.set(new);
+      #[cfg(windows)]
+      if new == 0 {
+        // SAFETY: Windows API
+        unsafe {
+          timeEndPeriod(1);
+        }
+      }
+    }
+  }
+
+  #[cfg(not(any(windows, test)))]
+  #[inline(always)]
+  fn maybe_unlock(&self, _high_res: ()) {}
+
+  #[cfg(any(windows, test))]
+  #[inline(always)]
+  fn maybe_lock(&self, timeout_ms: u64) -> bool {
+    if timeout_ms <= Self::LOW_RES_TIMER_RESOLUTION {
+      let old = self.lock_count.get();
+      #[cfg(windows)]
+      if old == 0 {
+        // SAFETY: Windows API
+        unsafe {
+          timeBeginPeriod(1);
+        }
+      }
+      self.lock_count.set(old + 1);
+      true
+    } else {
+      false
+    }
+  }
+
+  #[cfg(not(any(windows, test)))]
+  #[inline(always)]
+  fn maybe_lock(&self, _timeout_ms: u64) {}
+
+  #[cfg(any(windows, test))]
+  #[inline(always)]
+  fn clear(&self) {
+    #[cfg(windows)]
+    if self.lock_count.get() > 0 {
+      // SAFETY: Windows API
+      unsafe {
+        timeEndPeriod(1);
+      }
+    }
+
+    self.lock_count.set(0);
+  }
+
+  #[cfg(not(any(windows, test)))]
+  #[inline(always)]
+  fn clear(&self) {}
+
+  #[cfg(any(windows, test))]
+  fn is_locked(&self) -> bool {
+    self.lock_count.get() > 0
+  }
+}
+
 #[cfg(test)]
 mod tests {
   use super::*;
@@ -479,9 +631,19 @@ mod tests {
     let len = timers.len();
     let mut v = vec![];
     while !timers.is_empty() {
-      let mut batch = poll_fn(|cx| timers.poll_timers(cx)).await;
+      let mut batch = poll_fn(|cx| {
+        timers.assert_consistent();
+        timers.poll_timers(cx)
+      })
+      .await;
       v.append(&mut batch);
-      eprintln!("{}", v.len());
+      eprintln!(
+        "{} ({} {} {})",
+        v.len(),
+        timers.len(),
+        timers.data_map.borrow().len(),
+        timers.tombstone_count.get()
+      );
       timers.assert_consistent();
     }
     assert_eq!(v.len(), len);
@@ -499,21 +661,53 @@ mod tests {
     });
   }
 
+  #[test]
+  fn test_high_res_lock() {
+    async_test(async {
+      let timers = WebTimers::<()>::default();
+      assert!(!timers.high_res_timer_lock.is_locked());
+      let _a = timers.queue_timer(1, ());
+      assert!(timers.high_res_timer_lock.is_locked());
+
+      let v = poll_all(&timers).await;
+      assert_eq!(v.len(), 1);
+      assert!(!timers.high_res_timer_lock.is_locked());
+    });
+  }
+
   #[rstest]
   #[test]
-  fn test_timer_cancel_1(#[values(0, 1)] which: u64) {
+  fn test_timer_cancel_1(#[values(0, 1, 2, 3)] which: u64) {
     async_test(async {
       let timers = WebTimers::<()>::default();
-      for i in 0..2 {
-        let id = timers.queue_timer(i * 100, ());
+      for i in 0..4 {
+        let id = timers.queue_timer(i * 25, ());
         if i == which {
           assert!(timers.cancel_timer(id).is_some());
         }
       }
-      assert_eq!(timers.len(), 1);
+      assert_eq!(timers.len(), 3);
 
       let v = poll_all(&timers).await;
-      assert_eq!(v.len(), 1);
+      assert_eq!(v.len(), 3);
+    })
+  }
+
+  #[rstest]
+  #[test]
+  fn test_timer_cancel_2(#[values(0, 1, 2)] which: u64) {
+    async_test(async {
+      let timers = WebTimers::<()>::default();
+      for i in 0..4 {
+        let id = timers.queue_timer(i * 25, ());
+        if i == which || i == which + 1 {
+          assert!(timers.cancel_timer(id).is_some());
+        }
+      }
+      assert_eq!(timers.len(), 2);
+
+      let v = poll_all(&timers).await;
+      assert_eq!(v.len(), 2);
     })
   }
 
@@ -602,8 +796,8 @@ mod tests {
       for _ in 0..TEN_THOUSAND {
         ids.push(timers.queue_timer(1, ()));
       }
-      for _ in 0..10 {
-        timers.queue_timer(1000, ());
+      for i in 0..10 {
+        timers.queue_timer(i * 25, ());
       }
       for id in ids {
         timers.cancel_timer(id);
diff --git a/ops/Cargo.toml b/ops/Cargo.toml
index ee177bb..37458e5 100644
--- a/ops/Cargo.toml
+++ b/ops/Cargo.toml
@@ -2,7 +2,7 @@
 
 [package]
 name = "deno_ops"
-version = "0.140.0"
+version = "0.142.0"
 authors.workspace = true
 edition.workspace = true
 license.workspace = true
diff --git a/serde_v8/Cargo.toml b/serde_v8/Cargo.toml
index 491502a..362ee8c 100644
--- a/serde_v8/Cargo.toml
+++ b/serde_v8/Cargo.toml
@@ -2,7 +2,7 @@
 
 [package]
 name = "serde_v8"
-version = "0.173.0"
+version = "0.175.0"
 authors.workspace = true
 edition.workspace = true
 license.workspace = true


Test patch:
diff --git a/testing/checkin.d.ts b/testing/checkin.d.ts
index ae215c2..cb61294 100644
--- a/testing/checkin.d.ts
+++ b/testing/checkin.d.ts
@@ -28,11 +28,11 @@ declare namespace Deno {
   export function unrefTimer(id);
 }
 
-declare module 'ext:core/mod.js' {
+declare module "ext:core/mod.js" {
   const core: any;
 }
 
-declare module 'ext:core/ops' {
+declare module "ext:core/ops" {
   function op_log_debug(...any): any;
   function op_log_info(...any): any;
 
diff --git a/testing/checkin/runner/testing.rs b/testing/checkin/runner/testing.rs
index 258b21b..aae21ee 100644
--- a/testing/checkin/runner/testing.rs
+++ b/testing/checkin/runner/testing.rs
@@ -43,7 +43,7 @@ impl TestData {
       .get(&key)
       .unwrap_or_else(|| {
         panic!(
-          "Unable to locate {} of type {}",
+          "Unable to locate '{}' of type {}",
           key.0,
           std::any::type_name::<T>()
         )
@@ -53,11 +53,14 @@ impl TestData {
   }
 
   pub fn take<T: 'static + Any>(&mut self, name: String) -> T {
-    *self
-      .data
-      .remove(&(name, TypeId::of::<T>()))
-      .unwrap()
-      .downcast()
-      .unwrap()
+    let key = (name, TypeId::of::<T>());
+    let Some(res) = self.data.remove(&key) else {
+      panic!(
+        "Failed to remove '{}' of type {}",
+        key.0,
+        std::any::type_name::<T>()
+      );
+    };
+    *res.downcast().unwrap()
   }
 }
diff --git a/testing/checkin/runtime/async.ts b/testing/checkin/runtime/async.ts
index 3042ac8..bca09c1 100644
--- a/testing/checkin/runtime/async.ts
+++ b/testing/checkin/runtime/async.ts
@@ -68,47 +68,50 @@ export class StatsDiff {
   }
 }
 
+export enum LeakType {
+  AsyncOp = "AsyncOp",
+  Resource = "Resource",
+  Timer = "Timer",
+  Interval = "Interval",
+}
+
 // This contains an array of serialized RuntimeActivity structs.
 export class StatsCollection {
   // deno-lint-ignore no-explicit-any
   constructor(private data: any[]) {
+    console.log(data);
   }
 
-  private countResourceActivity(type: string): number {
+  count(...types: LeakType[]): number {
     let count = 0;
     for (const item of this.data) {
-      if (type in item) {
-        count++;
+      for (const type of types) {
+        if (type in item) {
+          count++;
+        }
       }
     }
     return count;
   }
 
-  countOps(): number {
-    return this.countResourceActivity("AsyncOp");
-  }
-
-  countOpsWithTraces(): number {
+  countWithTraces(...types: LeakType[]): number {
     let count = 0;
     for (const item of this.data) {
-      if ("AsyncOp" in item) {
-        if (typeof item["AsyncOp"][2] === "string") {
-          count++;
+      for (const type of types) {
+        if (type in item) {
+          // Make sure it's a non-empty stack trace
+          if (
+            typeof item[type][1] === "string" &&
+            item[type][1].trim().length > 0
+          ) {
+            count++;
+          }
         }
       }
     }
     return count;
   }
 
-  countResources(): number {
-    return this.countResourceActivity("Resource");
-  }
-
-  countTimers(): number {
-    return this.countResourceActivity("Timer") +
-      this.countResourceActivity("Interval");
-  }
-
   get rawData() {
     return this.data;
   }
diff --git a/testing/checkin/runtime/timers.ts b/testing/checkin/runtime/timers.ts
index 506d0c9..63d8537 100644
--- a/testing/checkin/runtime/timers.ts
+++ b/testing/checkin/runtime/timers.ts
@@ -4,7 +4,7 @@
  * Call a callback function after a delay.
  */
 export function setTimeout(callback, delay = 0) {
-  return Deno.core.queueTimer(
+  return Deno.core.queueUserTimer(
     Deno.core.getTimerDepth() + 1,
     false,
     delay,
@@ -16,7 +16,7 @@ export function setTimeout(callback, delay = 0) {
  * Call a callback function after a delay.
  */
 export function setInterval(callback, delay = 0) {
-  return Deno.core.queueTimer(
+  return Deno.core.queueUserTimer(
     Deno.core.getTimerDepth() + 1,
     true,
     delay,
diff --git a/testing/unit/stats_test.ts b/testing/unit/stats_test.ts
index 7991434..7552312 100644
--- a/testing/unit/stats_test.ts
+++ b/testing/unit/stats_test.ts
@@ -1,5 +1,10 @@
 // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
-import { barrierAwait, barrierCreate, StatsFactory } from "checkin:async";
+import {
+  barrierAwait,
+  barrierCreate,
+  LeakType,
+  StatsFactory,
+} from "checkin:async";
 import {
   assert,
   assertEquals,
@@ -15,17 +20,20 @@ test(async function testStatsOps() {
 
   barrierCreate("barrier", 3);
   const promise1 = barrierAwait("barrier");
-  assertEquals(1, StatsFactory.capture().dump().countOps());
+  assertEquals(1, StatsFactory.capture().dump().count(LeakType.AsyncOp));
   const promise2 = barrierAwait("barrier");
-  assertEquals(2, StatsFactory.capture().dump().countOps());
+  assertEquals(2, StatsFactory.capture().dump().count(LeakType.AsyncOp));
   // No traces here at all, even though we have ops
-  assertEquals(0, StatsFactory.capture().dump().countOpsWithTraces());
+  assertEquals(
+    0,
+    StatsFactory.capture().dump().countWithTraces(LeakType.AsyncOp),
+  );
   using statsMiddle = StatsFactory.capture();
   const diffMiddle = StatsFactory.diff(statsBefore, statsMiddle);
-  assertEquals(0, diffMiddle.disappeared.countOps());
-  assertEquals(2, diffMiddle.appeared.countOps());
+  assertEquals(0, diffMiddle.disappeared.count(LeakType.AsyncOp));
+  assertEquals(2, diffMiddle.appeared.count(LeakType.AsyncOp));
   // No traces here at all, even though we have ops
-  assertEquals(0, diffMiddle.appeared.countOpsWithTraces());
+  assertEquals(0, diffMiddle.appeared.countWithTraces(LeakType.AsyncOp));
 
   await Promise.all([promise1, promise2, barrierAwait("barrier")]);
 
@@ -40,8 +48,8 @@ test(function testStatsResources() {
   const [p1, p2] = op_pipe_create();
   using statsMiddle = StatsFactory.capture();
   const diffMiddle = StatsFactory.diff(statsBefore, statsMiddle);
-  assertEquals(0, diffMiddle.disappeared.countResources());
-  assertEquals(2, diffMiddle.appeared.countResources());
+  assertEquals(0, diffMiddle.disappeared.count(LeakType.Resource));
+  assertEquals(2, diffMiddle.appeared.count(LeakType.Resource));
   Deno.core.close(p1);
   Deno.core.close(p2);
 
@@ -58,8 +66,11 @@ test(function testTimers() {
 
   using statsMiddle = StatsFactory.capture();
   const diffMiddle = StatsFactory.diff(statsBefore, statsMiddle);
-  assertEquals(0, diffMiddle.disappeared.countTimers());
-  assertEquals(2, diffMiddle.appeared.countTimers());
+  assertEquals(
+    0,
+    diffMiddle.disappeared.count(LeakType.Timer, LeakType.Interval),
+  );
+  assertEquals(2, diffMiddle.appeared.count(LeakType.Timer, LeakType.Interval));
   clearTimeout(timeout);
   clearInterval(interval);
 
@@ -68,32 +79,74 @@ test(function testTimers() {
   assert(diff.empty);
 });
 
-test(async function testAsyncOpCallTrace() {
-  const oldTracingState = Deno.core.isOpCallTracingEnabled();
-  Deno.core.setOpCallTracingEnabled(true);
-  barrierCreate("barrier", 2);
+async function enableTracingForTest(f: () => Promise<void> | void) {
+  const oldTracingState = Deno.core.isLeakTracingEnabled();
+  Deno.core.setLeakTracingEnabled(true);
   try {
-    const tracesBefore = Deno.core.getAllOpCallTraces();
+    await f();
+  } finally {
+    Deno.core.setLeakTracingEnabled(oldTracingState);
+  }
+}
+
+test(async function testAsyncLeakTrace() {
+  await enableTracingForTest(async () => {
+    barrierCreate("barrier", 2);
+    const tracesBefore = Deno.core.getAllLeakTraces();
     using statsBefore = StatsFactory.capture();
     const p1 = barrierAwait("barrier");
-    const tracesAfter = Deno.core.getAllOpCallTraces();
+    const tracesAfter = Deno.core.getAllLeakTraces();
     using statsAfter = StatsFactory.capture();
     const diff = StatsFactory.diff(statsBefore, statsAfter);
     // We don't test the contents, just that we have a trace here
-    assertEquals(diff.appeared.countOpsWithTraces(), 1);
+    assertEquals(diff.appeared.countWithTraces(LeakType.AsyncOp), 1);
 
     assertEquals(tracesAfter.size, tracesBefore.size + 1);
     assertStackTraceEquals(
-      Deno.core.getOpCallTraceForPromise(p1),
+      Deno.core.getLeakTraceForPromise(p1),
       `
       at op_async_barrier_await (ext:core/00_infra.js:line:col)
       at barrierAwait (checkin:async:line:col)
-      at testAsyncOpCallTrace (test:///unit/stats_test.ts:line:col)
+      at test:///unit/stats_test.ts:line:col
+      at enableTracingForTest (test:///unit/stats_test.ts:line:col)
+      at testAsyncLeakTrace (test:///unit/stats_test.ts:line:col)
     `,
     );
     const p2 = barrierAwait("barrier");
     await Promise.all([p1, p2]);
-  } finally {
-    Deno.core.setOpCallTracingEnabled(oldTracingState);
-  }
+  });
+});
+
+test(async function testTimeoutLeakTrace() {
+  await enableTracingForTest(() => {
+    const tracesBefore = Deno.core.getAllLeakTraces();
+    using statsBefore = StatsFactory.capture();
+    const t1 = setTimeout(() => {}, 100_000);
+    const tracesAfter = Deno.core.getAllLeakTraces();
+    using statsAfter = StatsFactory.capture();
+    const diff = StatsFactory.diff(statsBefore, statsAfter);
+    assertEquals(diff.appeared.countWithTraces(LeakType.Timer), 1);
+
+    assertEquals(tracesAfter.size, tracesBefore.size + 1);
+    clearTimeout(t1);
+    const tracesFinal = Deno.core.getAllLeakTraces();
+    assertEquals(tracesFinal.size, 0);
+  });
+});
+
+test(async function testIntervalLeakTrace() {
+  await enableTracingForTest(() => {
+    const tracesBefore = Deno.core.getAllLeakTraces();
+    using statsBefore = StatsFactory.capture();
+    const t1 = setInterval(() => {}, 100_000);
+    const tracesAfter = Deno.core.getAllLeakTraces();
+    using statsAfter = StatsFactory.capture();
+    const diff = StatsFactory.diff(statsBefore, statsAfter);
+    assertEquals(diff.appeared.countWithTraces(LeakType.Interval), 1);
+
+    assertEquals(tracesAfter.size, tracesBefore.size + 1);
+    clearInterval(t1);
+    const tracesFinal = Deno.core.getAllLeakTraces();
+    assertEquals(tracesFinal.size, 0);
+  });
 });
diff --git a/testing/unit/timer_test.ts b/testing/unit/timer_test.ts
index e124227..978ac30 100644
--- a/testing/unit/timer_test.ts
+++ b/testing/unit/timer_test.ts
@@ -115,3 +115,23 @@ test(async function testTimerThis() {
   }, 1);
   await promise;
 });
+
+test(async function testCancellationDuringDispatch() {
+  const timers: number[] = [];
+  let timeouts = 0;
+
+  // If a timer is ready to be dispatched, but is cancelled during the
+  // dispatch of a previous timer that is also ready, that timer should
+  // not be dispatched.
+  function onTimeout() {
+    ++timeouts;
+    for (let i = 1; i < timers.length; i++) {
+      clearTimeout(timers[i]);
+    }
+  }
+  for (let i = 0; i < 10; i++) {
+    timers[i] = setTimeout(onTimeout, 1);
+  }
+  await new Promise((r) => setTimeout(r, 10));
+  assertEquals(timeouts, 1);
+});