Repository: denoland/deno_core
Pull request #468 (2024-01-23 19:00:09)

feat: expose op diffs to sanitizer interface


Patch:
diff --git a/core/lib.rs b/core/lib.rs
index 298b162..7dfa21f 100644
--- a/core/lib.rs
+++ b/core/lib.rs
@@ -131,6 +131,7 @@ pub use crate::resources::ResourceHandleFd;
 pub use crate::resources::ResourceHandleSocket;
 pub use crate::resources::ResourceId;
 pub use crate::resources::ResourceTable;
+pub use crate::runtime::stats;
 pub use crate::runtime::CompiledWasmModuleStore;
 pub use crate::runtime::CreateRealmOptions;
 pub use crate::runtime::CrossIsolateStore;
diff --git a/core/resources.rs b/core/resources.rs
index 13d0d3a..de3d8f0 100644
--- a/core/resources.rs
+++ b/core/resources.rs
@@ -357,6 +357,17 @@ pub struct ResourceTable {
 }
 
 impl ResourceTable {
+  /// Returns the number of resources currently active in the resource table.
+  /// Resources taken from the table do not contribute to this count.
+  pub fn len(&self) -> usize {
+    self.index.len()
+  }
+
+  /// Returns whether this table is empty.
+  pub fn is_empty(&self) -> bool {
+    self.index.is_empty()
+  }
+
   /// Inserts resource into the resource table, which takes ownership of it.
   ///
   /// The resource type is erased at runtime and must be statically known
diff --git a/core/runtime/bindings.rs b/core/runtime/bindings.rs
index de5cb89..1227565 100644
--- a/core/runtime/bindings.rs
+++ b/core/runtime/bindings.rs
@@ -2,6 +2,8 @@
 use log::debug;
 use std::option::Option;
 use std::os::raw::c_void;
+use std::path::PathBuf;
+use url::Url;
 use v8::MapFnTo;
 
 use super::jsruntime::CONTEXT_SETUP_SOURCES;
@@ -455,6 +457,51 @@ pub extern "C" fn host_initialize_import_meta_object_callback(
     v8::String::new_external_onebyte_static(scope, v8_static_strings::RESOLVE)
       .unwrap();
   meta.set(scope, resolve_key.into(), val.into());
+
+  maybe_add_import_meta_filename_dirname(scope, meta, &name);
+}
+
+fn maybe_add_import_meta_filename_dirname(
+  scope: &mut v8::HandleScope,
+  meta: v8::Local<v8::Object>,
+  name: &str,
+) {
+  // For `file:` URL we provide additional `filename` and `dirname` values
+  let Ok(name_url) = Url::parse(name) else {
+    return;
+  };
+
+  if name_url.scheme() != "file" {
+    return;
+  }
+
+  // If something goes wrong acquiring a filepath, let skip instead of crashing
+  // (mostly concerned about file paths on Windows).
+  let Ok(file_path) = name_url.to_file_path() else {
+    return;
+  };
+
+  // Use display() here so that Rust takes care of proper forward/backward slash
+  // formatting depending on the OS.
+  let escaped_filename = file_path.display().to_string();
+  let Some(filename_val) = v8::String::new(scope, &escaped_filename) else {
+    return;
+  };
+  let filename_key =
+    v8::String::new_external_onebyte_static(scope, b"filename").unwrap();
+  meta.create_data_property(scope, filename_key.into(), filename_val.into());
+
+  let dir_path = file_path
+    .parent()
+    .map(|p| p.to_owned())
+    .unwrap_or_else(|| PathBuf::from("/"));
+  let escaped_dirname = dir_path.display().to_string();
+  let Some(dirname_val) = v8::String::new(scope, &escaped_dirname) else {
+    return;
+  };
+  let dirname_key =
+    v8::String::new_external_onebyte_static(scope, b"dirname").unwrap();
+  meta.create_data_property(scope, dirname_key.into(), dirname_val.into());
 }
 
 fn import_meta_resolve(
diff --git a/core/runtime/jsruntime.rs b/core/runtime/jsruntime.rs
index 7b7f39d..1eb0b1a 100644
--- a/core/runtime/jsruntime.rs
+++ b/core/runtime/jsruntime.rs
@@ -5,8 +5,8 @@ use super::bindings::watch_promise;
 use super::exception_state::ExceptionState;
 use super::jsrealm::JsRealmInner;
 use super::op_driver::OpDriver;
-use super::op_driver::OpInflightStats;
 use super::snapshot_util;
+use super::stats::RuntimeActivityStatsFactory;
 use super::SnapshottedData;
 use crate::error::exception_to_err_result;
 use crate::error::AnyError;
@@ -79,14 +79,6 @@ use v8::Isolate;
 pub type WaitForInspectorDisconnectCallback = Box<dyn Fn()>;
 const STATE_DATA_OFFSET: u32 = 0;
 
-/// Information about in-flight ops, open resources, active timers and other runtime-specific
-/// data that can be used for test sanitization.
-pub struct RuntimeActivityStats {
-  /// This will be exposed in follow-up work.
-  #[allow(dead_code)]
-  op: OpInflightStats,
-}
-
 pub enum Snapshot {
   Static(&'static [u8]),
   JustCreated(v8::StartupData),
@@ -1001,9 +993,10 @@ impl JsRuntime {
     }
   }
 
-  pub fn inflight_stats(&self) -> RuntimeActivityStats {
-    RuntimeActivityStats {
-      op: self.inner.main_realm.0.context_state.pending_ops.stats(),
+  pub fn runtime_activity_stats_factory(&self) -> RuntimeActivityStatsFactory {
+    RuntimeActivityStatsFactory {
+      context_state: self.inner.main_realm.0.context_state.clone(),
+      op_state: self.inner.state.op_state.clone(),
     }
   }
 
diff --git a/core/runtime/mod.rs b/core/runtime/mod.rs
index 9560b5c..3c5cbae 100644
--- a/core/runtime/mod.rs
+++ b/core/runtime/mod.rs
@@ -8,6 +8,7 @@ pub mod op_driver;
 pub mod ops;
 pub mod ops_rust_to_v8;
 mod snapshot_util;
+pub mod stats;
 
 #[cfg(all(test, not(miri)))]
 mod tests;
diff --git a/core/runtime/op_driver/futures_unordered_driver.rs b/core/runtime/op_driver/futures_unordered_driver.rs
index a91595e..3b7601b 100644
--- a/core/runtime/op_driver/futures_unordered_driver.rs
+++ b/core/runtime/op_driver/futures_unordered_driver.rs
@@ -236,7 +236,6 @@ impl<C: OpMappingContext> OpDriver<C> for FuturesUnorderedDriver<C> {
       v.push(f.context())
     }
     OpInflightStats {
-      supported: true,
       ops: v.into_boxed_slice(),
     }
   }
diff --git a/core/runtime/op_driver/mod.rs b/core/runtime/op_driver/mod.rs
index 3e149c5..5422d9d 100644
--- a/core/runtime/op_driver/mod.rs
+++ b/core/runtime/op_driver/mod.rs
@@ -24,12 +24,10 @@ pub use self::op_results::V8OpMappingContext;
 pub use self::op_results::V8RetValMapper;
 
 #[derive(Default)]
-#[allow(dead_code)] // follow-up PR
 /// Returns a set of stats on inflight ops.
 pub struct OpInflightStats {
   /// The [`PromiseId`] of any inflight ops by each [`OpId`].
-  ops: Box<[PendingOpInfo]>,
-  supported: bool,
+  pub(super) ops: Box<[PendingOpInfo]>,
 }
 
 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
diff --git a/core/runtime/stats.rs b/core/runtime/stats.rs
new file mode 100644
index 0000000..4b4e834
--- /dev/null
+++ b/core/runtime/stats.rs
@@ -0,0 +1,156 @@
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+use super::op_driver::OpDriver;
+use super::op_driver::OpInflightStats;
+use super::ContextState;
+use crate::OpState;
+use crate::PromiseId;
+use crate::ResourceId;
+use bit_set::BitSet;
+use serde::Serialize;
+use std::cell::RefCell;
+use std::rc::Rc;
+
+#[derive(Clone)]
+pub struct RuntimeActivityStatsFactory {
+  pub(super) context_state: Rc<ContextState>,
+  pub(super) op_state: Rc<RefCell<OpState>>,
+}
+
+impl RuntimeActivityStatsFactory {
+  pub fn capture(self) -> RuntimeActivityStats {
+    let res = &self.op_state.borrow().resource_table;
+    let mut resources = ResourceOpenStats {
+      resources: Vec::with_capacity(res.len()),
+    };
+    for resource in res.names() {
+      resources
+        .resources
+        .push((resource.0, resource.1.to_string()))
+    }
+    let timers = TimerStats {
+      intervals: vec![],
+      timers: vec![],
+    };
+    RuntimeActivityStats {
+      context_state: self.context_state.clone(),
+      op: self.context_state.pending_ops.stats(),
+      resources,
+      timers,
+    }
+  }
+}
+
+pub struct ResourceOpenStats {
+  pub(super) resources: Vec<(u32, String)>,
+}
+
+pub struct TimerStats {
+  #[allow(dead_code)] // coming soon
+  pub(super) timers: Vec<usize>,
+  #[allow(dead_code)] // coming soon
+  pub(super) intervals: Vec<usize>,
+}
+
+/// Information about in-flight ops, open resources, active timers and other runtime-specific
+/// data that can be used for test sanitization.
+pub struct RuntimeActivityStats {
+  context_state: Rc<ContextState>,
+  pub(super) op: OpInflightStats,
+  pub(super) resources: ResourceOpenStats,
+  #[allow(dead_code)] // coming soon
+  pub(super) timers: TimerStats,
+}
+
+#[derive(Serialize)]
+pub enum RuntimeActivity {
+  AsyncOp(PromiseId, String),
+  Resource(ResourceId, String),
+  Timer(usize, usize),
+  Interval(usize, usize),
+}
+
+impl RuntimeActivityStats {
+  /// Capture the data within this [`RuntimeActivityStats`] as a [`RuntimeActivitySnapshot`]
+  /// with details of activity.
+  pub fn dump(&self) -> RuntimeActivitySnapshot {
+    let mut v =
+      Vec::with_capacity(self.op.ops.len() + self.resources.resources.len());
+    let ops = self.context_state.op_ctxs.borrow();
+    for op in self.op.ops.iter() {
+      v.push(RuntimeActivity::AsyncOp(
+        op.0,
+        ops[op.1 as usize].decl.name.to_owned(),
+      ));
+    }
+    for resource in self.resources.resources.iter() {
+      v.push(RuntimeActivity::Resource(resource.0, resource.1.clone()))
+    }
+    RuntimeActivitySnapshot { active: v }
+  }
+
+  pub fn diff(before: &Self, after: &Self) -> RuntimeActivityDiff {
+    let mut appeared = vec![];
+    let mut disappeared = vec![];
+    let ops = before.context_state.op_ctxs.borrow();
+
+    let mut a = BitSet::new();
+    for op in after.op.ops.iter() {
+      a.insert(op.0 as usize);
+    }
+    for op in before.op.ops.iter() {
+      if a.remove(op.0 as usize) {
+        // continuing op
+      } else {
+        // before, but not after
+        disappeared.push(RuntimeActivity::AsyncOp(
+          op.0,
+          ops[op.1 as usize].decl.name.to_owned(),
+        ));
+      }
+    }
+    for op in after.op.ops.iter() {
+      if a.contains(op.0 as usize) {
+        // after but not before
+        appeared.push(RuntimeActivity::AsyncOp(
+          op.0,
+          ops[op.1 as usize].decl.name.to_owned(),
+        ));
+      }
+    }
+
+    let mut a = BitSet::new();
+    for op in after.resources.resources.iter() {
+      a.insert(op.0 as usize);
+    }
+    for op in before.resources.resources.iter() {
+      if a.remove(op.0 as usize) {
+        // continuing op
+      } else {
+        // before, but not after
+        disappeared.push(RuntimeActivity::Resource(op.0, 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()));
+      }
+    }
+
+    RuntimeActivityDiff {
+      appeared,
+      disappeared,
+    }
+  }
+}
+
+#[derive(Serialize)]
+pub struct RuntimeActivityDiff {
+  pub appeared: Vec<RuntimeActivity>,
+  pub disappeared: Vec<RuntimeActivity>,
+}
+
+#[derive(Serialize)]
+pub struct RuntimeActivitySnapshot {
+  pub active: Vec<RuntimeActivity>,
+}


Test patch:
diff --git a/core/modules/tests.rs b/core/modules/tests.rs
index 8c532c0..bbe0a70 100644
--- a/core/modules/tests.rs
+++ b/core/modules/tests.rs
@@ -1673,3 +1673,39 @@ if (typeof internals === "undefined") throw new Error("core missing");
   futures::executor::block_on(runtime.run_event_loop(Default::default()))
     .unwrap();
 }
+
+#[test]
+fn import_meta_filename_dirname() {
+  #[cfg(not(target_os = "windows"))]
+  let main_specifier = resolve_url("file:///main_module.js").unwrap();
+  #[cfg(not(target_os = "windows"))]
+  let code = ascii_str!(
+    r#"if (import.meta.filename != '/main_module.js') throw Error();
+    if (import.meta.dirname != '/') throw Error();"#
+  );
+
+  #[cfg(target_os = "windows")]
+  let main_specifier = resolve_url("file:///C:/main_module.js").unwrap();
+  #[cfg(target_os = "windows")]
+  let code = ascii_str!(
+    r#"if (import.meta.filename != 'C:\\main_module.js') throw Error();
+    if (import.meta.dirname != 'C:\\') throw Error();"#
+  );
+
+  let loader = StaticModuleLoader::new([(main_specifier.clone(), code)]);
+
+  let mut runtime = JsRuntime::new(RuntimeOptions {
+    module_loader: Some(Rc::new(loader)),
+    ..Default::default()
+  });
+
+  let main_id_fut = runtime
+    .load_main_module(&main_specifier, None)
+    .boxed_local();
+  let main_id = futures::executor::block_on(main_id_fut).unwrap();
+
+  #[allow(clippy::let_underscore_future)]
+  let _ = runtime.mod_evaluate(main_id);
+  futures::executor::block_on(runtime.run_event_loop(Default::default()))
+    .unwrap();
+}
diff --git a/testing/checkin/runner/mod.rs b/testing/checkin/runner/mod.rs
index 0a59e0e..aafb13b 100644
--- a/testing/checkin/runner/mod.rs
+++ b/testing/checkin/runner/mod.rs
@@ -30,6 +30,10 @@ deno_core::extension!(
     ops::op_log_debug,
     ops::op_log_info,
     ops::op_test_register,
+    ops::op_stats_capture,
+    ops::op_stats_diff,
+    ops::op_stats_dump,
+    ops::op_stats_delete,
     ops_async::op_async_yield,
     ops_async::op_async_barrier_create,
     ops_async::op_async_barrier_await,
@@ -42,6 +46,7 @@ deno_core::extension!(
   esm_entry_point = "ext:checkin_runtime/__init.js",
   esm = [
     dir "checkin/runtime",
+    "__bootstrap.js",
     "__init.js",
     "async.ts" with_specifier "checkin:async",
     "console.ts" with_specifier "checkin:console",
@@ -68,7 +73,7 @@ fn create_runtime() -> JsRuntime {
     }
   }
 
-  JsRuntime::new(RuntimeOptions {
+  let mut runtime = JsRuntime::new(RuntimeOptions {
     extensions,
     module_loader: Some(Rc::new(
       ts_module_loader::TypescriptModuleLoader::default(),
@@ -78,7 +83,10 @@ fn create_runtime() -> JsRuntime {
     }),
     shared_array_buffer_store: Some(CrossIsolateStore::default()),
     ..Default::default()
-  })
+  });
+  let stats = runtime.runtime_activity_stats_factory();
+  runtime.op_state().borrow_mut().put(stats);
+  runtime
 }
 
 /// Run a integration test within the `checkin` runtime. This executes a single file, imports and all,
diff --git a/testing/checkin/runner/ops.rs b/testing/checkin/runner/ops.rs
index c031e05..b423752 100644
--- a/testing/checkin/runner/ops.rs
+++ b/testing/checkin/runner/ops.rs
@@ -1,8 +1,17 @@
 // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+use std::cell::RefCell;
+use std::rc::Rc;
+
 use deno_core::op2;
+use deno_core::stats::RuntimeActivityDiff;
+use deno_core::stats::RuntimeActivitySnapshot;
+use deno_core::stats::RuntimeActivityStats;
+use deno_core::stats::RuntimeActivityStatsFactory;
 use deno_core::v8;
+use deno_core::OpState;
 
 use super::testing::Output;
+use super::testing::TestData;
 use super::testing::TestFunctions;
 
 #[op2(fast)]
@@ -24,3 +33,45 @@ pub fn op_test_register(
 ) {
   tests.functions.push((name, f));
 }
+
+#[op2(fast)]
+pub fn op_stats_capture(#[string] name: String, state: Rc<RefCell<OpState>>) {
+  let stats = state
+    .borrow()
+    .borrow::<RuntimeActivityStatsFactory>()
+    .clone();
+  let data = stats.capture();
+  let mut state = state.borrow_mut();
+  let test_data = state.borrow_mut::<TestData>();
+  test_data.insert(name, data);
+}
+
+#[op2]
+#[serde]
+pub fn op_stats_dump(
+  #[string] name: String,
+  #[state] test_data: &mut TestData,
+) -> RuntimeActivitySnapshot {
+  let stats = test_data.get::<RuntimeActivityStats>(name);
+  stats.dump()
+}
+
+#[op2]
+#[serde]
+pub fn op_stats_diff(
+  #[string] before: String,
+  #[string] after: String,
+  #[state] test_data: &mut TestData,
+) -> RuntimeActivityDiff {
+  let before = test_data.get::<RuntimeActivityStats>(before);
+  let after = test_data.get::<RuntimeActivityStats>(after);
+  RuntimeActivityStats::diff(before, after)
+}
+
+#[op2(fast)]
+pub fn op_stats_delete(
+  #[string] name: String,
+  #[state] test_data: &mut TestData,
+) {
+  test_data.take::<RuntimeActivityStats>(name);
+}
diff --git a/testing/checkin/runner/testing.rs b/testing/checkin/runner/testing.rs
index f3d6956..29640f5 100644
--- a/testing/checkin/runner/testing.rs
+++ b/testing/checkin/runner/testing.rs
@@ -26,11 +26,27 @@ impl TestData {
   }
 
   pub fn get<T: 'static + Any>(&self, name: String) -> &T {
+    let key = (name, TypeId::of::<T>());
     self
       .data
-      .get(&(name, TypeId::of::<T>()))
-      .unwrap()
+      .get(&key)
+      .unwrap_or_else(|| {
+        panic!(
+          "Unable to locate {} of type {}",
+          key.0,
+          std::any::type_name::<T>()
+        )
+      })
       .downcast_ref()
       .unwrap()
   }
+
+  pub fn take<T: 'static + Any>(&mut self, name: String) -> T {
+    *self
+      .data
+      .remove(&(name, TypeId::of::<T>()))
+      .unwrap()
+      .downcast()
+      .unwrap()
+  }
 }
diff --git a/testing/checkin/runtime/__bootstrap.js b/testing/checkin/runtime/__bootstrap.js
new file mode 100644
index 0000000..23195e3
--- /dev/null
+++ b/testing/checkin/runtime/__bootstrap.js
@@ -0,0 +1,5 @@
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+
+// Allow 'using' to work in TypeScript
+Symbol.dispose = Symbol("Symbol.dispose");
+Symbol.asyncDispose = Symbol("Symbol.asyncDispose");
diff --git a/testing/checkin/runtime/__init.js b/testing/checkin/runtime/__init.js
index 20ae054..0e60401 100644
--- a/testing/checkin/runtime/__init.js
+++ b/testing/checkin/runtime/__init.js
@@ -1,4 +1,5 @@
 // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+import "ext:checkin_runtime/__bootstrap.js";
 import * as async from "checkin:async";
 import * as testing from "checkin:testing";
 import * as console from "checkin:console";
diff --git a/testing/checkin/runtime/async.ts b/testing/checkin/runtime/async.ts
index aa777c7..e17be2b 100644
--- a/testing/checkin/runtime/async.ts
+++ b/testing/checkin/runtime/async.ts
@@ -6,6 +6,10 @@ const {
   op_async_throw_error_eager,
   op_async_throw_error_lazy,
   op_async_throw_error_deferred,
+  op_stats_capture,
+  op_stats_diff,
+  op_stats_dump,
+  op_stats_delete,
 } = Deno
   .core
   .ensureFastOps();
@@ -30,3 +34,72 @@ export async function barrierAwait(name: string) {
 export async function asyncYield() {
   await op_async_yield();
 }
+
+let nextStats = 0;
+
+export class Stats {
+  constructor(public name: string) {
+    op_stats_capture(this.name);
+  }
+
+  dump(): StatsCollection {
+    return new StatsCollection(op_stats_dump(this.name).active);
+  }
+
+  [Symbol.dispose]() {
+    op_stats_delete(this.name);
+  }
+}
+
+export class StatsDiff {
+  #appeared;
+  #disappeared;
+
+  // deno-lint-ignore no-explicit-any
+  constructor(private diff: any) {
+    this.#appeared = new StatsCollection(this.diff.appeared);
+    this.#disappeared = new StatsCollection(this.diff.disappeared);
+  }
+
+  get empty(): boolean {
+    return this.#appeared.empty && this.#disappeared.empty;
+  }
+
+  get appeared(): StatsCollection {
+    return this.#appeared;
+  }
+
+  get disappeared(): StatsCollection {
+    return this.#disappeared;
+  }
+}
+
+export class StatsCollection {
+  // deno-lint-ignore no-explicit-any
+  constructor(private data: any[]) {
+  }
+
+  countOps(): number {
+    let count = 0;
+    for (const item of this.data) {
+      if ("AsyncOp" in item) {
+        count++;
+      }
+    }
+    return count;
+  }
+
+  get empty(): boolean {
+    return this.data.length == 0;
+  }
+}
+
+export class StatsFactory {
+  static capture(): Stats {
+    return new Stats(`stats-${nextStats++}`);
+  }
+
+  static diff(before: Stats, after: Stats): StatsDiff {
+    return new StatsDiff(op_stats_diff(before.name, after.name));
+  }
+}
diff --git a/testing/lib.rs b/testing/lib.rs
index ff2bd5e..69c0873 100644
--- a/testing/lib.rs
+++ b/testing/lib.rs
@@ -38,6 +38,7 @@ unit_test!(
   ops_async_test,
   ops_buffer_test,
   serialize_deserialize_test,
+  stats_test,
   tc39_test,
   timer_test,
   type_test,
diff --git a/testing/unit/stats_test.ts b/testing/unit/stats_test.ts
new file mode 100644
index 0000000..d8919b3
--- /dev/null
+++ b/testing/unit/stats_test.ts
@@ -0,0 +1,24 @@
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+import { barrierAwait, barrierCreate, StatsFactory } from "checkin:async";
+import { assert, assertEquals, test } from "checkin:testing";
+
+test(async function testStatsOps() {
+  using statsBefore = StatsFactory.capture();
+  assert(statsBefore.dump().empty);
+
+  barrierCreate("barrier", 3);
+  const promise1 = barrierAwait("barrier");
+  assertEquals(1, StatsFactory.capture().dump().countOps());
+  const promise2 = barrierAwait("barrier");
+  assertEquals(2, StatsFactory.capture().dump().countOps());
+  using statsMiddle = StatsFactory.capture();
+  const diffMiddle = StatsFactory.diff(statsBefore, statsMiddle);
+  assertEquals(0, diffMiddle.disappeared.countOps());
+  assertEquals(2, diffMiddle.appeared.countOps());
+
+  await Promise.all([promise1, promise2, barrierAwait("barrier")]);
+
+  using statsAfter = StatsFactory.capture();
+  const diff = StatsFactory.diff(statsBefore, statsAfter);
+  assert(diff.empty);
+});