Repository: denoland/deno_core
Pull request #573 (2024-02-12 19:44:27)

refactor(core): opcall tracing in Rust
Moves the core of the opcall tracing functionality into Rust. Tested against deno (which will need small source changes).

Re-organizes the op dispatch code a bit as well, but performance is unimpacted per benchmarks (if not slightly faster).


Patch:
diff --git a/.github/workflows/ci-job.yml b/.github/workflows/ci-job.yml
index c694a6e..111b016 100644
--- a/.github/workflows/ci-job.yml
+++ b/.github/workflows/ci-job.yml
@@ -39,6 +39,10 @@ jobs:
       - name: Clone repository
         uses: actions/checkout@v3
 
+      - name: Install ARM runner tools
+        if: ${{ inputs.os == 'ubuntu-22.04-arm64' }}
+        run: sudo apt update && sudo apt-get install -y unzip clang
+
       - name: Install Rust
         uses: dsherret/rust-toolchain-file@v1
 
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 020fde5..5b05262 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -57,6 +57,9 @@ jobs:
             linux:
               os: ubuntu-latest
               jobs: [lint, lint-deps, test, test-miri, test-ops, test-publish]
+            linux-arm:
+              os: ubuntu-22.04-arm64
+              jobs: [lint, test, test-miri]
             macos:
               os: macOS-latest
               jobs: [lint, test]
diff --git a/core/00_infra.js b/core/00_infra.js
index e589b27..4fc40f4 100644
--- a/core/00_infra.js
+++ b/core/00_infra.js
@@ -21,23 +21,12 @@
     RangeError,
     ReferenceError,
     SafeMap,
-    SafePromisePrototypeFinally,
-    StringPrototypeSlice,
     StringPrototypeSplit,
     SymbolFor,
     SyntaxError,
     TypeError,
     URIError,
   } = window.__bootstrap.primordials;
-  // TODO(bartlomieju): not ideal - effectively we have circular dependency between
-  // 00_infra.js and 01_core.js. Figure out how to fix it.
-  // We have two proposals:
-  //  1. Move `eventLoopTick` function to this file and add `setUpEventLoopTick`
-  //     that would be called by `01_core.js` and forward `op_run_microtasks`
-  //     and `op_dispatch_exception` so we can save references to them.
-  //  2. Add `captureStackTrace` function that will perform stack trace capturing
-  //     and can define which function should the trace hide.
-  const core_ = window.Deno.core;
 
   let nextPromiseId = 0;
   const promiseMap = new SafeMap();
@@ -48,6 +37,19 @@
   // to users. Currently missing bindings.
   const promiseIdSymbol = SymbolFor("Deno.core.internalPromiseId");
 
+  let isOpCallTracingEnabled = false;
+  let submitOpCallTrace;
+  let eventLoopTick;
+
+  function __setOpCallTracingEnabled(enabled) {
+    isOpCallTracingEnabled = enabled;
+  }
+
+  function __initializeCoreMethods(eventLoopTick_, submitOpCallTrace_) {
+    eventLoopTick = eventLoopTick_;
+    submitOpCallTrace = submitOpCallTrace_;
+  }
+
   const build = {
     target: "unknown",
     arch: "unknown",
@@ -109,42 +111,6 @@
     errorMap[className] = errorBuilder;
   }
 
-  let opCallTracingEnabled = false;
-  const opCallTraces = new SafeMap();
-
-  function getAllOpCallTraces() {
-    return new Map(opCallTraces);
-  }
-
-  function setOpCallTracingEnabled(enabled) {
-    opCallTracingEnabled = enabled;
-  }
-
-  function isOpCallTracingEnabled() {
-    return opCallTracingEnabled;
-  }
-
-  function getOpCallTraceForPromise(promise) {
-    const trace = MapPrototypeGet(opCallTraces, promise[promiseIdSymbol]);
-    if (trace) {
-      return trace.stack;
-    }
-    return null;
-  }
-
-  function handleOpCallTracing(opName, promiseId, p) {
-    if (opCallTracingEnabled) {
-      const stack = StringPrototypeSlice(new Error().stack, 6);
-      MapPrototypeSet(opCallTraces, promiseId, { opName, stack });
-      return SafePromisePrototypeFinally(
-        p,
-        () => MapPrototypeDelete(opCallTraces, promiseId),
-      );
-    } else {
-      return p;
-    }
-  }
-
   function setPromise(promiseId) {
     const idx = promiseId % RING_SIZE;
     // Move old promise from ring to map
@@ -153,11 +119,19 @@
       const oldPromiseId = promiseId - RING_SIZE;
       MapPrototypeSet(promiseMap, oldPromiseId, oldPromise);
     }
-    // Set new promise
-    return promiseRing[idx] = newPromise();
+
+    const promise = new Promise((resolve) => {
+      promiseRing[idx] = resolve;
+    });
+    const wrappedPromise = PromisePrototypeThen(
+      promise,
+      unwrapOpError(),
+    );
+    wrappedPromise[promiseIdSymbol] = promiseId;
+    return wrappedPromise;
   }
 
-  function getPromise(promiseId) {
+  function __resolvePromise(promiseId, res) {
     // Check if out of ring bounds, fallback to map
     const outOfBounds = promiseId < nextPromiseId - RING_SIZE;
     if (outOfBounds) {
@@ -166,27 +140,17 @@
         throw "Missing promise in map @ " + promiseId;
       }
       MapPrototypeDelete(promiseMap, promiseId);
-      return promise;
-    }
-    // Otherwise take from ring
-    const idx = promiseId % RING_SIZE;
-    const promise = promiseRing[idx];
-    if (!promise) {
-      throw "Missing promise in ring @ " + promiseId;
+      promise(res);
+    } else {
+      // Otherwise take from ring
+      const idx = promiseId % RING_SIZE;
+      const promise = promiseRing[idx];
+      if (!promise) {
+        throw "Missing promise in ring @ " + promiseId;
+      }
+      promiseRing[idx] = NO_PROMISE;
+      promise(res);
     }
-    promiseRing[idx] = NO_PROMISE;
-    return promise;
-  }
-
-  function newPromise() {
-    let resolve, reject;
-    const promise = new Promise((resolve_, reject_) => {
-      resolve = resolve_;
-      reject = reject_;
-    });
-    promise.resolve = resolve;
-    promise.reject = reject;
-    return promise;
   }
 
   function hasPromise(promiseId) {
@@ -200,7 +164,7 @@
     return promiseRing[idx] != NO_PROMISE;
   }
 
-  function unwrapOpError(hideFunction) {
+  function unwrapOpError() {
     return (res) => {
       // .$err_class_name is a special key that should only exist on errors
       const className = res?.$err_class_name;
@@ -216,8 +180,8 @@
       if (res.code) {
         err.code = res.code;
       }
-      // Strip unwrapOpResult() and errorBuilder() calls from stack trace
-      ErrorCaptureStackTrace(err, hideFunction);
+      // Strip eventLoopTick() calls from stack trace
+      ErrorCaptureStackTrace(err, eventLoopTick);
       throw err;
     };
   }
@@ -240,14 +204,11 @@
             ErrorCaptureStackTrace(err, async_op_0);
             return PromiseReject(err);
           }
+          if (isOpCallTracingEnabled) {
+            submitOpCallTrace(id);
+          }
           nextPromiseId = (id + 1) & 0xffffffff;
-          let promise = PromisePrototypeThen(
-            setPromise(id),
-            unwrapOpError(core_.eventLoopTick),
-          );
-          promise = handleOpCallTracing(opName, id, promise);
-          promise[promiseIdSymbol] = id;
-          return promise;
+          return setPromise(id);
         };
         break;
       case 1:
@@ -262,14 +223,11 @@
             ErrorCaptureStackTrace(err, async_op_1);
             return PromiseReject(err);
           }
+          if (isOpCallTracingEnabled) {
+            submitOpCallTrace(id);
+          }
           nextPromiseId = (id + 1) & 0xffffffff;
-          let promise = PromisePrototypeThen(
-            setPromise(id),
-            unwrapOpError(core_.eventLoopTick),
-          );
-          promise = handleOpCallTracing(opName, id, promise);
-          promise[promiseIdSymbol] = id;
-          return promise;
+          return setPromise(id);
         };
         break;
       case 2:
@@ -284,14 +242,11 @@
             ErrorCaptureStackTrace(err, async_op_2);
             return PromiseReject(err);
           }
+          if (isOpCallTracingEnabled) {
+            submitOpCallTrace(id);
+          }
           nextPromiseId = (id + 1) & 0xffffffff;
-          let promise = PromisePrototypeThen(
-            setPromise(id),
-            unwrapOpError(core_.eventLoopTick),
-          );
-          promise = handleOpCallTracing(opName, id, promise);
-          promise[promiseIdSymbol] = id;
-          return promise;
+          return setPromise(id);
         };
         break;
       case 3:
@@ -306,14 +261,11 @@
             ErrorCaptureStackTrace(err, async_op_3);
             return PromiseReject(err);
           }
+          if (isOpCallTracingEnabled) {
+            submitOpCallTrace(id);
+          }
           nextPromiseId = (id + 1) & 0xffffffff;
-          let promise = PromisePrototypeThen(
-            setPromise(id),
-            unwrapOpError(core_.eventLoopTick),
-          );
-          promise = handleOpCallTracing(opName, id, promise);
-          promise[promiseIdSymbol] = id;
-          return promise;
+          return setPromise(id);
         };
         break;
       case 4:
@@ -328,14 +280,11 @@
             ErrorCaptureStackTrace(err, async_op_4);
             return PromiseReject(err);
           }
+          if (isOpCallTracingEnabled) {
+            submitOpCallTrace(id);
+          }
           nextPromiseId = (id + 1) & 0xffffffff;
-          let promise = PromisePrototypeThen(
-            setPromise(id),
-            unwrapOpError(core_.eventLoopTick),
-          );
-          promise = handleOpCallTracing(opName, id, promise);
-          promise[promiseIdSymbol] = id;
-          return promise;
+          return setPromise(id);
         };
         break;
       case 5:
@@ -350,14 +299,11 @@
             ErrorCaptureStackTrace(err, async_op_5);
             return PromiseReject(err);
           }
+          if (isOpCallTracingEnabled) {
+            submitOpCallTrace(id);
+          }
           nextPromiseId = (id + 1) & 0xffffffff;
-          let promise = PromisePrototypeThen(
-            setPromise(id),
-            unwrapOpError(core_.eventLoopTick),
-          );
-          promise = handleOpCallTracing(opName, id, promise);
-          promise[promiseIdSymbol] = id;
-          return promise;
+          return setPromise(id);
         };
         break;
       case 6:
@@ -372,14 +318,11 @@
             ErrorCaptureStackTrace(err, async_op_6);
             return PromiseReject(err);
           }
+          if (isOpCallTracingEnabled) {
+            submitOpCallTrace(id);
+          }
           nextPromiseId = (id + 1) & 0xffffffff;
-          let promise = PromisePrototypeThen(
-            setPromise(id),
-            unwrapOpError(core_.eventLoopTick),
-          );
-          promise = handleOpCallTracing(opName, id, promise);
-          promise[promiseIdSymbol] = id;
-          return promise;
+          return setPromise(id);
         };
         break;
       case 7:
@@ -394,14 +337,11 @@
             ErrorCaptureStackTrace(err, async_op_7);
             return PromiseReject(err);
           }
+          if (isOpCallTracingEnabled) {
+            submitOpCallTrace(id);
+          }
           nextPromiseId = (id + 1) & 0xffffffff;
-          let promise = PromisePrototypeThen(
-            setPromise(id),
-            unwrapOpError(core_.eventLoopTick),
-          );
-          promise = handleOpCallTracing(opName, id, promise);
-          promise[promiseIdSymbol] = id;
-          return promise;
+          return setPromise(id);
         };
         break;
       case 8:
@@ -416,14 +356,11 @@
             ErrorCaptureStackTrace(err, async_op_8);
             return PromiseReject(err);
           }
+          if (isOpCallTracingEnabled) {
+            submitOpCallTrace(id);
+          }
           nextPromiseId = (id + 1) & 0xffffffff;
-          let promise = PromisePrototypeThen(
-            setPromise(id),
-            unwrapOpError(core_.eventLoopTick),
-          );
-          promise = handleOpCallTracing(opName, id, promise);
-          promise[promiseIdSymbol] = id;
-          return promise;
+          return setPromise(id);
         };
         break;
       case 9:
@@ -438,14 +375,11 @@
             ErrorCaptureStackTrace(err, async_op_9);
             return PromiseReject(err);
           }
+          if (isOpCallTracingEnabled) {
+            submitOpCallTrace(id);
+          }
           nextPromiseId = (id + 1) & 0xffffffff;
-          let promise = PromisePrototypeThen(
-            setPromise(id),
-            unwrapOpError(core_.eventLoopTick),
-          );
-          promise = handleOpCallTracing(opName, id, promise);
-          promise[promiseIdSymbol] = id;
-          return promise;
+          return setPromise(id);
         };
         break;
       /* END TEMPLATE */
@@ -472,15 +406,12 @@
     registerErrorBuilder,
     buildCustomError,
     registerErrorClass,
-    setOpCallTracingEnabled,
-    isOpCallTracingEnabled,
-    getAllOpCallTraces,
-    getOpCallTraceForPromise,
-    handleOpCallTracing,
     setUpAsyncStub,
-    getPromise,
     hasPromise,
     promiseIdSymbol,
+    __resolvePromise,
+    __setOpCallTracingEnabled,
+    __initializeCoreMethods,
   });
 
   ObjectAssign(globalThis.__bootstrap, { core });
diff --git a/core/01_core.js b/core/01_core.js
index 7772148..67ddbb4 100644
--- a/core/01_core.js
+++ b/core/01_core.js
@@ -6,6 +6,7 @@
     ArrayPrototypeMap,
     ArrayPrototypePush,
     Error,
+    ErrorCaptureStackTrace,
     FunctionPrototypeBind,
     ObjectAssign,
     ObjectFreeze,
@@ -14,6 +15,8 @@
     ObjectHasOwn,
     Proxy,
     setQueueMicrotask,
+    SafeMap,
+    StringPrototypeSlice,
     Symbol,
     SymbolFor,
     TypedArrayPrototypeGetLength,
@@ -24,11 +27,97 @@
   } = window.__bootstrap.primordials;
   const {
     ops,
-    getPromise,
     hasPromise,
     promiseIdSymbol,
     registerErrorClass,
+    __setOpCallTracingEnabled,
+    __initializeCoreMethods,
+    __resolvePromise,
   } = window.Deno.core;
+  const {
+    op_abort_wasm_streaming,
+    op_current_user_call_site,
+    op_decode,
+    op_deserialize,
+    op_destructure_error,
+    op_dispatch_exception,
+    op_encode,
+    op_encode_binary_string,
+    op_eval_context,
+    op_event_loop_has_more_work,
+    op_get_promise_details,
+    op_get_proxy_details,
+    op_has_tick_scheduled,
+    op_lazy_load_esm,
+    op_memory_usage,
+    op_op_names,
+    op_panic,
+    op_print,
+    op_queue_microtask,
+    op_ref_op,
+    op_resources,
+    op_run_microtasks,
+    op_serialize,
+    op_set_handled_promise_rejection_handler,
+    op_set_has_tick_scheduled,
+    op_set_promise_hooks,
+    op_set_wasm_streaming_callback,
+    op_str_byte_length,
+    op_timer_cancel,
+    op_timer_queue,
+    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_is_any_array_buffer,
+    op_is_arguments_object,
+    op_is_array_buffer,
+    op_is_array_buffer_view,
+    op_is_async_function,
+    op_is_big_int_object,
+    op_is_boolean_object,
+    op_is_boxed_primitive,
+    op_is_data_view,
+    op_is_date,
+    op_is_generator_function,
+    op_is_generator_object,
+    op_is_map,
+    op_is_map_iterator,
+    op_is_module_namespace_object,
+    op_is_native_error,
+    op_is_number_object,
+    op_is_promise,
+    op_is_proxy,
+    op_is_reg_exp,
+    op_is_set,
+    op_is_set_iterator,
+    op_is_shared_array_buffer,
+    op_is_string_object,
+    op_is_symbol_object,
+    op_is_typed_array,
+    op_is_weak_map,
+    op_is_weak_set,
+  } = ensureFastOps();
+
+  // core/infra collaborative code
+  delete window.Deno.core.__setOpCallTracingEnabled;
+  delete window.Deno.core.__initializeCoreMethods;
+  delete window.Deno.core.__resolvePromise;
+  __initializeCoreMethods(
+    eventLoopTick,
+    submitOpCallTrace,
+  );
+
+  function submitOpCallTrace(id) {
+    const error = new Error();
+    ErrorCaptureStackTrace(error, submitOpCallTrace);
+    op_opcall_tracing_submit(id, StringPrototypeSlice(error.stack, 6));
+  }
 
   let unhandledPromiseRejectionHandler = () => false;
   let timerDepth = 0;
@@ -53,8 +142,7 @@
     for (let i = 0; i < arguments.length - 3; i += 2) {
       const promiseId = arguments[i];
       const res = arguments[i + 1];
-      const promise = getPromise(promiseId);
-      promise.resolve(res);
+      __resolvePromise(promiseId, res);
     }
     // Drain nextTick queue if there's a tick scheduled.
     if (arguments[arguments.length - 1]) {
@@ -400,72 +488,6 @@
   globalThis.console = coreConsole;
   wrapConsole(coreConsole, v8Console);
 
-  const {
-    op_abort_wasm_streaming,
-    op_current_user_call_site,
-    op_decode,
-    op_deserialize,
-    op_destructure_error,
-    op_dispatch_exception,
-    op_encode,
-    op_encode_binary_string,
-    op_eval_context,
-    op_event_loop_has_more_work,
-    op_get_promise_details,
-    op_get_proxy_details,
-    op_has_tick_scheduled,
-    op_lazy_load_esm,
-    op_memory_usage,
-    op_op_names,
-    op_panic,
-    op_print,
-    op_queue_microtask,
-    op_ref_op,
-    op_resources,
-    op_run_microtasks,
-    op_serialize,
-    op_set_handled_promise_rejection_handler,
-    op_set_has_tick_scheduled,
-    op_set_promise_hooks,
-    op_set_wasm_streaming_callback,
-    op_str_byte_length,
-    op_timer_cancel,
-    op_timer_queue,
-    op_timer_ref,
-    op_timer_unref,
-    op_unref_op,
-    op_cancel_handle,
-
-    op_is_any_array_buffer,
-    op_is_arguments_object,
-    op_is_array_buffer,
-    op_is_array_buffer_view,
-    op_is_async_function,
-    op_is_big_int_object,
-    op_is_boolean_object,
-    op_is_boxed_primitive,
-    op_is_data_view,
-    op_is_date,
-    op_is_generator_function,
-    op_is_generator_object,
-    op_is_map,
-    op_is_map_iterator,
-    op_is_module_namespace_object,
-    op_is_native_error,
-    op_is_number_object,
-    op_is_promise,
-    op_is_proxy,
-    op_is_reg_exp,
-    op_is_set,
-    op_is_set_iterator,
-    op_is_shared_array_buffer,
-    op_is_string_object,
-    op_is_symbol_object,
-    op_is_typed_array,
-    op_is_weak_map,
-    op_is_weak_set,
-  } = ensureFastOps();
-
   function propWritable(value) {
     return {
       value,
@@ -586,6 +608,17 @@
     writeSync,
     shutdown,
     print: (msg, isErr) => op_print(msg, isErr),
+    setOpCallTracingEnabled: (enabled) => {
+      __setOpCallTracingEnabled(enabled);
+      op_opcall_tracing_enable(enabled);
+    },
+    isOpCallTracingEnabled: () => false,
+    getAllOpCallTraces: () => {
+      const traces = op_opcall_tracing_get_all();
+      return new SafeMap(traces);
+    },
+    getOpCallTraceForPromise: (promise) =>
+      op_opcall_tracing_get(promise[promiseIdSymbol]),
     setMacrotaskCallback,
     setNextTickCallback,
     runMicrotasks: () => op_run_microtasks(),
diff --git a/core/ops_builtin.rs b/core/ops_builtin.rs
index 3f1e11c..74a0de8 100644
--- a/core/ops_builtin.rs
+++ b/core/ops_builtin.rs
@@ -119,7 +119,11 @@ builtin_ops! {
   ops_builtin_v8::op_current_user_call_site,
   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_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
 }
 
 #[op2(fast)]
diff --git a/core/ops_builtin_v8.rs b/core/ops_builtin_v8.rs
index 7965929..08b3a5c 100644
--- a/core/ops_builtin_v8.rs
+++ b/core/ops_builtin_v8.rs
@@ -14,6 +14,7 @@ use crate::source_map::SourceMapApplication;
 use crate::JsBuffer;
 use crate::JsRuntime;
 use crate::OpState;
+use crate::PromiseId;
 use anyhow::Error;
 use serde::Deserialize;
 use serde::Serialize;
@@ -43,6 +44,47 @@ pub fn op_unref_op(scope: &mut v8::HandleScope, promise_id: i32) {
   context_state.unrefed_ops.borrow_mut().insert(promise_id);
 }
 
+#[op2]
+pub fn op_opcall_tracing_enable(scope: &mut v8::HandleScope, enabled: bool) {
+  let context_state = JsRealm::state_from_scope(scope);
+  context_state.opcall_tracing_set_enabled(enabled);
+}
+
+#[op2]
+pub fn op_opcall_tracing_submit(
+  scope: &mut v8::HandleScope,
+  #[smi] promise: PromiseId,
+  #[string] trace: String,
+) {
+  let context_state = JsRealm::state_from_scope(scope);
+  context_state.opcall_tracing_submit_trace(promise, trace);
+}
+
+#[op2]
+#[serde]
+pub fn op_opcall_tracing_get_all(
+  scope: &mut v8::HandleScope,
+) -> Vec<(String, String)> {
+  let context_state = JsRealm::state_from_scope(scope);
+  // This is pretty inefficient, but so is opcall tracing
+  let traces = context_state.opcall_tracing_get_all();
+  let mut out = Vec::with_capacity(traces.len());
+  for (key, value) in traces {
+    out.push((key.to_string(), value));
+  }
+  out
+}
+
+#[op2]
+#[string]
+pub fn op_opcall_tracing_get(
+  scope: &mut v8::HandleScope,
+  #[smi] promise: PromiseId,
+) -> Option<String> {
+  let context_state = JsRealm::state_from_scope(scope);
+  context_state.opcall_tracing_get(promise)
+}
+
 /// Queue a timer, returning a "large" integer in an f64 (allowing up to `MAX_SAFE_INTEGER`
 /// timers to exist).
 #[op2]
diff --git a/core/rebuild_async_stubs.js b/core/rebuild_async_stubs.js
index 4912b0d..37b8bad 100755
--- a/core/rebuild_async_stubs.js
+++ b/core/rebuild_async_stubs.js
@@ -17,14 +17,11 @@ function __TEMPLATE__(__ARGS_PARAM__) {
     ErrorCaptureStackTrace(err, __TEMPLATE__);
     return PromiseReject(err);
   }
+  if (isOpCallTracingEnabled) {
+    submitOpCallTrace(id);
+  }
   nextPromiseId = (id + 1) & 0xffffffff;
-  let promise = PromisePrototypeThen(
-    setPromise(id),
-    unwrapOpError(core_.eventLoopTick),
-  );
-  promise = handleOpCallTracing(opName, id, promise);
-  promise[promiseIdSymbol] = id;
-  return promise;
+  return setPromise(id);
 }
 
 const infraJsPath = new URL("00_infra.js", import.meta.url);
diff --git a/core/runtime/jsrealm.rs b/core/runtime/jsrealm.rs
index 5ca38b5..f0dd138 100644
--- a/core/runtime/jsrealm.rs
+++ b/core/runtime/jsrealm.rs
@@ -12,10 +12,12 @@ use crate::ops::OpCtx;
 use crate::tasks::V8TaskSpawnerFactory;
 use crate::web_timeout::WebTimers;
 use crate::GetErrorClassFn;
+use crate::PromiseId;
 use anyhow::Error;
 use futures::stream::StreamExt;
 use std::cell::Cell;
 use std::cell::RefCell;
+use std::collections::HashMap;
 use std::collections::HashSet;
 use std::hash::BuildHasherDefault;
 use std::hash::Hasher;
@@ -56,6 +58,8 @@ 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_tracing_enabled: Cell<bool>,
+  pub(crate) opcall_traces: RefCell<HashMap<i32, String>>,
   pub(crate) pending_ops: Rc<OpDriverImpl>,
   // We don't explicitly re-read this prop but need the slice to live alongside
   // the context
@@ -81,6 +85,8 @@ impl ContextState {
       js_event_loop_tick_cb: Default::default(),
       js_wasm_streaming_cb: Default::default(),
       wasm_instantiate_fn: Default::default(),
+      opcall_traces: Default::default(),
+      opcall_tracing_enabled: Default::default(),
       op_ctxs,
       pending_ops: op_driver,
       task_spawner_factory: Default::default(),
@@ -88,6 +94,29 @@ impl ContextState {
       unrefed_ops: Default::default(),
     }
   }
+
+  pub fn opcall_tracing_set_enabled(&self, enabled: bool) {
+    self.opcall_tracing_enabled.set(enabled);
+    if !enabled {
+      self.opcall_traces.borrow_mut().clear();
+    }
+  }
+
+  pub fn opcall_tracing_submit_trace(
+    &self,
+    promise_id: PromiseId,
+    trace: String,
+  ) {
+    self.opcall_traces.borrow_mut().insert(promise_id, trace);
+  }
+
+  pub fn opcall_tracing_get_all(&self) -> HashMap<PromiseId, String> {
+    self.opcall_traces.borrow().clone()
+  }
+
+  pub fn opcall_tracing_get(&self, promise_id: PromiseId) -> Option<String> {
+    self.opcall_traces.borrow().get(&promise_id).cloned()
+  }
 }
 
 /// A representation of a JavaScript realm tied to a [`JsRuntime`], that allows
diff --git a/core/runtime/jsruntime.rs b/core/runtime/jsruntime.rs
index 500493c..65cb33d 100644
--- a/core/runtime/jsruntime.rs
+++ b/core/runtime/jsruntime.rs
@@ -2132,6 +2132,7 @@ impl JsRuntime {
       }
 
       context_state.unrefed_ops.borrow_mut().remove(&promise_id);
+      context_state.opcall_traces.borrow_mut().remove(&promise_id);
       dispatched_ops |= true;
       args.push(v8::Integer::new(scope, promise_id).into());
       args.push(res.unwrap_or_else(std::convert::identity));
diff --git a/core/runtime/snapshot_util.rs b/core/runtime/snapshot_util.rs
index 23e4a25..3af5c5f 100644
--- a/core/runtime/snapshot_util.rs
+++ b/core/runtime/snapshot_util.rs
@@ -33,7 +33,10 @@ impl SnapshotLoadDataStore {
     v8::Local<'s, T>: TryFrom<v8::Local<'s, v8::Data>>,
   {
     let Some(data) = self.data.get_mut(id as usize) else {
-      panic!("Attempted to read snapshot data out of range: {id}");
+      panic!(
+        "Attempted to read snapshot data out of range: {id} (of {})",
+        self.data.len()
+      );
     };
     let Some(data) = data.take() else {
       panic!("Attempted to read the snapshot data at index {id} twice");
@@ -305,12 +308,13 @@ pub(crate) fn set_snapshotted_data(
 ) {
   let local_context = v8::Local::new(scope, context);
 
+  let js_handled_promise_rejection_cb = snapshotted_data
+    .js_handled_promise_rejection_cb
+    .map(|v| data_store.register(v));
   let raw_snapshot_data = RawSnapshottedData {
     data_count: data_store.data.len() as _,
     module_map_data: snapshotted_data.module_map_data,
-    js_handled_promise_rejection_cb: snapshotted_data
-      .js_handled_promise_rejection_cb
-      .map(|v| data_store.register(v)),
+    js_handled_promise_rejection_cb,
   };
 
   #[cfg(all(


Test patch:
diff --git a/testing/unit/ops_async_test.ts b/testing/unit/ops_async_test.ts
index c49701f..453e4ce 100644
--- a/testing/unit/ops_async_test.ts
+++ b/testing/unit/ops_async_test.ts
@@ -78,7 +78,6 @@ test(async function testAsyncOpCallTrace() {
     assertStackTraceEquals(
       Deno.core.getOpCallTraceForPromise(p1),
       `
-      at handleOpCallTracing (ext:core/00_infra.js:line:col)
       at op_async_barrier_await (ext:core/00_infra.js:line:col)
       at barrierAwait (checkin:async:line:col)
       at testAsyncOpCallTrace (test:///unit/ops_async_test.ts:line:col)