diff --git a/.github/openclaw/check-stack-visitor.py b/.github/openclaw/check-stack-visitor.py new file mode 100644 index 0000000000000..19ac16b5b57ec --- /dev/null +++ b/.github/openclaw/check-stack-visitor.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +"""Pin the live stack visitor to the same call tokens as captured stacks.""" +import json +import re +import subprocess +import sys +from pathlib import Path + +fixture = Path(__file__).with_name("qualification") / "stack-visitor.js" +expected = [(1, 22), (2, 21), (3, 1)] +for mode, flags in [("default", []), ("interpreter", ["--useJIT=false"])]: + result = subprocess.run([sys.argv[1], *flags, str(fixture)], capture_output=True, text=True, check=True) + actual = [(int(line), int(column)) for line, column in re.findall(r"stack-visitor\.js:(\d+):(\d+)", result.stderr)] + if actual != expected: + raise SystemExit(f"{mode}: expected {expected}, got {actual}\n{result.stderr}") + print(json.dumps({"mode": mode, "positions": actual, "passed": True})) diff --git a/.github/openclaw/qualification/inspection-idle.test.ts b/.github/openclaw/qualification/inspection-idle.test.ts new file mode 100644 index 0000000000000..6723a9e280083 --- /dev/null +++ b/.github/openclaw/qualification/inspection-idle.test.ts @@ -0,0 +1,119 @@ +import { expect, test } from "bun:test"; +import { Worker } from "node:worker_threads"; + +test("worker inspection preserves idle purge and atomic notifications", async () => { + const shared = new SharedArrayBuffer(16); + const flag = new Int32Array(shared); + const messages: any[] = []; + let failure: Error | undefined; + const worker = new Worker(` + const { parentPort, workerData } = require("node:worker_threads"); + const { heapStats } = require("bun:jsc"); + const flag = new Int32Array(workerData); + const held = new Uint8Array(16 * 1024 * 1024).fill(7); + const purges = () => heapStats().mimalloc.purge_calls; + function allocate() { + let strings = []; + for (let i = 0; i < 100000; i++) strings.push(Buffer.alloc(900 + (i % 5) * 8, 97).toString("latin1")); + strings = strings.filter((_, i) => i % 64 === 0); + Bun.gc(true); + return strings; + } + let strings = allocate(); + let before = purges(); + for (let stable = 0, tries = 0; stable < 3 && tries < 50; tries++) { + const start = performance.now(); + while (performance.now() - start < 60); + const now = purges(); + stable = now === before ? stable + 1 : 0; + before = now; + } + Atomics.store(flag, 1, 1); + let released = 0; + for (let i = 0; i < 10 && released < 500; i++) { + if (Atomics.wait(flag, 0, 0, 250) !== "timed-out") throw new Error("unexpected purge wait result"); + released = purges() - before; + } + parentPort.postMessage({ kind: "purge", released, retained: strings.length }); + Atomics.wait(flag, 3, 0, 10000); + const waits = []; + for (let i = 0; i < 8; i++) { + strings = allocate(); + Atomics.store(flag, 0, 0); + Atomics.store(flag, 1, i + 2); + const start = performance.now(); + const result = Atomics.wait(flag, 0, 0, 10000); + waits.push({ result, elapsed: performance.now() - start }); + } + parentPort.postMessage({ kind: "done", waits, bytes: held.byteLength, retained: strings.length }); + // Keep the VM alive until every requested inspection has settled. + Atomics.wait(flag, 2, 0, 10000); + `, { eval: true, workerData: shared }); + worker.on("message", message => messages.push(message)); + worker.on("error", error => { failure = error; }); + const deadline = performance.now() + 25_000; + async function waitFor(read: () => T): Promise> { + for (;;) { + if (failure) throw failure; + const result = read(); + if (result) return result as NonNullable; + if (performance.now() >= deadline) throw new Error("worker inspection/notification stalled"); + await Bun.sleep(1); + } + } + function checkStatistics(stats: { external_memory: number; used_heap_size: number; total_heap_size: number }) { + expect(stats.external_memory).toBeGreaterThanOrEqual(16 * 1024 * 1024); + expect(stats.total_heap_size).toBeGreaterThanOrEqual(stats.used_heap_size); + } + async function inspect() { + let timer: ReturnType | undefined; + try { + return await Promise.race([ + worker.getHeapStatistics(), + new Promise((_, reject) => { + timer = setTimeout(() => reject(new Error("worker inspection did not settle")), 5000); + }), + ]); + } finally { + clearTimeout(timer); + } + } + let inspections = 0; + try { + await waitFor(() => Atomics.load(flag, 1) === 1); + for (let round = 0; round < 4; round++) { + await Bun.sleep(round === 0 ? 80 : 60); + const rows = await Promise.all(Array.from({ length: 8 }, () => inspect())); + rows.forEach(checkStatistics); + inspections += rows.length; + } + const purge = await waitFor(() => messages.find(message => message.kind === "purge")); + expect(purge.released).toBeGreaterThanOrEqual(500); + expect(purge.retained).toBe(1563); + Atomics.store(flag, 3, 1); + Atomics.notify(flag, 3); + for (let i = 0; i < 8; i++) { + await waitFor(() => Atomics.load(flag, 1) === i + 2); + await Bun.sleep(100 + i); + const pending = inspect(); + Atomics.store(flag, 0, 1); + Atomics.notify(flag, 0); + checkStatistics(await pending); + inspections++; + } + const done = await waitFor(() => messages.find(message => message.kind === "done")); + expect(done.waits).toHaveLength(8); + for (const wait of done.waits) { + expect(["ok", "not-equal"]).toContain(wait.result); + expect(wait.elapsed).toBeLessThan(5000); + } + expect(done.bytes).toBe(16 * 1024 * 1024); + console.log(JSON.stringify({ purges: purge.released, inspections, notificationRaces: done.waits.length })); + } finally { + Atomics.store(flag, 3, 1); + Atomics.notify(flag, 3); + Atomics.store(flag, 2, 1); + Atomics.notify(flag, 2); + await worker.terminate(); + } +}, 30_000); diff --git a/.github/openclaw/qualification/patches/009-stack-positions.patch b/.github/openclaw/qualification/patches/009-stack-positions.patch new file mode 100644 index 0000000000000..cb44adcf99074 --- /dev/null +++ b/.github/openclaw/qualification/patches/009-stack-positions.patch @@ -0,0 +1,1342 @@ +diff --git a/CHANGELOG.md b/CHANGELOG.md +index e6e2a21a..5a296061 100644 +--- a/CHANGELOG.md ++++ b/CHANGELOG.md +@@ -2,6 +2,8 @@ + + ## Unreleased + ++- Preserve built-in error constructor syntax and returning arrow frames, and use JSC syntax-selected call and property-read stack positions. Retain runtime callee parentheses and computed access, and map call, bracket, and template delimiters back to their original source. Continues the source-position fixes from [oven-sh/bun#35179](https://github.com/oven-sh/bun/pull/35179), [oven-sh/bun#37396](https://github.com/oven-sh/bun/pull/37396), and [oven-sh/bun#41580](https://github.com/oven-sh/bun/pull/41580). Thanks @robobun! ++ + - Align WebKit and VM source-position assertions with Node's constructor locations and select these regressions for native stack-formatting changes in both fork CI lanes. + + - Preserve the native-context count in the fast `v8.getHeapStatistics()` adapter and avoid appending a second strict code-generation flag to inherited worker arguments. +diff --git a/docs/runtime/nodejs-compat.mdx b/docs/runtime/nodejs-compat.mdx +index 0274c5b4..e94321aa 100644 +--- a/docs/runtime/nodejs-compat.mdx ++++ b/docs/runtime/nodejs-compat.mdx +@@ -19,7 +19,7 @@ The OpenClaw fork reports missing runtime imports without downloading npm packag + + `Error.prepareStackTrace` supports deletion, descriptor restoration, and accessor replacement. Nested stack formatting and throwing formatters leave later captures usable. + +-`CallSite.getLineNumber()` and `getColumnNumber()` return one-based positions, or `null` when a frame has no source position. Stack strings include column 1 and use the same constructor positions as CallSites, pointing to the `new` keyword, including across line breaks and in `node:vm` scripts. Some expression positions still differ from Node.js because JavaScriptCore and Bun's transpiler select different syntax locations. ++`CallSite.getLineNumber()` and `getColumnNumber()` return one-based positions, or `null` when a frame has no source position. Stack strings include column 1 and use the same constructor positions as CallSites, pointing to the `new` keyword, including across line breaks and in `node:vm` scripts. The matching OpenClaw engine supplies syntax-selected positions for calls, constructors, property reads, and async continuations. Built-in error constructors retain `new`, including in returning arrows. Runtime transpilation retains callee parentheses and computed access and maps opening call, bracket, and template delimiters to the original source. File-name and eval-origin formatting still follow Bun's existing conventions. + + ### [`node:assert`](https://nodejs.org/api/assert.html) + +diff --git a/src/ast/e.rs b/src/ast/e.rs +index 520c12f6..6db08699 100644 +--- a/src/ast/e.rs ++++ b/src/ast/e.rs +@@ -259,7 +259,9 @@ pub struct Call { + pub args: ExprNodeList, + pub optional_chain: Option, + pub is_direct_eval: bool, ++ pub open_paren_loc: crate::Loc, + pub close_paren_loc: crate::Loc, ++ pub preserve_parenthesized_target: bool, + + /// True if there is a comment containing "@__PURE__" or "#__PURE__" preceding + /// this call expression. This is an annotation used for tree shaking, and +@@ -281,7 +283,9 @@ impl Default for Call { + args: bun_alloc::AstAlloc::vec(), + optional_chain: None, + is_direct_eval: false, ++ open_paren_loc: crate::Loc::EMPTY, + close_paren_loc: crate::Loc::EMPTY, ++ preserve_parenthesized_target: false, + can_be_unwrapped_if_unused: CallUnwrap::Never, + was_jsx_element: false, + } +@@ -334,6 +338,7 @@ impl Default for Dot { + } + } + pub struct Index { ++ pub open_bracket_loc: crate::Loc, + pub index: ExprNodeIndex, + pub target: ExprNodeIndex, + pub optional_chain: Option, +@@ -2114,6 +2119,7 @@ pub struct TemplatePart { + } + + pub struct Template { ++ pub head_loc: crate::Loc, + pub tag: Option, + /// Arena-owned mutable slice. Stored as a + /// `StoreSlice` so writers (`substitute_single_use_symbol_in_expr`, the +@@ -2335,6 +2341,7 @@ impl Template { + // provenance for downstream mutators. + Expr::init( + Template { ++ head_loc: crate::Loc::EMPTY, + tag: None, + parts: crate::StoreSlice::from_bump(parts), + head: TemplateContents::Cooked( +diff --git a/src/ast/expr.rs b/src/ast/expr.rs +index cfedeb4f..f24c2239 100644 +--- a/src/ast/expr.rs ++++ b/src/ast/expr.rs +@@ -2000,7 +2000,9 @@ impl Data { + .try_deep_clone_with(|e| e.deep_clone_no_detach(bump))?, + optional_chain: el.optional_chain, + is_direct_eval: el.is_direct_eval, ++ open_paren_loc: el.open_paren_loc, + close_paren_loc: el.close_paren_loc, ++ preserve_parenthesized_target: el.preserve_parenthesized_target, + can_be_unwrapped_if_unused: el.can_be_unwrapped_if_unused, + was_jsx_element: el.was_jsx_element, + }); +@@ -2020,6 +2022,7 @@ impl Data { + } + Data::EIndex(el) => { + let item = bump.alloc(E::Index { ++ open_bracket_loc: el.open_bracket_loc, + target: el.target.deep_clone_no_detach(bump)?, + index: el.index.deep_clone_no_detach(bump)?, + optional_chain: el.optional_chain, +@@ -2080,6 +2083,7 @@ impl Data { + } + Data::ETemplate(el) => { + let item = bump.alloc(E::Template { ++ head_loc: el.head_loc, + tag: match &el.tag { + Some(tag) => Some(tag.deep_clone_no_detach(bump)?), + None => None, +diff --git a/src/ast/fold_string_addition.rs b/src/ast/fold_string_addition.rs +index e096e0a3..85fb1615 100644 +--- a/src/ast/fold_string_addition.rs ++++ b/src/ast/fold_string_addition.rs +@@ -93,6 +93,7 @@ pub fn fold_string_addition( + if right.head.is_utf8() { + return Some(Expr::init( + E::Template { ++ head_loc: crate::Loc::EMPTY, + tag: None, + parts: right.parts, + head: e::TemplateContents::Cooked(join_strings( +diff --git a/src/ast/known_global.rs b/src/ast/known_global.rs +index 15c439d7..a69b7697 100644 +--- a/src/ast/known_global.rs ++++ b/src/ast/known_global.rs +@@ -103,7 +103,7 @@ impl KnownGlobal { + let constructor = lookup(original_name)?; + + match constructor { +- // Error constructors can be called without 'new' with identical behavior ++ // Removing `new` changes stack positions and makes returned errors eligible for tail calls. + KnownGlobal::Error + | KnownGlobal::TypeError + | KnownGlobal::SyntaxError +@@ -111,10 +111,7 @@ impl KnownGlobal { + | KnownGlobal::ReferenceError + | KnownGlobal::EvalError + | KnownGlobal::URIError +- | KnownGlobal::AggregateError => { +- // Convert `new Error(...)` to `Error(...)` to save bytes +- Some(Self::call_from_new(e, loc)) +- } ++ | KnownGlobal::AggregateError => None, + + KnownGlobal::Object => { + let n = e.args.len_u32(); +diff --git a/src/bundler/linker_context/generateCodeForLazyExport.rs b/src/bundler/linker_context/generateCodeForLazyExport.rs +index 44c9a361..ff5d0edf 100644 +--- a/src/bundler/linker_context/generateCodeForLazyExport.rs ++++ b/src/bundler/linker_context/generateCodeForLazyExport.rs +@@ -344,6 +344,7 @@ pub(crate) fn generate_code_for_lazy_export( + bun_ast::StoreSlice::new_mut(arena.alloc_slice_fill_iter(template_parts)); + value = Expr::init( + E::Template { ++ head_loc: bun_ast::Loc::EMPTY, + tag: None, + parts: parts_slice, + head: E::TemplateContents::Cooked(E::String::init(b"")), +diff --git a/src/js_parser/lower/lower_decorators.rs b/src/js_parser/lower/lower_decorators.rs +index c285fbfb..dff0909c 100644 +--- a/src/js_parser/lower/lower_decorators.rs ++++ b/src/js_parser/lower/lower_decorators.rs +@@ -1142,6 +1142,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + let this = p.new_expr(E::This {}, loc); + let member = p.new_expr( + E::Index { ++ open_bracket_loc: bun_ast::Loc::EMPTY, + target: this, + index: key, + optional_chain: None, +@@ -1438,6 +1439,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + ); + self.new_expr( + E::Index { ++ open_bracket_loc: bun_ast::Loc::EMPTY, + target: object, + index: key, + optional_chain: None, +diff --git a/src/js_parser/p.rs b/src/js_parser/p.rs +index 7da4223f..ae67d9a1 100644 +--- a/src/js_parser/p.rs ++++ b/src/js_parser/p.rs +@@ -178,6 +178,8 @@ pub(crate) struct ParserSnapshot<'a> { + should_fold_typescript_constant_expressions: bool, + fn_or_arrow_data_parse: FnOrArrowDataParse, + latest_arrow_arg_loc: bun_ast::Loc, ++ parenthesized_target_loc: bun_ast::Loc, ++ parenthesized_suffix_loc: bun_ast::Loc, + forbid_suffix_after_as_loc: bun_ast::Loc, + after_arrow_body_loc: bun_ast::Loc, + esm_import_keyword: bun_ast::Range, +@@ -271,6 +273,8 @@ pub struct P<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> { + // allocated_names: ListManaged(string) = ListManaged(string).init(bun.default_allocator), + // allocated_names_pool: ?*AllocatedNamesPool.Node = null, + pub(crate) latest_arrow_arg_loc: bun_ast::Loc, ++ pub(crate) parenthesized_target_loc: bun_ast::Loc, ++ pub(crate) parenthesized_suffix_loc: bun_ast::Loc, + pub(crate) forbid_suffix_after_as_loc: bun_ast::Loc, + pub(crate) current_scope: js_ast::StoreRef, + pub(crate) scopes_for_current_part: List<'a, *mut js_ast::Scope>, +@@ -5486,7 +5490,16 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + }); + } + ++ pub(crate) fn preserve_parenthesized_call_target(&self, expr: Expr) -> bool { ++ self.options.features.dont_bundle_twice ++ && self.parenthesized_target_loc.eql(expr.loc) ++ && self.parenthesized_suffix_loc.eql(self.lexer.loc()) ++ } ++ + pub(crate) fn mark_expr_as_parenthesized(&mut self, expr: &mut Expr) { ++ // Runtime loads retain the callee parentheses that select JSC's call position. ++ self.parenthesized_target_loc = expr.loc; ++ self.parenthesized_suffix_loc = self.lexer.loc(); + match &mut expr.data { + js_ast::ExprData::EArray(ex) => ex.is_parenthesized = true, + js_ast::ExprData::EObject(ex) => ex.is_parenthesized = true, +@@ -7564,6 +7577,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + } + _ => self.new_expr( + E::Index { ++ open_bracket_loc: bun_ast::Loc::EMPTY, + target, + index: key, + optional_chain: None, +@@ -8351,6 +8365,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + .should_fold_typescript_constant_expressions, + fn_or_arrow_data_parse: self.fn_or_arrow_data_parse.clone(), + latest_arrow_arg_loc: self.latest_arrow_arg_loc, ++ parenthesized_target_loc: self.parenthesized_target_loc, ++ parenthesized_suffix_loc: self.parenthesized_suffix_loc, + forbid_suffix_after_as_loc: self.forbid_suffix_after_as_loc, + after_arrow_body_loc: self.after_arrow_body_loc, + esm_import_keyword: self.esm_import_keyword, +@@ -8385,6 +8401,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + snapshot.should_fold_typescript_constant_expressions; + self.fn_or_arrow_data_parse = snapshot.fn_or_arrow_data_parse; + self.latest_arrow_arg_loc = snapshot.latest_arrow_arg_loc; ++ self.parenthesized_target_loc = snapshot.parenthesized_target_loc; ++ self.parenthesized_suffix_loc = snapshot.parenthesized_suffix_loc; + self.forbid_suffix_after_as_loc = snapshot.forbid_suffix_after_as_loc; + self.after_arrow_body_loc = snapshot.after_arrow_body_loc; + self.esm_import_keyword = snapshot.esm_import_keyword; +@@ -9841,6 +9859,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + fn_only_data_visit: FnOnlyDataVisit::default(), + allocated_names: BumpVec::new_in(arena), + latest_arrow_arg_loc: bun_ast::Loc::EMPTY, ++ parenthesized_target_loc: bun_ast::Loc::EMPTY, ++ parenthesized_suffix_loc: bun_ast::Loc::EMPTY, + forbid_suffix_after_as_loc: bun_ast::Loc::EMPTY, + scopes_for_current_part: BumpVec::new_in(arena), + symbols: BumpVec::with_capacity_in(estimated_symbol_count, arena), +diff --git a/src/js_parser/parse/parse_prefix.rs b/src/js_parser/parse/parse_prefix.rs +index 5c4d54f7..2ff66c10 100644 +--- a/src/js_parser/parse/parse_prefix.rs ++++ b/src/js_parser/parse/parse_prefix.rs +@@ -54,8 +54,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + p.allow_in = true; + + let mut value = p.parse_expr(Level::Lowest)?; +- p.mark_expr_as_parenthesized(&mut value); + p.lexer.expect(T::TCloseParen)?; ++ p.mark_expr_as_parenthesized(&mut value); + + p.allow_in = old_allow_in; + return Ok(value); +@@ -295,6 +295,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + + Ok(p.new_expr( + E::Template { ++ head_loc: loc, + tag: None, + head: E::TemplateContents::Cooked(head), + parts, +diff --git a/src/js_parser/parse/parse_suffix.rs b/src/js_parser/parse/parse_suffix.rs +index d6b6bd15..856d7d13 100644 +--- a/src/js_parser/parse/parse_suffix.rs ++++ b/src/js_parser/parse/parse_suffix.rs +@@ -83,6 +83,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + let index = p.new_expr(E::PrivateIdentifier { ref_ }, name_loc); + *left = p.new_expr( + E::Index { ++ open_bracket_loc: bun_ast::Loc::EMPTY, + target, + index, + optional_chain: old_optional_chain, +@@ -140,6 +141,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + match p.lexer.token { + T::TOpenBracket => { + // "a?.[b]" ++ let open_bracket_loc = p.lexer.loc(); + p.lexer.next()?; + + // allow "in" inside the brackets; +@@ -155,6 +157,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + let target = *left; + *left = p.new_expr( + E::Index { ++ open_bracket_loc, + target, + index, + optional_chain: optional_start, +@@ -170,6 +173,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + return Ok(Continuation::Done); + } + ++ let open_paren_loc = p.lexer.loc(); ++ let preserve_parenthesized_target = p.preserve_parenthesized_call_target(*left); + let list_loc = p.parse_call_args()?; + let loc = left.loc; + let target = *left; +@@ -177,7 +182,9 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + E::Call { + target, + args: list_loc.list, ++ open_paren_loc, + close_paren_loc: list_loc.loc, ++ preserve_parenthesized_target, + optional_chain: optional_start, + ..Default::default() + }, +@@ -200,6 +207,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + return Ok(Continuation::Done); + } + ++ let open_paren_loc = p.lexer.loc(); ++ let preserve_parenthesized_target = p.preserve_parenthesized_call_target(*left); + let list_loc = p.parse_call_args()?; + let loc = left.loc; + let target = *left; +@@ -207,7 +216,9 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + E::Call { + target, + args: list_loc.list, ++ open_paren_loc, + close_paren_loc: list_loc.loc, ++ preserve_parenthesized_target, + optional_chain: optional_start, + ..Default::default() + }, +@@ -226,6 +237,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + let index = p.new_expr(E::PrivateIdentifier { ref_ }, name_loc); + *left = p.new_expr( + E::Index { ++ open_bracket_loc: bun_ast::Loc::EMPTY, + target, + index, + optional_chain: optional_start, +@@ -281,6 +293,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + ); + } + // p.markSyntaxFeature(compat.TemplateLiteral, p.lexer.Range()); ++ let head_loc = p.lexer.loc(); + let head = E::Str::new(p.lexer.raw_template_contents()); + p.lexer.next()?; + +@@ -288,6 +301,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + let tag = *left; + *left = p.new_expr( + E::Template { ++ head_loc, + tag: Some(tag), + head: E::TemplateContents::Raw(head), + parts: E::Template::empty_parts(), +@@ -312,12 +326,14 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + ); + } + // p.markSyntaxFeature(compat.TemplateLiteral, p.lexer.Range()); ++ let head_loc = p.lexer.loc(); + let head = E::Str::new(p.lexer.raw_template_contents()); + let (parts, _tail_loc) = p.parse_template_parts(true)?; + let tag = *left; + let loc = left.loc; + *left = p.new_expr( + E::Template { ++ head_loc, + tag: Some(tag), + head: E::TemplateContents::Raw(head), + parts, +@@ -346,6 +362,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + return Ok(Continuation::Done); + } + ++ let open_bracket_loc = p.lexer.loc(); + p.lexer.next()?; + + // Allow "in" inside the brackets +@@ -362,6 +379,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + let target = *left; + *left = p.new_expr( + E::Index { ++ open_bracket_loc, + target, + index, + optional_chain: old_optional_chain, +@@ -384,6 +402,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + return Ok(Continuation::Done); + } + ++ let open_paren_loc = p.lexer.loc(); ++ let preserve_parenthesized_target = p.preserve_parenthesized_call_target(*left); + let list_loc = p.parse_call_args()?; + let loc = left.loc; + let target = *left; +@@ -391,7 +411,9 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + E::Call { + target, + args: list_loc.list, ++ open_paren_loc, + close_paren_loc: list_loc.loc, ++ preserve_parenthesized_target, + optional_chain: old_optional_chain, + ..Default::default() + }, +@@ -486,6 +508,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + p: &mut Self, + optional_chain: &mut Option, + old_optional_chain: Option, ++ left: &Expr, + ) -> CResult { + // Skip over TypeScript non-null assertions + if p.lexer.has_newline_before { +@@ -497,7 +520,11 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + return Err(crate::Error::SyntaxError); + } + ++ let parenthesized = p.preserve_parenthesized_call_target(*left); + p.lexer.next()?; ++ if parenthesized { ++ p.parenthesized_suffix_loc = p.lexer.loc(); ++ } + *optional_chain = old_optional_chain; + + Ok(Continuation::Next) +@@ -878,8 +905,12 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + // TypeScript allows type arguments to be specified with angle brackets + // inside an expression. Unlike in other languages, this unfortunately + // appears to require backtracking to parse. ++ let parenthesized = p.preserve_parenthesized_call_target(*left); + if Self::IS_TYPESCRIPT_ENABLED && p.try_skip_type_script_type_arguments_with_backtracking() + { ++ if parenthesized { ++ p.parenthesized_suffix_loc = p.lexer.loc(); ++ } + *optional_chain = old_optional_chain; + return Ok(Continuation::Next); + } +@@ -969,8 +1000,12 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + // TypeScript allows type arguments to be specified with angle brackets + // inside an expression. Unlike in other languages, this unfortunately + // appears to require backtracking to parse. ++ let parenthesized = p.preserve_parenthesized_call_target(*left); + if Self::IS_TYPESCRIPT_ENABLED && p.try_skip_type_script_type_arguments_with_backtracking() + { ++ if parenthesized { ++ p.parenthesized_suffix_loc = p.lexer.loc(); ++ } + *optional_chain = old_optional_chain; + return Ok(Continuation::Next); + } +@@ -1555,7 +1590,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + T::TSlash => Self::sfx_t_slash(p, level, left), + T::TSlashEquals => Self::sfx_t_slash_equals(p, level, left), + T::TExclamation => { +- Self::sfx_t_exclamation(p, &mut optional_chain, old_optional_chain) ++ Self::sfx_t_exclamation(p, &mut optional_chain, old_optional_chain, left) + } + T::TBarBar => Self::sfx_t_bar_bar(p, level, left, flags), + T::TAmpersandAmpersand => Self::sfx_t_ampersand_ampersand(p, level, left, flags), +diff --git a/src/js_parser/parse/parse_typescript.rs b/src/js_parser/parse/parse_typescript.rs +index eb4d49f9..7acac3ed 100644 +--- a/src/js_parser/parse/parse_typescript.rs ++++ b/src/js_parser/parse/parse_typescript.rs +@@ -136,6 +136,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + let index = p.new_expr(E::PrivateIdentifier { ref_ }, name_loc); + expr = p.new_expr( + E::Index { ++ open_bracket_loc: bun_ast::Loc::EMPTY, + target: expr, + index, + optional_chain: None, +diff --git a/src/js_parser/visit/mod.rs b/src/js_parser/visit/mod.rs +index e12fd145..ae13f46c 100644 +--- a/src/js_parser/visit/mod.rs ++++ b/src/js_parser/visit/mod.rs +@@ -1350,6 +1350,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + } + _ => self.new_expr( + E::Index { ++ open_bracket_loc: bun_ast::Loc::EMPTY, + target: this_target, + index: key, + optional_chain: None, +diff --git a/src/js_parser/visit/visit_expr.rs b/src/js_parser/visit/visit_expr.rs +index 23c08903..c5e0d61a 100644 +--- a/src/js_parser/visit/visit_expr.rs ++++ b/src/js_parser/visit/visit_expr.rs +@@ -855,8 +855,8 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + let is_template_tag = matches!(p.template_tag, Data::EIndex(tag) if core::ptr::eq(&raw const *e_, &raw const *tag)); + let is_delete_target = matches!(p.delete_target, Data::EIndex(dt) if core::ptr::eq(&raw const *e_, &raw const *dt)); + +- // "a['b']" => "a.b" +- if p.options.features.minify_syntax { ++ // Runtime stack positions distinguish computed access from dot access. ++ if p.options.features.minify_syntax && !p.options.features.dont_bundle_twice { + if let Some(mut s) = e_.index.data.e_string() { + if !s.is_utf16 && s.is_identifier(p.arena) { + let dot = p.new_expr( +@@ -985,7 +985,10 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + + // "a['b' + '']" => "a.b" + // "enum A { B = 'b' }; a[A.B]" => "a.b" +- if p.options.features.minify_syntax && s.is_identifier(p.arena) { ++ if p.options.features.minify_syntax ++ && !p.options.features.dont_bundle_twice ++ && s.is_identifier(p.arena) ++ { + let dot = p.new_expr( + E::Dot { + name: s.data, +diff --git a/src/js_parser/visit/visit_stmt.rs b/src/js_parser/visit/visit_stmt.rs +index f3995507..33a4cca1 100644 +--- a/src/js_parser/visit/visit_stmt.rs ++++ b/src/js_parser/visit/visit_stmt.rs +@@ -2332,6 +2332,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + Expr::assign( + p.new_expr( + E::Index { ++ open_bracket_loc: bun_ast::Loc::EMPTY, + target: Expr::init_identifier(data.arg, value.loc), + index: name_as_e_string.unwrap(), + optional_chain: None, +@@ -2353,6 +2354,7 @@ impl<'a, const TYPESCRIPT: bool, const SCAN_ONLY: bool> P<'a, TYPESCRIPT, SCAN_O + value_exprs.push(Expr::assign( + p.new_expr( + E::Index { ++ open_bracket_loc: bun_ast::Loc::EMPTY, + target: Expr::init_identifier(data.arg, value.loc), + index: assign_target, + optional_chain: None, +diff --git a/src/js_printer/lib.rs b/src/js_printer/lib.rs +index 40408623..764a2900 100644 +--- a/src/js_printer/lib.rs ++++ b/src/js_printer/lib.rs +@@ -3502,6 +3502,9 @@ pub(crate) mod __gated_printer { + && self.is_unbound_eval_identifier(e.target) + && e.optional_chain.is_none(); + ++ if e.preserve_parenthesized_target { ++ self.print(b"("); ++ } + if is_unbound_eval { + self.print(b"(0,"); + self.print_space(); +@@ -3513,9 +3516,15 @@ pub(crate) mod __gated_printer { + self.print_expr(e.target, Level::Postfix, target_flags); + } + ++ if e.preserve_parenthesized_target { ++ self.print(b")"); ++ } + if e.optional_chain == Some(js_ast::OptionalChain::Start) { + self.print(b"?."); + } ++ if !e.open_paren_loc.is_empty() { ++ self.add_source_mapping(e.open_paren_loc); ++ } + self.print(b"("); + let args = e.args.slice(); + +@@ -3765,6 +3774,9 @@ pub(crate) mod __gated_printer { + self.print_symbol(priv_.ref_); + } + _ => { ++ if !e.open_bracket_loc.is_empty() { ++ self.add_source_mapping(e.open_bracket_loc); ++ } + self.print(b"["); + self.add_source_mapping(e.index.loc); + self.print_expr(e.index, Level::Lowest, ExprFlag::none()); +@@ -4063,6 +4075,7 @@ pub(crate) mod __gated_printer { + // would be a cross-thread data race. Re-prints recompute + // the identical fold, so emitted output is unchanged. + let mut e = E::Template { ++ head_loc: e.head_loc, + tag: e.tag, + parts: e.parts, + head: match &e.head { +@@ -4128,6 +4141,7 @@ pub(crate) mod __gated_printer { + // parts slice stays valid past this block. + let parts_slice = js_ast::StoreSlice::from_bump(replaced); + let mut copy = E::Template { ++ head_loc: e.head_loc, + tag: e.tag, + parts: parts_slice, + head: match &e.head { +@@ -4157,6 +4171,7 @@ pub(crate) mod __gated_printer { + // fields out is safe even though `t` itself may + // point at `copy`. + e = E::Template { ++ head_loc: t.head_loc, + tag: t.tag, + parts: t.parts, + head: match &t.head { +@@ -4204,6 +4219,9 @@ pub(crate) mod __gated_printer { + self.add_source_mapping(expr.loc); + } + ++ if !e.head_loc.is_empty() { ++ self.add_source_mapping(e.head_loc); ++ } + self.print(b"`"); + match &e.head { + E::TemplateContents::Raw(raw) => self.print_raw_template_literal(raw), +diff --git a/src/jsc/RuntimeTranspilerCache.rs b/src/jsc/RuntimeTranspilerCache.rs +index 1142ca3b..c857749a 100644 +--- a/src/jsc/RuntimeTranspilerCache.rs ++++ b/src/jsc/RuntimeTranspilerCache.rs +@@ -66,7 +66,9 @@ bun_core::declare_scope!(cache, visible); + /// Version 34: Discard output containing a previous runtime plugin's resolved import paths. + /// Version 35: Preserve inferred names and inline type-only runtime evaluation edges. + /// Version 36: Runtime plugin imports retain their original specifiers and resolve when executed. +-const EXPECTED_VERSION: u32 = 36; ++/// Version 37: Preserve builtin error constructors and their stack positions and frames. ++/// Version 38: Retain runtime call syntax and map opening call/property delimiters. ++const EXPECTED_VERSION: u32 = 38; + + /// Source files smaller than this are not written to / read from the on-disk + /// transpiler cache. Originally 50 KiB, which excluded almost every file in a +diff --git a/src/jsc/bindings/ErrorStackFrame.cpp b/src/jsc/bindings/ErrorStackFrame.cpp +index 14ec9fd8..9f8c3616 100644 +--- a/src/jsc/bindings/ErrorStackFrame.cpp ++++ b/src/jsc/bindings/ErrorStackFrame.cpp +@@ -22,12 +22,13 @@ static bool isConstruct(JSC::CodeBlock* code, JSC::BytecodeIndex bc) + } + } + +-ZigStackFramePosition getAdjustedPositionForBytecode(JSC::CodeBlock* code, JSC::BytecodeIndex bc) ++ZigStackFramePosition getAdjustedPositionForBytecode(JSC::CodeBlock* code, JSC::BytecodeIndex bc, bool isAsync) + { + auto expr = code->expressionInfoForBytecodeIndex(bc); +- auto offset = expr.divot; +- // Constructors point to `new`; other calls retain JSC's syntax location. +- if (isConstruct(code, bc)) ++ auto callSitePosition = code->callSitePositionForBytecodeIndex(bc, isAsync); ++ auto offset = callSitePosition.value_or(expr.divot); ++ // Older or implicit bytecode can lack a syntax-selected stack position. ++ if (!callSitePosition && isConstruct(code, bc)) + offset -= std::min(offset, expr.startOffset); + + auto lineColumn = code->source().provider()->documentLineColumnForOffset(offset); +@@ -38,16 +39,18 @@ ZigStackFramePosition getAdjustedPositionForBytecode(JSC::CodeBlock* code, JSC:: + }; + } + +-ZigStackFramePosition getAdjustedLineColumnForBytecode(JSC::CodeBlock* code, JSC::BytecodeIndex bc) ++ZigStackFramePosition getAdjustedLineColumnForBytecode(JSC::CodeBlock* code, JSC::BytecodeIndex bc, bool isAsync) + { + if (isConstruct(code, bc)) { +- auto position = getAdjustedPositionForBytecode(code, bc); ++ auto position = getAdjustedPositionForBytecode(code, bc, isAsync); + position.byte_position = -1; + return position; + } + +- // Keep JSC's cached lookup for frames that do not need an expression-range adjustment. +- auto lineColumn = code->lineColumnForBytecodeIndex(bc); ++ auto callSitePosition = code->callSitePositionForBytecodeIndex(bc, isAsync); ++ auto lineColumn = callSitePosition ++ ? code->source().provider()->documentLineColumnForOffset(*callSitePosition) ++ : code->lineColumnForBytecodeIndex(bc); + return { + .line_zero_based = OrdinalNumber::fromOneBasedInt(lineColumn.line).zeroBasedInt(), + .column_zero_based = OrdinalNumber::fromOneBasedInt(lineColumn.column).zeroBasedInt(), +diff --git a/src/jsc/bindings/ErrorStackFrame.h b/src/jsc/bindings/ErrorStackFrame.h +index a1c31678..70f1b2fa 100644 +--- a/src/jsc/bindings/ErrorStackFrame.h ++++ b/src/jsc/bindings/ErrorStackFrame.h +@@ -4,8 +4,8 @@ + + namespace Bun { + +-ZigStackFramePosition getAdjustedPositionForBytecode(JSC::CodeBlock* code, JSC::BytecodeIndex bc); ++ZigStackFramePosition getAdjustedPositionForBytecode(JSC::CodeBlock* code, JSC::BytecodeIndex bc, bool isAsync); + +-ZigStackFramePosition getAdjustedLineColumnForBytecode(JSC::CodeBlock* code, JSC::BytecodeIndex bc); ++ZigStackFramePosition getAdjustedLineColumnForBytecode(JSC::CodeBlock* code, JSC::BytecodeIndex bc, bool isAsync); + + } // namespace Bun +diff --git a/src/jsc/bindings/ErrorStackTrace.cpp b/src/jsc/bindings/ErrorStackTrace.cpp +index 3550bb52..866d011c 100644 +--- a/src/jsc/bindings/ErrorStackTrace.cpp ++++ b/src/jsc/bindings/ErrorStackTrace.cpp +@@ -341,7 +341,7 @@ bool JSCStackFrame::calculateSourcePositions() + return false; + } + +- auto location = Bun::getAdjustedLineColumnForBytecode(m_codeBlock, m_bytecodeIndex); ++ auto location = Bun::getAdjustedLineColumnForBytecode(m_codeBlock, m_bytecodeIndex, isAsync()); + m_sourcePositions.line = location.line(); + m_sourcePositions.column = location.column(); + +diff --git a/src/jsc/bindings/FormatStackTraceForJS.cpp b/src/jsc/bindings/FormatStackTraceForJS.cpp +index 60aca2e7..403bf131 100644 +--- a/src/jsc/bindings/FormatStackTraceForJS.cpp ++++ b/src/jsc/bindings/FormatStackTraceForJS.cpp +@@ -321,7 +321,7 @@ WTF::String formatStackTrace( + if (!frame.hasLineAndColumnInfo()) continue; + + if (frame.codeBlock() && frame.hasBytecodeIndex()) { +- auto position = Bun::getAdjustedLineColumnForBytecode(frame.codeBlock(), frame.bytecodeIndex()); ++ auto position = Bun::getAdjustedLineColumnForBytecode(frame.codeBlock(), frame.bytecodeIndex(), frame.isAsyncFrame()); + originalLineColumns[i] = { static_cast(position.line().oneBasedInt()), static_cast(position.column().oneBasedInt()) }; + if (auto overrideLine = frame.codeBlock()->ownerExecutable()->overrideLineNumber(vm)) + originalLineColumns[i].line = *overrideLine; +diff --git a/src/jsc/bindings/ZigException.cpp b/src/jsc/bindings/ZigException.cpp +index cfd1059c..b7442cb3 100644 +--- a/src/jsc/bindings/ZigException.cpp ++++ b/src/jsc/bindings/ZigException.cpp +@@ -143,7 +143,7 @@ static void populateStackFramePosition(const JSC::StackFrame& stackFrame, BunStr + return; + } + +- auto location = Bun::getAdjustedPositionForBytecode(code, stackFrame.bytecodeIndex()); ++ auto location = Bun::getAdjustedPositionForBytecode(code, stackFrame.bytecodeIndex(), stackFrame.isAsyncFrame()); + memcpy(&position, &location, sizeof(ZigStackFramePosition)); + if (flags == PopulateStackTraceFlags::OnlyPosition) + return; +diff --git a/src/react_compiler/codegen.rs b/src/react_compiler/codegen.rs +index 473b14c9..5b3f1704 100644 +--- a/src/react_compiler/codegen.rs ++++ b/src/react_compiler/codegen.rs +@@ -669,6 +669,7 @@ fn codegen_reactive_scope( + let cache_slot = |index: u32| { + Expr::init( + E::Index { ++ open_bracket_loc: bun_ast::Loc::EMPTY, + target: cache_ident(), + index: Expr::init(E::Number::new(index as f64), loc), + optional_chain: None, +@@ -2005,6 +2006,7 @@ fn codegen_base_instruction_value( + let prop = codegen_place_to_expression(cx, property)?; + Ok(Expr::init( + E::Index { ++ open_bracket_loc: bun_ast::Loc::EMPTY, + target: obj, + index: prop, + optional_chain: None, +@@ -2027,6 +2029,7 @@ fn codegen_base_instruction_value( + op: OpCode::BinAssign, + left: Expr::init( + E::Index { ++ open_bracket_loc: bun_ast::Loc::EMPTY, + target: obj, + index: prop, + optional_chain: None, +@@ -2049,6 +2052,7 @@ fn codegen_base_instruction_value( + op: OpCode::UnDelete, + value: Expr::init( + E::Index { ++ open_bracket_loc: bun_ast::Loc::EMPTY, + target: obj, + index: prop, + optional_chain: None, +@@ -2179,6 +2183,7 @@ fn codegen_base_instruction_value( + let tag_expr = codegen_place_to_expression(cx, tag)?; + Ok(Expr::init( + E::Template { ++ head_loc: bun_ast::Loc::EMPTY, + tag: Some(tag_expr), + head: E::TemplateContents::Raw(value.raw), + parts: StoreSlice::EMPTY, +@@ -2204,6 +2209,7 @@ fn codegen_base_instruction_value( + } + Ok(Expr::init( + E::Template { ++ head_loc: bun_ast::Loc::EMPTY, + tag: None, + head, + parts: StoreSlice::new_mut(parts.leak()), +@@ -2369,6 +2375,7 @@ fn codegen_function_expression( + }); + let wrapped = Expr::init( + E::Index { ++ open_bracket_loc: bun_ast::Loc::EMPTY, + target: Expr::init( + E::Object { + properties: props, +@@ -3311,6 +3318,7 @@ fn property_access_expr( + ), + PropertyLiteral::Number(n) => Expr::init( + E::Index { ++ open_bracket_loc: bun_ast::Loc::EMPTY, + target, + index: Expr::init(E::Number::new(n.value()), loc), + optional_chain, +diff --git a/test/bundler/bundler_minify.test.ts b/test/bundler/bundler_minify.test.ts +index 052aa492..0d629bee 100644 +--- a/test/bundler/bundler_minify.test.ts ++++ b/test/bundler/bundler_minify.test.ts +@@ -1229,7 +1229,7 @@ describe("bundler", () => { + }, + }); + +- itBundled("minify/ErrorConstructorOptimization", { ++ itBundled("minify/ErrorConstructorPositions", { + files: { + "/entry.js": /* js */ ` + // Test all Error constructors +@@ -1272,25 +1272,25 @@ describe("bundler", () => { + `, + }, + capture: [ +- "Error()", +- 'Error("message")', +- 'Error("message", { cause: "cause" })', +- "TypeError()", +- 'TypeError("type error")', +- "SyntaxError()", +- 'SyntaxError("syntax error")', +- "RangeError()", +- 'RangeError("range error")', +- "ReferenceError()", +- 'ReferenceError("ref error")', +- "EvalError()", +- 'EvalError("eval error")', +- "URIError()", +- 'URIError("uri error")', +- 'AggregateError([], "aggregate error")', +- 'AggregateError([Error("e1")], "multiple")', +- "Error(msg)", +- "TypeError(getErrorMessage())", ++ "new Error", ++ 'new Error("message")', ++ 'new Error("message", { cause: "cause" })', ++ "new TypeError", ++ 'new TypeError("type error")', ++ "new SyntaxError", ++ 'new SyntaxError("syntax error")', ++ "new RangeError", ++ 'new RangeError("range error")', ++ "new ReferenceError", ++ 'new ReferenceError("ref error")', ++ "new EvalError", ++ 'new EvalError("eval error")', ++ "new URIError", ++ 'new URIError("uri error")', ++ 'new AggregateError([], "aggregate error")', ++ 'new AggregateError([new Error("e1")], "multiple")', ++ "new Error(msg)", ++ "new TypeError(getErrorMessage())", + "/* @__PURE__ */ new Date", + "/* @__PURE__ */ new Map", + "/* @__PURE__ */ new Set", +diff --git a/test/js/bun/jsc/webkit-upgrade-7b485a76e9.test.ts b/test/js/bun/jsc/webkit-upgrade-7b485a76e9.test.ts +index 27805cb4..42f6196b 100644 +--- a/test/js/bun/jsc/webkit-upgrade-7b485a76e9.test.ts ++++ b/test/js/bun/jsc/webkit-upgrade-7b485a76e9.test.ts +@@ -150,8 +150,8 @@ describe("WebKit 7b485a76e9 upgrade", () => { + const frames = [...stack.matchAll(/positions\.js:(\d+):(\d+)/g)].map(m => [Number(m[1]), Number(m[2])]); + expect(frames).toEqual([ + [6, 28], +- [4, 15], +- [7, 6], ++ [4, 10], ++ [7, 1], + ]); + + // lineOffset and columnOffset move the first line. columnOffset does not move later lines. +diff --git a/test/js/bun/sourcemap/internal-sourcemap-roundtrip.test.ts b/test/js/bun/sourcemap/internal-sourcemap-roundtrip.test.ts +index ba26e511..e5d1ebd8 100644 +--- a/test/js/bun/sourcemap/internal-sourcemap-roundtrip.test.ts ++++ b/test/js/bun/sourcemap/internal-sourcemap-roundtrip.test.ts +@@ -486,7 +486,7 @@ describe.concurrent("sourcemap of a source with a truncated trailing UTF-8 seque + const map = await Bun.file(path.join(String(dir), "out", "in.js.map")).json(); + expect(map).toMatchObject({ + sources: ["../in.js"], +- mappings: ";AAAA,QAAQ,IAAI,CAAC;", ++ mappings: ";AAAA,QAAQ,GAAG,CAAC,CAAC;", + }); + }); + }); +diff --git a/test/js/bun/sourcemap/internal-sourcemap.test.ts b/test/js/bun/sourcemap/internal-sourcemap.test.ts +index b8efdae7..63c78056 100644 +--- a/test/js/bun/sourcemap/internal-sourcemap.test.ts ++++ b/test/js/bun/sourcemap/internal-sourcemap.test.ts +@@ -76,9 +76,9 @@ describe("InternalSourceMap", () => { + + const positions = extractPositions(stdout); + expect(positions).toEqual([ +- "6:13", // throw new Error("boom") ++ "6:9", // throw new Error("boom") + "24:3", // gamma() at top level +- "31:17", // new Error("here").stack ++ "31:13", // new Error("here").stack + "34:13", // captureViaCaptureStackTrace() + ]); + +@@ -103,10 +103,12 @@ describe("InternalSourceMap", () => { + const { stdout, stderr, exited } = await run({ "index.ts": lines.join("\n") + "\n" }); + + expect(stderr).toBe(""); +- // Each frame must point at its line's `Error` in 1-based UTF-16 columns, ++ // Each frame must point at its line's `new` keyword in 1-based UTF-16 columns, + // `a1` being the all-ASCII control. + const frames = [...stdout.matchAll(/at ([a-d]1) \(.*index\.ts:(\d+):(\d+)\)/g)].map(m => `${m[1]} ${m[2]}:${m[3]}`); +- expect(frames).toEqual(["a1", "b1", "c1", "d1"].map((fn, i) => `${fn} ${i + 1}:${lines[i].indexOf("Error(") + 1}`)); ++ expect(frames).toEqual( ++ ["a1", "b1", "c1", "d1"].map((fn, i) => `${fn} ${i + 1}:${lines[i].indexOf("new Error(") + 1}`), ++ ); + expect(exited).toBe(0); + }); + +diff --git a/test/js/node/module/node-module-module.test.js b/test/js/node/module/node-module-module.test.js +index a5bc03c0..2bee38b3 100644 +--- a/test/js/node/module/node-module-module.test.js ++++ b/test/js/node/module/node-module-module.test.js +@@ -1781,7 +1781,7 @@ console.log("survived", require("./late.js"));`, + expect(stderr).toMatchInlineSnapshot(` + "1 | require("module").runMain = () => { + 2 | throw new RangeError("from the override"); +- ^ ++ ^ + RangeError: from the override + at (file:NN:NN) + +diff --git a/test/js/node/v8/callsite-async-position-fixture.mjs b/test/js/node/v8/callsite-async-position-fixture.mjs +new file mode 100644 +index 00000000..84cfc6db +--- /dev/null ++++ b/test/js/node/v8/callsite-async-position-fixture.mjs +@@ -0,0 +1,12 @@ ++async function leaf() { ++await Promise.resolve(); ++return new Error().stack; ++} ++async function parent() { ++return await leaf(); ++} ++const trace = await parent(); ++const parentFrame = trace.split("\n").find(line => /\bparent(?:\s|@|\()/.test(line)); ++const position = /:(\d+):(\d+)\)?$/.exec(parentFrame); ++if (!position || +position[1] !== 6 || +position[2] !== 8) ++ throw new Error(JSON.stringify({ expected: [6, 8], trace })); +diff --git a/test/js/node/v8/callsite-syntax-positions-fixture.js b/test/js/node/v8/callsite-syntax-positions-fixture.js +new file mode 100644 +index 00000000..32eed3ee +--- /dev/null ++++ b/test/js/node/v8/callsite-syntax-positions-fixture.js +@@ -0,0 +1,116 @@ ++// Keep call positions independent of exception-expression divots. ++let trace; ++function capture() { const stack = new Error().stack; trace = stack; return stack; } ++function Capture() { this.stack = new Error().stack; trace = this.stack; } ++function factory() { return capture; } ++const object = { capture }; ++const name = "capture"; ++const cases = [ ++ [function () { ++capture() ++ }, 10, 1, "capture()"], ++ [function () { ++capture /* comment */ () ++ }, 13, 1, "capture /* comment */ ()"], ++ [function () { ++capture ++() ++ }, 16, 1, "capture\n()"], ++ [function () { ++factory()() ++ }, 20, 10, "factory()()"], ++ [function () { ++factory() ++() ++ }, 24, 1, "factory()\n()"], ++ [function () { ++factory() /* comment */ () ++ }, 27, 25, "factory() /* comment */ ()"], ++ [function () { ++object.capture() ++ }, 30, 8, "object.capture()"], ++ [function () { ++object ++.capture() ++ }, 34, 2, "object\n.capture()"], ++ [function () { ++object . capture() ++ }, 37, 10, "object . capture()"], ++ [function () { ++object. /* comment */ capture() ++ }, 40, 23, "object. /* comment */ capture()"], ++ [function () { ++object.\u0063apture() ++ }, 43, 8, "object.\\u0063apture()"], ++ [function () { ++object[name]() ++ }, 46, 13, "object[name]()"], ++ [function () { ++object["capture"]() ++ }, 49, 18, "object[\"capture\"]()"], ++ [function () { ++object[ ++name ++] ++() ++ }, 55, 1, "object[\nname\n]\n()"], ++ [function () { ++(capture)() ++ }, 58, 10, "(capture)()"], ++ [function () { ++((capture))() ++ }, 61, 12, "((capture))()"], ++ [function () { ++(object.capture)() ++ }, 64, 17, "(object.capture)()"], ++ [function () { ++(object[name])() ++ }, 67, 15, "(object[name])()"], ++ [function () { ++(0, capture)() ++ }, 70, 13, "(0, capture)()"], ++ [function () { ++capture?.() ++ }, 73, 10, "capture?.()"], ++ [function () { ++object?.capture() ++ }, 76, 9, "object?.capture()"], ++ [function () { ++object.capture?.() ++ }, 79, 17, "object.capture?.()"], ++ [function () { ++object?.capture?.() ++ }, 82, 18, "object?.capture?.()"], ++ [function () { ++this() ++ }, 85, 5, "this()"], ++ [function () { ++capture`text` ++ }, 88, 8, "capture`text`"], ++ [function () { ++Reflect.apply(capture, null, []) ++ }, 91, 9, "Reflect.apply(capture, null, [])"], ++ [function () { ++new Capture().stack ++ }, 94, 1, "new Capture().stack"], ++ [function () { ++new ++Capture().stack ++ }, 97, 1, "new\nCapture().stack"], ++ [function () { ++new (Capture)().stack ++ }, 101, 1, "new (Capture)().stack"], ++ [function () { ++Reflect.construct(Capture, []).stack ++ }, 104, 9, "Reflect.construct(Capture, []).stack"], ++]; ++for (let iteration = 0; iteration < Math.min(typeof testLoopCount === "number" ? testLoopCount : 20, 100); ++iteration) { ++ for (const [fn, line, column, body] of cases) { ++ trace = ""; ++ fn.call(capture); ++ const frames = trace.split("\n").filter(line => /:(\d+):(\d+)\)?$/.test(line)); ++ const position = /:(\d+):(\d+)\)?$/.exec(frames[1]); ++ if (!position || +position[1] !== line || +position[2] !== column) ++ throw new Error(JSON.stringify({ body, expected: [line, column], trace })); ++ } ++} +diff --git a/test/js/node/v8/callsite-typescript-positions-fixture.ts b/test/js/node/v8/callsite-typescript-positions-fixture.ts +new file mode 100644 +index 00000000..1bc876b8 +--- /dev/null ++++ b/test/js/node/v8/callsite-typescript-positions-fixture.ts +@@ -0,0 +1,63 @@ ++import assert from "node:assert/strict"; ++let frame: NodeJS.CallSite; ++function capture(..._args: unknown[]) { ++ const target: { stack?: NodeJS.CallSite[] } = {}; ++ const previous = Error.prepareStackTrace; ++ try { ++ Error.prepareStackTrace = (_, frames) => frames; ++ Error.captureStackTrace(target, capture); ++ frame = target.stack![0]; ++ } finally { ++ Error.prepareStackTrace = previous; ++ } ++} ++const object = { capture }; ++const cases = [ ++ [function () { ++(object).capture(); ++ }, 17, 10, "receiver-parentheses"], ++ [function () { ++(object.capture)(); ++ }, 20, 25, "callee-parentheses"], ++ [function () { ++((capture))(); ++ }, 23, 20, "nested-parentheses"], ++ [function () { ++(capture as typeof capture)(); ++ }, 26, 36, "asserted-callee"], ++ [function () { ++object["capture"](); ++ }, 29, 26, "computed-callee"], ++ [function () { ++object?.["capture"](); ++ }, 32, 28, "optional-computed-callee"], ++ [function () { ++const marker = "🎉"; (capture)(); ++ }, 35, 39, "astral-prefix"], ++ [function () { ++(capture); object.capture(); ++ }, 38, 19, "prior-parenthesized-expression"], ++ [function () { ++(capture)<() => T>(); ++ }, 41, 22, "generic-function-type"], ++ [function () { ++(capture)!(); ++ }, 44, 11, "non-null-callee"], ++ [function () { ++(capture)!(); ++ }, 47, 19, "generic-non-null-callee"], ++ [function () { ++(capture)!!(); ++ }, 50, 12, "repeated-non-null-callee"], ++ [function () { ++(object)!.capture(); ++ }, 53, 11, "non-null-receiver"], ++ [function () { ++capture`text`; ++ }, 56, 16, "tagged-template"], ++ ] as const; ++for (const [fn, line, column, label] of cases) { ++ fn(); ++ assert.deepEqual([frame!.getLineNumber(), frame!.getColumnNumber()], [line, column], label); ++ assert.deepEqual(/:(\d+):(\d+)\)?$/.exec(frame!.toString())!.slice(1).map(Number), [line, column], label); ++} +diff --git a/test/js/node/v8/capture-stack-trace.test.js b/test/js/node/v8/capture-stack-trace.test.js +index ac527424..fa239c56 100644 +--- a/test/js/node/v8/capture-stack-trace.test.js ++++ b/test/js/node/v8/capture-stack-trace.test.js +@@ -206,8 +206,8 @@ test.concurrent("CallSite columns preserve async continuation metadata", async ( + expect(stderr).toBe(""); + expect(JSON.parse(stdout)).toEqual([ + { position: [4, 7], textPosition: [4, 7], async: false, eval: false, native: false, file: true }, +- // JSC selects the awaited call; V8 selects `await`. Preserve the location while fixing its units. +- { position: [8, 14], textPosition: [8, 14], async: true, eval: false, native: false, file: true }, ++ // Async frames point at `await`, as in Node. ++ { position: [8, 8], textPosition: [8, 8], async: true, eval: false, native: false, file: true }, + ]); + expect(exitCode).toBe(0); + }); +@@ -2091,7 +2091,7 @@ test.concurrent( + "define stack": error => (Object.defineProperty(error, "stack", { value: "defined" }), error.stack), + // A clone is made from the complete string: it must not get a second first line. + "structuredClone": error => structuredClone(error).stack.split("\\n").filter(line => line.includes("boom")), +- "Bun.inspect": error => Bun.inspect(error).split("\\n").filter(line => line.includes("boom")).map(line => line.trim()), ++ "Bun.inspect": error => Bun.inspect(error).split("\\n").map(line => line.trim()).filter(line => line === "TypeError: boom"), + "JSON.stringify": error => JSON.stringify(error), + }; + const rows = {}; +@@ -2412,7 +2412,7 @@ test.concurrent.each([[{}], [{ BUN_JSC_useSourceProviderCache: "0" }]])( + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toBe(""); +- expect(stdout.trim()).toEndWith("[eval]:5:14)"); // 2:18 when the lexer resumed on the template literal's first line ++ expect(stdout.trim()).toEndWith("[eval]:5:10)"); // Line 2 when the lexer resumed on the template literal's first line + expect(exitCode).toBe(0); + }, + ); +@@ -2456,3 +2456,31 @@ test.skipIf(totalmem() < 10 * 1024 ** 3)( + }, + 30_000, + ); ++ ++test("syntax positions preserve built-in constructors, arrow frames, and property reads", async () => { ++ await using proc = Bun.spawn({ ++ cmd: [bunExe(), import.meta.dir + "/stack-position-regressions-fixture.js"], ++ env: bunEnv, ++ stdout: "pipe", ++ stderr: "pipe", ++ }); ++ const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); ++ expect({ stdout, stderr, exitCode }).toEqual({ stdout: "", stderr: "", exitCode: 0 }); ++}); ++ ++for (const fixture of [ ++ "callsite-syntax-positions-fixture.js", ++ "callsite-async-position-fixture.mjs", ++ "callsite-typescript-positions-fixture.ts", ++]) { ++ test(`syntax-selected stack positions: ${fixture}`, async () => { ++ await using proc = Bun.spawn({ ++ cmd: [bunExe(), import.meta.dir + "/" + fixture], ++ env: bunEnv, ++ stdout: "pipe", ++ stderr: "pipe", ++ }); ++ const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); ++ expect({ stdout, stderr, exitCode }).toEqual({ stdout: "", stderr: "", exitCode: 0 }); ++ }); ++} +diff --git a/test/js/node/v8/error-prepare-stack-default-fixture.js b/test/js/node/v8/error-prepare-stack-default-fixture.js +index 17df9c6d..5e3ce31b 100644 +--- a/test/js/node/v8/error-prepare-stack-default-fixture.js ++++ b/test/js/node/v8/error-prepare-stack-default-fixture.js +@@ -30,8 +30,8 @@ function oneWrapperLevel() { + oneWrapperLevel(); + + // The native line column numbers might differ a bit here. +-const stackIgnoringLineAndColumn = stack.replaceAll(":12:26", ":NN:NN").replaceAll(/native:.*$/gm, "native)"); +-const stack2IgnoringLineAndColumn = stack2.replaceAll(":17:26", ":NN:NN").replaceAll(/native:.*$/gm, "native)"); ++const stackIgnoringLineAndColumn = stack.replaceAll(":12:9", ":NN:NN").replaceAll(/native:.*$/gm, "native)"); ++const stack2IgnoringLineAndColumn = stack2.replaceAll(":17:9", ":NN:NN").replaceAll(/native:.*$/gm, "native)"); + if (stackIgnoringLineAndColumn !== stack2IgnoringLineAndColumn) { + console.log("\n-----\n"); + console.log(stackIgnoringLineAndColumn); +diff --git a/test/js/node/v8/stack-position-regressions-fixture.js b/test/js/node/v8/stack-position-regressions-fixture.js +new file mode 100644 +index 00000000..fe71747d +--- /dev/null ++++ b/test/js/node/v8/stack-position-regressions-fixture.js +@@ -0,0 +1,104 @@ ++"use strict"; ++const name = "x"; ++const cases = [ ++ ['Error', function () { ++const error = new Error("test"); return error; ++ }, 5, 15], ++ ['TypeError', function () { ++const error = new TypeError("test"); return error; ++ }, 8, 15], ++ ['SyntaxError', function () { ++const error = new SyntaxError("test"); return error; ++ }, 11, 15], ++ ['RangeError', function () { ++const error = new RangeError("test"); return error; ++ }, 14, 15], ++ ['ReferenceError', function () { ++const error = new ReferenceError("test"); return error; ++ }, 17, 15], ++ ['EvalError', function () { ++const error = new EvalError("test"); return error; ++ }, 20, 15], ++ ['URIError', function () { ++const error = new URIError("test"); return error; ++ }, 23, 15], ++ ['AggregateError', function () { ++const error = new AggregateError([], "test"); return error; ++ }, 26, 15], ++ ['arrow-new', () => new Error("arrow"), 28, 25], ++ ['arrow-type', () => new TypeError("arrow"), 29, 26], ++ ['arrow-alias', () => new globalThis.Error("arrow"), 30, 27], ++ ['multiline-new', function () { ++const error = new ++ Error("multiline"); return error; ++ }, 32, 15], ++ ['constructor-comment', function () { ++const error = new /* comment */ Error("comment"); return error; ++ }, 36, 15], ++ ['null-dot', function () { ++try { null.x; } catch (error) { return error; } ++ }, 39, 12], ++ ['undefined-dot', function () { ++try { undefined.x; } catch (error) { return error; } ++ }, 42, 17], ++ ['null-length', function () { ++try { null.length; } catch (error) { return error; } ++ }, 45, 12], ++ ['null-comment', function () { ++try { null . /* comment */ x; } catch (error) { return error; } ++ }, 48, 28], ++ ['null-escape', function () { ++try { null.\u0078; } catch (error) { return error; } ++ }, 51, 12], ++ ['null-multiline', function () { ++try { null ++ .x; } catch (error) { return error; } ++ }, 55, 6], ++ ['null-bracket', function () { ++try { null["x"]; } catch (error) { return error; } ++ }, 58, 11], ++ ['null-number', function () { ++try { null[1]; } catch (error) { return error; } ++ }, 61, 11], ++ ['null-computed', function () { ++try { null[name]; } catch (error) { return error; } ++ }, 64, 11], ++ ['null-call', function () { ++try { null.x(); } catch (error) { return error; } ++ }, 67, 12], ++ ['null-parenthesized-call', function () { ++try { (null.x)(); } catch (error) { return error; } ++ }, 70, 13], ++ ['null-bracket-call', function () { ++try { null["x"](); } catch (error) { return error; } ++ }, 73, 11], ++ ['null-call-method', function () { ++try { null.call(); } catch (error) { return error; } ++ }, 76, 12], ++ ['null-apply-method', function () { ++try { null.apply(null, []); } catch (error) { return error; } ++ }, 79, 12], ++ ['null-has-own', function () { ++try { null.hasOwnProperty(name); } catch (error) { return error; } ++ }, 82, 12], ++ ]; ++function checkPosition(error, line, column, shape, structured) { ++ const frames = structured ? error.stack : error.stack.split("\n").filter(frame => /:(\d+):(\d+)\)?$/.test(frame)); ++ const actual = structured ? [frames[0].getLineNumber(), frames[0].getColumnNumber()] : /:(\d+):(\d+)\)?$/.exec(frames[0])?.slice(1).map(Number); ++ if (!actual || actual[0] !== line || actual[1] !== column) ++ throw new Error(JSON.stringify({ shape, structured, expected: [line, column], actual, trace: structured ? frames.map(String) : error.stack })); ++} ++for (let iteration = 0; iteration < Math.min(typeof testLoopCount === "number" ? testLoopCount : 20, 100); ++iteration) { ++ for (const [shape, create, line, column] of cases) { ++ checkPosition(create(), line, column, shape, false); ++ if (typeof process !== "undefined") { ++ const previous = Error.prepareStackTrace; ++ try { ++ Error.prepareStackTrace = (_, frames) => frames; ++ checkPosition(create(), line, column, shape, true); ++ } finally { ++ Error.prepareStackTrace = previous; ++ } ++ } ++ } ++} +diff --git a/test/js/node/vm/__snapshots__/vm-sourceUrl.test.ts.snap b/test/js/node/vm/__snapshots__/vm-sourceUrl.test.ts.snap +index bbc85154..43f86730 100644 +--- a/test/js/node/vm/__snapshots__/vm-sourceUrl.test.ts.snap ++++ b/test/js/node/vm/__snapshots__/vm-sourceUrl.test.ts.snap +@@ -16,7 +16,7 @@ exports[`can get sourceURL inside node:vm 1`] = ` + ^ + error: hello + at hello (hellohello.js:4:24) +- at hellohello.js:7:6 ++ at hellohello.js:7:1 + at (:21:15) + " + `; +@@ -26,7 +26,7 @@ exports[`eval sourceURL is correct 1`] = ` + ^ + error: hello + at hello (hellohello.js:4:24) +- at eval (hellohello.js:7:6) ++ at eval (hellohello.js:7:1) + at (:39:15) + " + `; diff --git a/.github/openclaw/qualification/patches/010-arraybuffer-accounting.patch b/.github/openclaw/qualification/patches/010-arraybuffer-accounting.patch new file mode 100644 index 0000000000000..bdbf1f837aefa --- /dev/null +++ b/.github/openclaw/qualification/patches/010-arraybuffer-accounting.patch @@ -0,0 +1,1428 @@ +diff --git a/CHANGELOG.md b/CHANGELOG.md +index 29d1cea0..1594a8d8 100644 +--- a/CHANGELOG.md ++++ b/CHANGELOG.md +@@ -2,6 +2,8 @@ + + ## Unreleased + ++- Account for typed-array and ArrayBuffer payloads before GC, preserve allocation-origin charges across worker transfers, separate external storage from JS heap statistics, and answer worker heap-statistics requests during JavaScript loops and atomic waits. Builds on oven-sh/bun#34406 and oven-sh/WebKit#303; thanks @robobun. ++ + - Align WebKit and VM source-position assertions with Node's constructor locations and select these regressions for native stack-formatting changes in both fork CI lanes. + + - Preserve the native-context count in the fast `v8.getHeapStatistics()` adapter and avoid appending a second strict code-generation flag to inherited worker arguments. +diff --git a/docs/runtime/nodejs-compat.mdx b/docs/runtime/nodejs-compat.mdx +index 0274c5b4..f59d8aae 100644 +--- a/docs/runtime/nodejs-compat.mdx ++++ b/docs/runtime/nodejs-compat.mdx +@@ -214,6 +214,8 @@ Socket close notifications use internal scheduling and continue to run when user + + ### [`node:process`](https://nodejs.org/api/process.html) + ++`process.memoryUsage().arrayBuffers` includes fixed backing storage and typed arrays before GC, including optimized allocations. Transferring a buffer moves its `external` payload charge to the receiving worker while its `arrayBuffers` charge stays with the allocating worker until storage is freed. Resizable buffers contribute their current payload to `external`; shared fixed backing storage contributes only to the allocator's `arrayBuffers`. Shared growable backing storage does not contribute to either field. Unshared WebAssembly memory contributes to `external` at allocation and growth, including before `.buffer` is accessed. External memory also includes native object overhead. Backing storage is excluded from `heapUsed` and `heapTotal`. ++ + `process.platform`, `arch`, `version`, `versions`, `pid`, and `release` expose Node-compatible data descriptors. `argv` and `execArgv` initialize lazily and support descriptor-based replacement. `ppid` and `title` retain their native behavior behind data descriptors. `exitCode` remains a non-configurable accessor. + + Initializing `process.stdout` or `process.stderr` does not retain a duplicate pipe writer. Explicitly closing the underlying descriptor publishes EOF while the process remains alive. On POSIX, socket and pipe stdio use nonblocking writes. `end()` and pipeline completion flush pending output and shut down the write side of socket stdio, so peers receive EOF before process exit. Actual pipes stay open until process exit or explicit descriptor closure. `destroy()` preserves fd 1/2; after POSIX socket or pipe finalization, later stream writes fail with `EPIPE`. On Windows, piped stdout/stderr reset their writable state after `end()` or pipeline completion and accept subsequent writes. +@@ -276,6 +278,8 @@ The same replay took Node 24 **39.7 ms at 2,500 modules and 63.4 ms at 4,000**. + + ### [`node:worker_threads`](https://nodejs.org/api/worker_threads.html) + ++`worker.getHeapStatistics()` takes an owning-thread snapshot while a worker is idle, executing JavaScript, or blocked in `Atomics.wait()`. The snapshot includes freshly allocated external storage. Inspection preserves the atomic wait and pending requests settle if the worker exits. ++ + File workers select `--require` and `--import` preloads from their environment independently of `execArgv`. Explicit `env` or `execArgv` options re-read the worker's `NODE_OPTIONS` and `BUN_OPTIONS` before applying the selected CLI arguments. `execArgv: []` removes CLI preloads while environment preloads still apply. Require preloads run before import preloads. + + `MessagePort.emit()` and `parentPort.emit()` preserve the emitted value for Node listeners. Web listeners receive `MessageEvent` objects for `message` and `messageerror`, and `CustomEvent` objects for other events. +diff --git a/src/js/node/v8.ts b/src/js/node/v8.ts +index 27da92d8..7907365a 100644 +--- a/src/js/node/v8.ts ++++ b/src/js/node/v8.ts +@@ -10,6 +10,8 @@ const { + stopGCProfiler, + discardGCProfiler, + getHeapUsage, ++ serialize: serializeNative, ++ bufferEnvelopeMagic: kBufferEnvelopeMagic, + queryObjects: queryHeapObjects, + } = $cpp("NodeV8.cpp", "Bun::createNodeV8Binding"); + +@@ -41,7 +43,6 @@ const FunctionPrototypeCall = Function.prototype.call; + const uncurryThis = func => FunctionPrototypeCall.bind(func); + const ObjectGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; + const Uint8ArrayCtor = Uint8Array; +-const BufferAllocUnsafe = Buffer.allocUnsafe; + const TypedArrayProto = Object.getPrototypeOf(Uint8ArrayCtor.prototype); + const TypedArrayPrototypeGetBuffer = uncurryThis(ObjectGetOwnPropertyDescriptor(TypedArrayProto, "buffer")!.get); + const TypedArrayPrototypeGetByteOffset = uncurryThis( +@@ -50,7 +51,6 @@ const TypedArrayPrototypeGetByteOffset = uncurryThis( + const TypedArrayPrototypeGetByteLength = uncurryThis( + ObjectGetOwnPropertyDescriptor(TypedArrayProto, "byteLength")!.get, + ); +-const TypedArrayPrototypeSet = uncurryThis(TypedArrayProto.set); + const DataViewPrototypeGetBuffer = uncurryThis(ObjectGetOwnPropertyDescriptor(DataView.prototype, "buffer")!.get); + const DataViewPrototypeGetByteOffset = uncurryThis( + ObjectGetOwnPropertyDescriptor(DataView.prototype, "byteOffset")!.get, +@@ -299,7 +299,6 @@ function getCppHeapStatistics(type = "detailed") { + // Buffer-bearing payloads are framed as MAGIC + version + SSV([value, buffers]) so deserialize + // can restore Buffer prototypes (see internal/serialization_buffers). Leading 0xFF cannot collide + // with bare SSV output; Buffer-free payloads stay bare SSV so older readers keep working. +-const kBufferEnvelopeMagic = [0xff, 0x42, 0x55, 0x4e, 0x01]; // 0xFF "BUN" v1 + + function hasBufferEnvelopeMagic(view) { + // In-bounds integer-indexed reads on a typed array never consult the +@@ -347,14 +346,7 @@ function stopCoverage() { + } + function serialize(arg1) { + const tagged = require("internal/serialization_buffers").tagBuffers(arg1); +- if (tagged === null) { +- return jsc.serialize(arg1, { binaryType: "nodebuffer" }); +- } +- const payload = jsc.serialize(tagged, { binaryType: "nodebuffer" }); +- const framed = BufferAllocUnsafe(kBufferEnvelopeMagic.length + TypedArrayPrototypeGetByteLength(payload)); +- for (let i = 0; i < kBufferEnvelopeMagic.length; i++) framed[i] = kBufferEnvelopeMagic[i]; +- TypedArrayPrototypeSet(framed, payload, kBufferEnvelopeMagic.length); +- return framed; ++ return serializeNative(tagged === null ? arg1 : tagged, tagged !== null); + } + + function getDefaultHeapSnapshotPath() { +diff --git a/src/jsc/JSValue.rs b/src/jsc/JSValue.rs +index 70c5c377..a45f34bb 100644 +--- a/src/jsc/JSValue.rs ++++ b/src/jsc/JSValue.rs +@@ -592,6 +592,7 @@ impl JSValue { + } else { + Some(MarkedArrayBuffer_deallocator) + }, ++ crate::array_buffer::ArrayBufferAllocationMode::RuntimeOwned, + ) + }) + } +@@ -619,6 +620,7 @@ impl JSValue { + } else { + Some(MarkedArrayBuffer_deallocator) + }, ++ crate::array_buffer::ArrayBufferAllocationMode::RuntimeOwned, + ) + }) + } +@@ -646,6 +648,7 @@ impl JSValue { + len, + ctx, + Some(free), ++ crate::array_buffer::ArrayBufferAllocationMode::RuntimeOwned, + ) + }) + } +@@ -2005,6 +2008,7 @@ unsafe extern "C" { + len: usize, + ctx: *mut c_void, + deallocator: Option, ++ allocation: crate::array_buffer::ArrayBufferAllocationMode, + ) -> JSValue; + safe fn JSBuffer__bufferFromLength(global: &JSGlobalObject, len: i64) -> JSValue; + safe fn JSC__JSValue__dateInstanceFromNumber(global: &JSGlobalObject, n: f64) -> JSValue; +diff --git a/src/jsc/array_buffer.rs b/src/jsc/array_buffer.rs +index 149d4671..233adbb0 100644 +--- a/src/jsc/array_buffer.rs ++++ b/src/jsc/array_buffer.rs +@@ -13,6 +13,14 @@ bun_core::declare_scope!(ArrayBuffer, visible); + /// when a zero-copy ArrayBuffer/typed array backing store is collected. + pub type JSTypedArrayBytesDeallocator = Option; + ++/// Matches JSC::ArrayBufferAllocationMode. Foreign storage does not acquire an allocator-origin charge. ++#[repr(u8)] ++#[derive(Clone, Copy)] ++pub enum ArrayBufferAllocationMode { ++ External = 0, ++ RuntimeOwned = 1, ++} ++ + // ────────────────────────────────────────────────────────────────────────── + // ArrayBuffer + // ────────────────────────────────────────────────────────────────────────── +@@ -115,6 +123,7 @@ unsafe extern "C" { + len: usize, + dealloc: JSTypedArrayBytesDeallocator, + ctx: *mut c_void, ++ allocation: ArrayBufferAllocationMode, + ) -> JSValue; + fn Bun__makeTypedArrayWithBytesNoCopy( + global: &JSGlobalObject, +@@ -123,6 +132,7 @@ unsafe extern "C" { + len: usize, + dealloc: JSTypedArrayBytesDeallocator, + ctx: *mut c_void, ++ allocation: ArrayBufferAllocationMode, + ) -> JSValue; + fn Bun__createTypedArrayForCopy( + global: *const JSGlobalObject, +@@ -459,6 +469,7 @@ impl ArrayBuffer { + // The deallocator ignores its ctx (mi_free needs no ctx). Any non-null + // sentinel would do; pass the data ptr itself. + self.ptr.cast(), ++ ArrayBufferAllocationMode::RuntimeOwned, + ) + }; + } +@@ -472,6 +483,7 @@ impl ArrayBuffer { + self.byte_len, + Some(MarkedArrayBuffer_deallocator), + self.ptr.cast(), ++ ArrayBufferAllocationMode::RuntimeOwned, + ) + } + } +@@ -504,6 +516,7 @@ impl ArrayBuffer { + self.byte_len, + None, + ptr::null_mut(), ++ ArrayBufferAllocationMode::External, + ) + }; + } +@@ -517,6 +530,7 @@ impl ArrayBuffer { + self.byte_len, + None, + ptr::null_mut(), ++ ArrayBufferAllocationMode::External, + ) + }; + } +@@ -542,6 +556,7 @@ impl ArrayBuffer { + ctx: &JSGlobalObject, + deallocator: *mut c_void, + callback: JSTypedArrayBytesDeallocator, ++ allocation: ArrayBufferAllocationMode, + ) -> JsResult { + if !self.value.is_empty() { + return Ok(self.value); +@@ -557,6 +572,7 @@ impl ArrayBuffer { + self.byte_len, + callback, + deallocator, ++ allocation, + ) + }; + } +@@ -570,6 +586,7 @@ impl ArrayBuffer { + self.byte_len, + callback, + deallocator, ++ allocation, + ) + } + } +@@ -1017,12 +1034,20 @@ pub(crate) unsafe fn make_array_buffer_with_bytes_no_copy( + len: usize, + deallocator: JSTypedArrayBytesDeallocator, + deallocator_context: *mut c_void, ++ allocation: ArrayBufferAllocationMode, + ) -> JsResult { + crate::host_fn::from_js_host_call(global, || { + // SAFETY: forwarded verbatim; the caller upholds this function's + // contract (`ptr` valid for `len` bytes until `deallocator` runs). + unsafe { +- Bun__makeArrayBufferWithBytesNoCopy(global, ptr, len, deallocator, deallocator_context) ++ Bun__makeArrayBufferWithBytesNoCopy( ++ global, ++ ptr, ++ len, ++ deallocator, ++ deallocator_context, ++ allocation, ++ ) + } + }) + } +@@ -1042,6 +1067,7 @@ pub unsafe fn make_typed_array_with_bytes_no_copy( + len: usize, + deallocator: JSTypedArrayBytesDeallocator, + deallocator_context: *mut c_void, ++ allocation: ArrayBufferAllocationMode, + ) -> JsResult { + crate::host_fn::from_js_host_call(global, || { + // SAFETY: forwarded verbatim; the caller upholds this function's +@@ -1054,6 +1080,7 @@ pub unsafe fn make_typed_array_with_bytes_no_copy( + len, + deallocator, + deallocator_context, ++ allocation, + ) + } + }) +diff --git a/src/jsc/bindings/BunClientData.cpp b/src/jsc/bindings/BunClientData.cpp +index d56add18..915a1de9 100644 +--- a/src/jsc/bindings/BunClientData.cpp ++++ b/src/jsc/bindings/BunClientData.cpp +@@ -63,7 +63,6 @@ JSVMClientData::JSVMClientData(VM& vm, RefPtr sourceProvide + , CLIENT_ISO_SUBSPACE_INIT(m_domConstructorSpace) + , CLIENT_ISO_SUBSPACE_INIT(m_domNamespaceObjectSpace) + , m_clientSubspaces(makeUnique()) +- , m_heapUsage(vm.heap) + { + } + +diff --git a/src/jsc/bindings/BunClientData.h b/src/jsc/bindings/BunClientData.h +index fd9e452ecd4fa6176785ab2a9618f4ae96f34cb5..abc88abf4ee0afed212312506f2c58b4d24d1806 100644 +--- a/src/jsc/bindings/BunClientData.h ++++ b/src/jsc/bindings/BunClientData.h +@@ -76,7 +76,6 @@ class DOMWrapperWorld; + #include "NodeVMOptionNames.h" + #include "NodeVMSourceOriginCache.h" + #include "NodeVMCompilationCache.h" +-#include + namespace Zig { + class GlobalObject; + } +@@ -84,47 +83,6 @@ class GlobalObject; + namespace Bun { + class StrongRootBlock; + +-// Add allocations since the last collection to its survivor size. Heap::size() +-// counts mark bits and misses newly allocated cells and array backing storage. +-class HeapUsage final : public JSC::HeapObserver { +- WTF_MAKE_NONCOPYABLE(HeapUsage); +- +-public: +- explicit HeapUsage(JSC::Heap& heap) +- : m_heap(heap) +- { +- m_heap.addObserver(this); +- } +- +- ~HeapUsage() final +- { +- m_heap.removeObserver(this); +- } +- +- size_t get() const +- { +- return m_sizeAfterLastCollection + (m_heap.totalBytesAllocated() - m_allocatedAtLastCollection); +- } +- +-private: +- void willGarbageCollect() final {} +- +- // Heap::didFinishCollection() notifies observers after updateAllocationLimits() +- // stored this collection's size, in the end phase of the collection, while +- // the mutator is stopped. The mutator reads m_sizeAfterLastCollection once +- // it resumes, the same way it reads JSC's own counters. +- void didGarbageCollect(JSC::CollectionScope scope) final +- { +- m_sizeAfterLastCollection = scope == JSC::CollectionScope::Full +- ? m_heap.sizeAfterLastFullCollection() +- : m_heap.sizeAfterLastEdenCollection(); +- m_allocatedAtLastCollection = m_heap.totalBytesAllocated(); +- } +- +- JSC::Heap& m_heap; +- size_t m_sizeAfterLastCollection { 0 }; +- uint64_t m_allocatedAtLastCollection { 0 }; +-}; + } + + namespace JSC { +@@ -250,7 +208,6 @@ public: + Bun::NodeVMSourceOriginCache& nodeVMSourceOriginCache() { return m_nodeVMSourceOriginCache; } + + // Survivor size plus allocations since the last collection; mutator thread only. +- size_t heapUsage() const { return m_heapUsage.get(); } + + // The VM's default (first) Zig::GlobalObject: what defaultGlobalObject(JSC::VM&) returns on threads whose thread-local + // default is not this VM's, e.g. the collector thread running a collection's end phase. gcProtect'ed for the VM's life. +@@ -351,8 +308,6 @@ public: + Bun::NodeVMCompilationCache nodeVMCompilationCache; + + private: +- Bun::HeapUsage m_heapUsage; +- + SentinelLinkedList> m_clients; + bool m_isWorkerVM { false }; + bool m_isNodeWorkerVM { false }; +diff --git a/src/jsc/bindings/BunProcess.cpp b/src/jsc/bindings/BunProcess.cpp +index 3dec12b1..0faab265 100644 +--- a/src/jsc/bindings/BunProcess.cpp ++++ b/src/jsc/bindings/BunProcess.cpp +@@ -4198,26 +4198,14 @@ JSC_DEFINE_HOST_FUNCTION(Process_functionMemoryUsage, (JSC::JSGlobalObject * glo + // arrayBuffers: 9386 + // } + +- size_t heapUsed = WebCore::clientData(vm)->heapUsage(); +- size_t heapTotal = std::max(vm.heap.capacity(), heapUsed); ++ size_t heapUsed = vm.heap.jsHeapSizeForReporting(); ++ size_t heapTotal = vm.heap.jsHeapCapacityForReporting(); + result->putDirectOffset(vm, 0, JSC::jsNumber(current_rss)); + result->putDirectOffset(vm, 1, JSC::jsNumber(heapTotal)); + + result->putDirectOffset(vm, 2, JSC::jsNumber(heapUsed)); + +- result->putDirectOffset(vm, 3, JSC::jsNumber(vm.heap.extraMemorySize() + vm.heap.externalMemorySize())); +- +- // JSC won't count this number until vm.heap.addReference() is called. +- // That will only happen in cases like: +- // - new ArrayBuffer() +- // - new Uint8Array(42).buffer +- // - fs.readFile(path, "utf-8") (sometimes) +- // - ... +- // +- // But it won't happen in cases like: +- // - new Uint8Array(42) +- // - Buffer.alloc(42) +- // - new Uint8Array(42).slice() ++ result->putDirectOffset(vm, 3, JSC::jsNumber(vm.heap.externalMemorySizeForReporting())); + result->putDirectOffset(vm, 4, JSC::jsNumber(vm.heap.arrayBufferSize())); + + RELEASE_AND_RETURN(throwScope, JSC::JSValue::encode(result)); +diff --git a/src/jsc/bindings/JSBuffer.cpp b/src/jsc/bindings/JSBuffer.cpp +index 95986d0d..91ec703d 100644 +--- a/src/jsc/bindings/JSBuffer.cpp ++++ b/src/jsc/bindings/JSBuffer.cpp +@@ -414,7 +414,7 @@ bool Bun::rejectBytesNoCopyAboveArrayBufferLimit(JSC::JSGlobalObject* globalObje + return true; + } + +-JSC::EncodedJSValue JSBuffer__bufferFromPointerAndLengthAndDeinit(JSC::JSGlobalObject* lexicalGlobalObject, char* ptr, size_t length, void* ctx, JSTypedArrayBytesDeallocator bytesDeallocator) ++JSC::EncodedJSValue JSBuffer__bufferFromPointerAndLengthAndDeinit(JSC::JSGlobalObject* lexicalGlobalObject, char* ptr, size_t length, void* ctx, JSTypedArrayBytesDeallocator bytesDeallocator, JSC::ArrayBufferAllocationMode allocationMode) + { + JSC::JSUint8Array* uint8Array = nullptr; + +@@ -429,7 +429,8 @@ JSC::EncodedJSValue JSBuffer__bufferFromPointerAndLengthAndDeinit(JSC::JSGlobalO + + auto buffer = ArrayBuffer::createFromBytes({ reinterpret_cast(ptr), length }, createSharedTask([=](void* p) { + bytesDeallocator(p, ctx); +- })); ++ }), ++ allocationMode); + + uint8Array = JSC::JSUint8Array::create(lexicalGlobalObject, subclassStructure, WTF::move(buffer), 0, length); + } else { +@@ -2630,7 +2631,8 @@ extern "C" JSC::EncodedJSValue JSBuffer__fromMmap(Zig::GlobalObject* globalObjec + + auto buffer = ArrayBuffer::createFromBytes({ static_cast(ptr), length }, createSharedTask([lengthAsContext](void* p) { + unmapBufferBytes(p, lengthAsContext); +- })); ++ }), ++ JSC::ArrayBufferAllocationMode::RuntimeOwned); + + auto* view = JSC::JSUint8Array::create(globalObject, structure, WTF::move(buffer), 0, length); + RETURN_IF_EXCEPTION(scope, {}); +diff --git a/src/jsc/bindings/JSBuffer.h b/src/jsc/bindings/JSBuffer.h +index 2104d8a5..8b783747 100644 +--- a/src/jsc/bindings/JSBuffer.h ++++ b/src/jsc/bindings/JSBuffer.h +@@ -23,6 +23,7 @@ + #include "root.h" + + #include ++#include + #include + #include + #include +@@ -31,7 +32,7 @@ + #include "headers-handwritten.h" + + extern "C" JSC::EncodedJSValue JSBuffer__bufferFromLength(JSC::JSGlobalObject* lexicalGlobalObject, int64_t length); +-extern "C" JSC::EncodedJSValue JSBuffer__bufferFromPointerAndLengthAndDeinit(JSC::JSGlobalObject* lexicalGlobalObject, char* ptr, size_t length, void* ctx, JSTypedArrayBytesDeallocator bytesDeallocator); ++extern "C" JSC::EncodedJSValue JSBuffer__bufferFromPointerAndLengthAndDeinit(JSC::JSGlobalObject* lexicalGlobalObject, char* ptr, size_t length, void* ctx, JSTypedArrayBytesDeallocator bytesDeallocator, JSC::ArrayBufferAllocationMode); + extern "C" JSC::EncodedJSValue Bun__encoding__toString(const uint8_t* input, size_t len, JSC::JSGlobalObject* globalObject, Encoding encoding); + extern "C" JSC::EncodedJSValue Bun__encoding__toStringUTF8(const uint8_t* input, size_t len, JSC::JSGlobalObject* globalObject); + extern "C" bool Bun__Buffer_fill(EncodedSlice*, void*, size_t, WebCore::BufferEncodingType); +diff --git a/src/jsc/bindings/NodeV8.cpp b/src/jsc/bindings/NodeV8.cpp +index f714bf4a..d5a75f2f 100644 +--- a/src/jsc/bindings/NodeV8.cpp ++++ b/src/jsc/bindings/NodeV8.cpp +@@ -5,11 +5,14 @@ + #include "ErrorCode.h" + #include "BunClientData.h" + #include "NodeV8.h" ++#include "MessagePort.h" ++#include "SerializedScriptValue.h" + #include "ZigGlobalObject.h" + + #include + #include + #include ++#include + #include + #include + #include +@@ -17,21 +20,44 @@ + #include + #include + #include ++#include + + namespace Bun { + + using namespace JSC; + ++static constexpr std::array bufferEnvelopeMagic { 0xff, 0x42, 0x55, 0x4e, 0x01 }; ++ ++JSC_DEFINE_HOST_FUNCTION(functionSerializeForNode, (JSGlobalObject * globalObject, CallFrame* callFrame)) ++{ ++ auto& vm = globalObject->vm(); ++ auto scope = DECLARE_THROW_SCOPE(vm); ++ auto* domGlobal = uncheckedDowncast(globalObject); ++ Vector> transfers; ++ Vector> ports; ++ auto result = WebCore::SerializedScriptValue::create(*domGlobal, callFrame->argument(0), WTF::move(transfers), ports, WebCore::SerializationForStorage::Yes); ++ EXCEPTION_ASSERT(result.hasException() == !!scope.exception()); ++ if (result.hasException()) { ++ WebCore::propagateException(*domGlobal, scope, result.releaseException()); ++ RELEASE_AND_RETURN(scope, {}); ++ } ++ auto serialized = result.releaseReturnValue(); ++ auto prefix = callFrame->argument(1).asBoolean() ? std::span(bufferEnvelopeMagic) : std::span(); ++ auto buffer = serialized->toArrayBuffer(ArrayBufferSharingMode::Default, prefix); ++ size_t length = buffer->byteLength(); ++ RELEASE_AND_RETURN(scope, JSValue::encode(JSUint8Array::create(globalObject, domGlobal->JSBufferSubclassStructure(), WTF::move(buffer), 0, length))); ++} ++ + JSC_DEFINE_HOST_FUNCTION(functionGetHeapUsage, (JSGlobalObject * globalObject, CallFrame*)) + { + auto& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); +- size_t used = WebCore::clientData(vm)->heapUsage(); +- size_t capacity = std::max(vm.heap.capacity(), used); ++ size_t used = vm.heap.jsHeapSizeForReporting(); ++ size_t capacity = vm.heap.jsHeapCapacityForReporting(); + JSObject* result = constructEmptyObject(globalObject); + result->putDirect(vm, Identifier::fromString(vm, "heapSize"_s), jsNumber(used)); + result->putDirect(vm, Identifier::fromString(vm, "heapCapacity"_s), jsNumber(capacity)); +- result->putDirect(vm, Identifier::fromString(vm, "extraMemorySize"_s), jsNumber(vm.heap.extraMemorySize() + vm.heap.externalMemorySize())); ++ result->putDirect(vm, Identifier::fromString(vm, "extraMemorySize"_s), jsNumber(vm.heap.externalMemorySizeForReporting())); + result->putDirect(vm, Identifier::fromString(vm, "globalObjectCount"_s), jsNumber(vm.heap.globalObjectCount())); + RELEASE_AND_RETURN(scope, JSValue::encode(result)); + } +@@ -156,7 +182,16 @@ JSC_DEFINE_HOST_FUNCTION(functionStopGCProfiler, (JSGlobalObject * globalObject, + JSC::JSObject* createNodeV8Binding(JSC::JSGlobalObject* globalObject) + { + auto& vm = JSC::getVM(globalObject); ++ auto scope = DECLARE_THROW_SCOPE(vm); + JSC::JSObject* object = JSC::constructEmptyObject(vm, globalObject->nullPrototypeObjectStructure()); ++ auto* magic = constructEmptyArray(globalObject, nullptr, bufferEnvelopeMagic.size()); ++ RETURN_IF_EXCEPTION(scope, nullptr); ++ for (unsigned i = 0; i < bufferEnvelopeMagic.size(); ++i) { ++ magic->putDirectIndex(globalObject, i, jsNumber(bufferEnvelopeMagic[i])); ++ RETURN_IF_EXCEPTION(scope, nullptr); ++ } ++ object->putDirect(vm, Identifier::fromString(vm, "bufferEnvelopeMagic"_s), magic); ++ object->putDirectNativeFunction(vm, globalObject, Identifier::fromString(vm, "serialize"_s), 2, functionSerializeForNode, ImplementationVisibility::Public, NoIntrinsic, 0); + object->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "queryObjects"_s), 1, functionQueryObjects, ImplementationVisibility::Public, JSC::NoIntrinsic, 0); + object->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "getHeapUsage"_s), 0, functionGetHeapUsage, ImplementationVisibility::Public, JSC::NoIntrinsic, 0); + object->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "isStringOneByteRepresentation"_s), 1, functionIsStringOneByteRepresentation, ImplementationVisibility::Public, JSC::NoIntrinsic, 0); +diff --git a/src/jsc/bindings/ScriptExecutionContext.cpp b/src/jsc/bindings/ScriptExecutionContext.cpp +index d5ee798b..43a1527d 100644 +--- a/src/jsc/bindings/ScriptExecutionContext.cpp ++++ b/src/jsc/bindings/ScriptExecutionContext.cpp +@@ -303,6 +303,24 @@ bool ScriptExecutionContext::postTaskTo(ScriptExecutionContextIdentifier identif + return true; + } + ++bool ScriptExecutionContext::postVMInspection(ScriptExecutionContextIdentifier identifier, Function&& inspection) ++{ ++ const BunVmHandleRef* retained = nullptr; ++ { ++ Locker locker { allScriptExecutionContextsMapLock }; ++ auto* context = allScriptExecutionContextsMap().get(identifier); ++ if (!context || context->isTerminating()) ++ return false; ++ context->m_vm->traps().requestVMInspection(WTF::move(inspection)); ++ retained = Bun__VmHandle__retainRef(context->m_vmHandle); ++ } ++ Bun__VmHandle__postAndRelease(retained, new EventLoopTask([](ScriptExecutionContext& context) { ++ context.vm().traps().runVMInspections(); ++ }), ++ BunLoopKind::Regular); ++ return true; ++} ++ + void ScriptExecutionContext::didCreateDestructionObserver(ContextDestructionObserver& observer) + { + #if ASSERT_ENABLED +@@ -444,10 +462,8 @@ void ScriptExecutionContext::removeFromContextsMap() + + void ScriptExecutionContext::markTerminating() + { +- // An early-out for postTaskTo(): from here posts to this context are pointless. Not +- // a fence — a poster that looked us up just before this still posts, and the VM +- // handle deals with it (queued and released unrun by the teardown, or refused and +- // deleted once the handle is closed). ++ // Fence VM-inspection registration before the owning thread starts tearing down JSC. ++ Locker locker { allScriptExecutionContextsMapLock }; + m_isTerminating.store(true, std::memory_order_release); + } + +diff --git a/src/jsc/bindings/ScriptExecutionContext.h b/src/jsc/bindings/ScriptExecutionContext.h +index 7c7b4de1..7519df43 100644 +--- a/src/jsc/bindings/ScriptExecutionContext.h ++++ b/src/jsc/bindings/ScriptExecutionContext.h +@@ -131,6 +131,7 @@ public: + // target's thread when the work whose completion this is was initiated, or Regular for work no + // script there initiated. + WEBCORE_EXPORT static bool postTaskTo(ScriptExecutionContextIdentifier identifier, BunLoopKind loopKind, Function&& task); ++ static bool postVMInspection(ScriptExecutionContextIdentifier, Function&&); + WEBCORE_EXPORT static bool ensureOnContextThread(ScriptExecutionContextIdentifier, Function&& task); + + WEBCORE_EXPORT JSC::JSGlobalObject* globalObject(); +diff --git a/src/jsc/bindings/Uint8Array.cpp b/src/jsc/bindings/Uint8Array.cpp +index 5b4171ab..19b47bb5 100644 +--- a/src/jsc/bindings/Uint8Array.cpp ++++ b/src/jsc/bindings/Uint8Array.cpp +@@ -24,7 +24,8 @@ extern "C" JSC::EncodedJSValue JSUint8Array__fromDefaultAllocator(JSC::JSGlobalO + + auto buffer = ArrayBuffer::createFromBytes({ ptr, length }, createSharedTask([](void* p) { + freeDefaultAllocatorBytes(p, nullptr); +- })); ++ }), ++ JSC::ArrayBufferAllocationMode::RuntimeOwned); + + uint8Array = JSC::JSUint8Array::create(lexicalGlobalObject, lexicalGlobalObject->typedArrayStructureWithTypedArrayType(), WTF::move(buffer), 0, length); + } else { +@@ -47,7 +48,8 @@ extern "C" JSC::EncodedJSValue JSArrayBuffer__fromDefaultAllocator(JSC::JSGlobal + + buffer = ArrayBuffer::createFromBytes({ ptr, length }, createSharedTask([](void* p) { + freeDefaultAllocatorBytes(p, nullptr); +- })); ++ }), ++ JSC::ArrayBufferAllocationMode::RuntimeOwned); + } else { + buffer = ArrayBuffer::create(0, 1); + } +diff --git a/src/jsc/bindings/ZigGlobalObject.cpp b/src/jsc/bindings/ZigGlobalObject.cpp +index 2c61e485..1d5127e6 100644 +--- a/src/jsc/bindings/ZigGlobalObject.cpp ++++ b/src/jsc/bindings/ZigGlobalObject.cpp +@@ -1494,7 +1494,8 @@ extern "C" JSC::EncodedJSValue ArrayBuffer__fromSharedMemfd(int64_t fd, JSC::JSG + + auto buffer = ArrayBuffer::createFromBytes({ reinterpret_cast(reinterpret_cast(ptr) + byteOffset), byteLength }, createSharedTask([ptr, totalLength](void* p) { + munmap(ptr, totalLength); +- })); ++ }), ++ JSC::ArrayBufferAllocationMode::RuntimeOwned); + + if (type == JSC::Uint8ArrayType) { + auto uint8array = JSC::JSUint8Array::create(globalObject, globalObject->m_typedArrayUint8.get(globalObject), WTF::move(buffer), 0, byteLength); +@@ -1576,7 +1577,7 @@ extern "C" JSC::EncodedJSValue Bun__createUint8ArrayForCopy(JSC::JSGlobalObject* + RELEASE_AND_RETURN(scope, JSValue::encode(array)); + } + +-extern "C" JSC::EncodedJSValue Bun__makeArrayBufferWithBytesNoCopy(JSC::JSGlobalObject* globalObject, const void* ptr, size_t len, JSTypedArrayBytesDeallocator deallocator, void* deallocatorContext) ++extern "C" JSC::EncodedJSValue Bun__makeArrayBufferWithBytesNoCopy(JSC::JSGlobalObject* globalObject, const void* ptr, size_t len, JSTypedArrayBytesDeallocator deallocator, void* deallocatorContext, JSC::ArrayBufferAllocationMode allocationMode) + { + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); +@@ -1586,14 +1587,15 @@ extern "C" JSC::EncodedJSValue Bun__makeArrayBufferWithBytesNoCopy(JSC::JSGlobal + + auto buffer = ArrayBuffer::createFromBytes({ static_cast(ptr), len }, createSharedTask([=](void* p) { + if (deallocator) deallocator(p, deallocatorContext); +- })); ++ }), ++ allocationMode); + + JSArrayBuffer* jsBuffer = JSArrayBuffer::create(vm, globalObject->arrayBufferStructure(ArrayBufferSharingMode::Default), WTF::move(buffer)); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsBuffer); + } + +-extern "C" JSC::EncodedJSValue Bun__makeTypedArrayWithBytesNoCopy(JSC::JSGlobalObject* globalObject, TypedArrayType ty, const void* ptr, size_t len, JSTypedArrayBytesDeallocator deallocator, void* deallocatorContext) ++extern "C" JSC::EncodedJSValue Bun__makeTypedArrayWithBytesNoCopy(JSC::JSGlobalObject* globalObject, TypedArrayType ty, const void* ptr, size_t len, JSTypedArrayBytesDeallocator deallocator, void* deallocatorContext, JSC::ArrayBufferAllocationMode allocationMode) + { + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); +@@ -1603,7 +1605,8 @@ extern "C" JSC::EncodedJSValue Bun__makeTypedArrayWithBytesNoCopy(JSC::JSGlobalO + + auto buffer_ = ArrayBuffer::createFromBytes({ static_cast(ptr), len }, createSharedTask([=](void* p) { + if (deallocator) deallocator(p, deallocatorContext); +- })); ++ }), ++ allocationMode); + RefPtr&& buffer = WTF::move(buffer_); + if (!buffer) { + throwOutOfMemoryError(globalObject, scope); +diff --git a/src/jsc/bindings/sqlite/JSSQLStatement.cpp b/src/jsc/bindings/sqlite/JSSQLStatement.cpp +index 949228d7..e75aa96d 100644 +--- a/src/jsc/bindings/sqlite/JSSQLStatement.cpp ++++ b/src/jsc/bindings/sqlite/JSSQLStatement.cpp +@@ -1404,7 +1404,7 @@ JSC_DEFINE_HOST_FUNCTION(jsSQLStatementSerialize, (JSC::JSGlobalObject * lexical + return {}; + } + +- RELEASE_AND_RETURN(scope, JSBuffer__bufferFromPointerAndLengthAndDeinit(lexicalGlobalObject, reinterpret_cast(data), static_cast(length), NULL, sqlite_free_typed_array)); ++ RELEASE_AND_RETURN(scope, JSBuffer__bufferFromPointerAndLengthAndDeinit(lexicalGlobalObject, reinterpret_cast(data), static_cast(length), NULL, sqlite_free_typed_array, JSC::ArrayBufferAllocationMode::External)); + } + + JSC_DEFINE_HOST_FUNCTION(jsSQLStatementLoadExtensionFunction, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) +diff --git a/src/jsc/bindings/webcore/JSWorker.cpp b/src/jsc/bindings/webcore/JSWorker.cpp +index 94ffaa2f..a244c0b1 100644 +--- a/src/jsc/bindings/webcore/JSWorker.cpp ++++ b/src/jsc/bindings/webcore/JSWorker.cpp +@@ -1041,11 +1041,10 @@ static inline JSC::EncodedJSValue jsWorkerPrototypeFunction_getHeapStatisticsBod + uint64_t reqId = worker.contextProxy().registerCrossVMRequest(vm, promise); + auto parentId = globalObject->scriptExecutionContext()->identifier(); + auto parentLoopKind = globalObject->scriptExecutionContext()->currentLoopKind(); +- bool accepted = worker.contextProxy().postTaskToWorkerGlobalScope([reqId, parentId, parentLoopKind, protectedProxy = Ref { worker.contextProxy() }](ScriptExecutionContext& workerCtx) mutable { +- auto& wvm = workerCtx.vm(); +- double heapSize = static_cast(WebCore::clientData(wvm)->heapUsage()); +- double capacity = std::max(static_cast(wvm.heap.capacity()), heapSize); +- double extra = static_cast(wvm.heap.extraMemorySize() + wvm.heap.externalMemorySize()); ++ bool accepted = worker.contextProxy().postVMInspection([reqId, parentId, parentLoopKind, protectedProxy = Ref { worker.contextProxy() }](JSC::VM& wvm) mutable { ++ double heapSize = static_cast(wvm.heap.jsHeapSizeForReporting()); ++ double capacity = static_cast(wvm.heap.jsHeapCapacityForReporting()); ++ double extra = static_cast(wvm.heap.externalMemorySizeForReporting()); + ScriptExecutionContext::postTaskTo(parentId, parentLoopKind, [reqId, protectedProxy = WTF::move(protectedProxy), heapSize, capacity, extra](ScriptExecutionContext& parentCtx) { + resolveCrossVMRequest(protectedProxy.get(), reqId, parentCtx, [&](VM& pvm, JSGlobalObject* go) -> JSValue { + JSObject* o = constructEmptyObject(go); +diff --git a/src/jsc/bindings/webcore/MessageEvent.cpp b/src/jsc/bindings/webcore/MessageEvent.cpp +index 7847f3bc..f2aa8062 100644 +--- a/src/jsc/bindings/webcore/MessageEvent.cpp ++++ b/src/jsc/bindings/webcore/MessageEvent.cpp +@@ -94,11 +94,11 @@ auto MessageEvent::create(JSC::JSGlobalObject& globalObject, Ref strongData(vm, deserialized); + +- auto event = adoptRef(*new MessageEvent(eventNames().messageEvent, WTF::move(data), origin, lastEventId, WTF::move(source), WTF::move(ports))); ++ auto event = adoptRef(*new MessageEvent(eventNames().messageEvent, JSValueTag {}, origin, lastEventId, WTF::move(source), WTF::move(ports))); + JSC::Strong strongWrapper(vm, uncheckedDowncast(toJS(&globalObject, uncheckedDowncast(&globalObject), event.get()))); + RETURN_IF_EXCEPTION(scope, std::nullopt); +- // Since we've already deserialized the SerializedScriptValue, cache the result so we don't have to deserialize +- // again the next time JSMessageEvent::data() gets called by the main world. ++ // Keep the decoded value, not the transport storage and its pre-transfer memory estimate. ++ event->jsData().set(vm, strongWrapper.get(), deserialized); + event->cachedData().set(vm, strongWrapper.get(), deserialized); + + return MessageEventWithStrongData { event, WTF::move(strongWrapper) }; +diff --git a/src/jsc/bindings/webcore/SerializedScriptValue.cpp b/src/jsc/bindings/webcore/SerializedScriptValue.cpp +index 4a9e48b8..56138416 100644 +--- a/src/jsc/bindings/webcore/SerializedScriptValue.cpp ++++ b/src/jsc/bindings/webcore/SerializedScriptValue.cpp +@@ -4848,8 +4848,10 @@ Ref SerializedScriptValue::createDoubleArrayFastPath(Vect + return adoptRef(*new SerializedScriptValue(WTF::move(data), length, FastPath::DoubleArray)); + } + +-Ref SerializedScriptValue::toArrayBuffer() ++Ref SerializedScriptValue::toArrayBuffer(JSC::ArrayBufferSharingMode sharingMode, std::span prefix) + { ++ // Framing is applied before this serialization's backing storage is exposed. ++ m_data.insertSpan(0, prefix); + if (this->m_data.size() == 0) { + return ArrayBuffer::create(static_cast(0), static_cast(1)); + } +@@ -4861,7 +4863,8 @@ Ref SerializedScriptValue::toArrayBuffer() + })); + + // Note: using the SharedArrayBufferContents::create function directly didn't work. +- arrayBuffer->makeShared(); ++ if (sharingMode == JSC::ArrayBufferSharingMode::Shared) ++ arrayBuffer->makeShared(); + + return arrayBuffer; + } +diff --git a/src/jsc/bindings/webcore/SerializedScriptValue.h b/src/jsc/bindings/webcore/SerializedScriptValue.h +index 7f3665aa3d96c3c7da437006d0539a14bc8fffac..38a38b9cc6c2eb2f9fa4234a16cd17761207c920 100644 +--- a/src/jsc/bindings/webcore/SerializedScriptValue.h ++++ b/src/jsc/bindings/webcore/SerializedScriptValue.h +@@ -139,7 +139,7 @@ public: + + JSC::JSValue deserialize(JSC::JSGlobalObject&, JSC::JSGlobalObject*, const Vector>&, const Vector& blobURLs, const Vector& blobFilePaths, SerializationErrorMode = SerializationErrorMode::Throwing, bool* didFail = nullptr); + +- WEBCORE_EXPORT Ref toArrayBuffer(); ++ WEBCORE_EXPORT Ref toArrayBuffer(JSC::ArrayBufferSharingMode = JSC::ArrayBufferSharingMode::Shared, std::span prefix = {}); + static JSC::JSValue fromArrayBuffer(JSC::JSGlobalObject&, JSC::JSGlobalObject*, JSC::ArrayBuffer* arrayBuffer, size_t byteOffset = 0, size_t maxByteLength = 0, SerializationErrorMode = SerializationErrorMode::Throwing, bool* didFail = nullptr); + + static Ref createFromWireBytes(Vector&& data) +diff --git a/src/jsc/bindings/webcore/WorkerMessagingProxy.cpp b/src/jsc/bindings/webcore/WorkerMessagingProxy.cpp +index 4836aed2..138f72eb 100644 +--- a/src/jsc/bindings/webcore/WorkerMessagingProxy.cpp ++++ b/src/jsc/bindings/webcore/WorkerMessagingProxy.cpp +@@ -304,6 +304,16 @@ bool WorkerMessagingProxy::postTaskToWorkerGlobalScope(Function&& inspection) ++{ ++ { ++ Locker lock { m_pendingTasksLock }; ++ if (m_state.load() >= State::Closing || !m_workerVMReady) ++ return false; ++ } ++ return ScriptExecutionContext::postVMInspection(m_workerContextIdentifier, WTF::move(inspection)); ++} ++ + uint64_t WorkerMessagingProxy::registerCrossVMRequest(JSC::VM& vm, JSC::JSPromise* promise) + { + uint64_t id = m_nextRequestId.fetch_add(1); +@@ -452,6 +462,7 @@ void WorkerMessagingProxy::workerThreadStarted() + Locker lock { m_pendingTasksLock }; + if (m_state.load() != State::Pending) + return; ++ m_workerVMReady = true; + } + ScriptExecutionContext::postTaskTo(m_loaderContextIdentifier, m_loaderLoopKind, [protectedThis = Ref { *this }](ScriptExecutionContext&) { + RefPtr workerObject = protectedThis->m_workerObject; +diff --git a/src/jsc/bindings/webcore/WorkerMessagingProxy.h b/src/jsc/bindings/webcore/WorkerMessagingProxy.h +index f2e8ea66..b938d071 100644 +--- a/src/jsc/bindings/webcore/WorkerMessagingProxy.h ++++ b/src/jsc/bindings/webcore/WorkerMessagingProxy.h +@@ -84,6 +84,7 @@ public: + void postMessageToWorkerGlobalScope(MessageWithMessagePorts&&); + // Queued while Pending, posted while Running, refused (false) once Closing. + bool postTaskToWorkerGlobalScope(Function&&); ++ bool postVMInspection(Function&&); + void setKeepAlive(bool); + // Whether the thread keeps the parent's loop alive; nullopt once the thread is released + // (node: the handle is gone and hasRef() reads back undefined). +@@ -157,6 +158,7 @@ private: + // (and run by workerGlobalScopeStarted) or posted directly, never lost. + Lock m_pendingTasksLock; + Deque> m_pendingTasks WTF_GUARDED_BY_LOCK(m_pendingTasksLock); ++ bool m_workerVMReady WTF_GUARDED_BY_LOCK(m_pendingTasksLock) { false }; + HashMap> m_pendingCrossVMRequests WTF_GUARDED_BY_LOCK(m_pendingTasksLock); + std::atomic m_nextRequestId { 1 }; + +diff --git a/src/runtime/api/BunObject.rs b/src/runtime/api/BunObject.rs +index 29c34548..01d5a642 100644 +--- a/src/runtime/api/BunObject.rs ++++ b/src/runtime/api/BunObject.rs +@@ -1780,6 +1780,7 @@ fn mmap_file(global_this: &JSGlobalObject, callframe: &CallFrame) -> JsResult(), + Some(global_deallocator), ++ jsc::array_buffer::ArrayBufferAllocationMode::RuntimeOwned, + ) + } + } +@@ -2662,6 +2665,7 @@ pub(crate) mod JSZlib { + global_this, + ptr.cast::(), + Some(global_deallocator), ++ jsc::array_buffer::ArrayBufferAllocationMode::RuntimeOwned, + ) + } + } +diff --git a/src/runtime/ffi/FFIObject.rs b/src/runtime/ffi/FFIObject.rs +index 510d8ae8..b7705db3 100644 +--- a/src/runtime/ffi/FFIObject.rs ++++ b/src/runtime/ffi/FFIObject.rs +@@ -44,6 +44,7 @@ fn create_buffer_with_ctx( + len: usize, + ctx: *mut c_void, + deallocator: jsc::JSTypedArrayBytesDeallocator, ++ allocation: jsc::array_buffer::ArrayBufferAllocationMode, + ) -> JSValue; + } + // SAFETY: `global` is live; `slice` stays valid for the Buffer's lifetime. +@@ -55,6 +56,7 @@ fn create_buffer_with_ctx( + slice.len(), + ctx, + callback, ++ jsc::array_buffer::ArrayBufferAllocationMode::External, + ) + }) + } +@@ -652,6 +654,7 @@ fn to_array_buffer( + global_this, + ctx.unwrap_or(core::ptr::null_mut()), + callback, ++ jsc::array_buffer::ArrayBufferAllocationMode::External, + ) + } + } +diff --git a/src/runtime/image/Image.rs b/src/runtime/image/Image.rs +index b31492c3..1943ca15 100644 +--- a/src/runtime/image/Image.rs ++++ b/src/runtime/image/Image.rs +@@ -1799,7 +1799,12 @@ impl PipelineTask { + // it exactly once at GC and ignores the null ctx. + let v = unsafe { + ArrayBuffer::from_bytes(mut_slice, jsc::JSType::Uint8Array) +- .to_js_with_context(global, core::ptr::null_mut(), Some(out.free)) ++ .to_js_with_context( ++ global, ++ core::ptr::null_mut(), ++ Some(out.free), ++ jsc::array_buffer::ArrayBufferAllocationMode::RuntimeOwned, ++ ) + }; + match v { + Ok(v) => promise.resolve(global, v)?, +diff --git a/src/runtime/webcore/Blob.rs b/src/runtime/webcore/Blob.rs +index f85d1129..43c9a8be 100644 +--- a/src/runtime/webcore/Blob.rs ++++ b/src/runtime/webcore/Blob.rs +@@ -2828,6 +2828,7 @@ impl BlobExt for Blob { + global, + store.into_raw().cast::(), + Some(blob_store_array_buffer_deallocator), ++ jsc::array_buffer::ArrayBufferAllocationMode::External, + ) + } + } +@@ -2857,6 +2858,7 @@ impl BlobExt for Blob { + global, + store.into_raw().cast::(), + Some(blob_store_array_buffer_deallocator), ++ jsc::array_buffer::ArrayBufferAllocationMode::RuntimeOwned, + ) + } + } +diff --git a/test/js/node/process/arraybuffer-accounting.fixture.mjs b/test/js/node/process/arraybuffer-accounting.fixture.mjs +new file mode 100644 +index 0000000000000000000000000000000000000000..d2bce713c2bd0a7bc66262eb327e1a6cf52676b4 +--- /dev/null ++++ b/test/js/node/process/arraybuffer-accounting.fixture.mjs +@@ -0,0 +1,361 @@ ++import assert from "node:assert/strict"; ++import { once } from "node:events"; ++import { readFileSync } from "node:fs"; ++import { createRequire } from "node:module"; ++import { getHeapStatistics, serialize } from "node:v8"; ++import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads"; ++ ++const MiB = 1024 * 1024; ++const bytes = 16 * MiB; ++const collect = () => (globalThis.Bun ? Bun.gc(true) : globalThis.gc()); ++const tick = () => new Promise(resolve => setImmediate(resolve)); ++function sample() { ++ const m = process.memoryUsage(); ++ const v = getHeapStatistics(); ++ return { ++ arrayBuffers: m.arrayBuffers, ++ external: m.external, ++ external_memory: v.external_memory, ++ heapUsed: m.heapUsed, ++ heapTotal: m.heapTotal, ++ used_heap_size: v.used_heap_size, ++ total_heap_size: v.total_heap_size, ++ }; ++} ++function externalDelta(after, before, expected, label) { ++ for (const key of ["external", "external_memory"]) ++ assert.ok( ++ Math.abs(after[key] - before[key] - expected) < 65536, ++ `${label} ${key}: ${after[key] - before[key]} != ${expected}`, ++ ); ++} ++async function settle() { ++ await tick(); ++ collect(); ++ await tick(); ++ collect(); ++} ++async function lifecycle(kind) { ++ if (["file", "blob", "response", "serialize", "serialize-buffer"].includes(kind)) return nativeOutput(kind); ++ const isWasm = kind === "wasm" || kind === "wasm-shared"; ++ if (isWasm) { ++ globalThis.warmWasm = new WebAssembly.Memory({ initial: 1, maximum: 256, shared: kind === "wasm-shared" }); ++ warmWasm.buffer; ++ } ++ const native = ++ kind === "native" ? createRequire(import.meta.url)("../../../napi/napi-app/build/Debug/napitests.node") : undefined; ++ await settle(); ++ const rows = []; ++ if (kind === "fast") { ++ function alloc(n) { ++ return new Uint8Array(n); ++ } ++ globalThis.warm = Array.from({ length: 100000 }, () => alloc(128)); ++ await settle(); ++ rows.push(sample()); ++ globalThis.held = Array.from({ length: 8192 }, () => alloc(512)); ++ } else { ++ rows.push(sample()); ++ if (kind === "typed") globalThis.held = new Uint8Array(bytes); ++ if (kind === "buffer") globalThis.held = Buffer.alloc(bytes); ++ if (kind === "native") globalThis.held = native.create_external_arraybuffer_for_transfer(bytes); ++ if (["arraybuffer", "detach", "views", "resize-transfer"].includes(kind)) globalThis.held = new ArrayBuffer(bytes); ++ if (kind === "resizable") globalThis.held = new ArrayBuffer(4 * MiB, { maxByteLength: bytes }); ++ if (kind === "shared") globalThis.held = new SharedArrayBuffer(bytes); ++ if (kind === "growable") globalThis.held = new SharedArrayBuffer(4 * MiB, { maxByteLength: bytes }); ++ if (isWasm) globalThis.held = new WebAssembly.Memory({ initial: 64, maximum: 256, shared: kind === "wasm-shared" }); ++ } ++ const expectedBuffers = ++ kind === "resizable" || kind === "growable" || kind === "native" || isWasm ? 0 : kind === "fast" ? 4 * MiB : bytes; ++ const expectedExternal = ++ kind === "shared" || kind === "growable" || kind === "wasm-shared" ++ ? 0 ++ : kind === "resizable" || kind === "wasm" ++ ? 4 * MiB ++ : kind === "native" ++ ? bytes ++ : expectedBuffers; ++ rows.push(sample()); ++ assert.equal(rows[1].arrayBuffers - rows[0].arrayBuffers, expectedBuffers, `${kind} arrayBuffers before GC`); ++ externalDelta(rows[1], rows[0], expectedExternal, `${kind} allocation`); ++ assert.ok(rows[1].heapUsed - rows[0].heapUsed < 3 * MiB, `${kind} payload entered JS heap`); ++ assert.ok(rows[1].used_heap_size - rows[0].used_heap_size < 3 * MiB, `${kind} payload entered v8 JS heap`); ++ if (kind === "typed") { ++ globalThis.materialized = held.buffer; ++ assert.equal(sample().arrayBuffers, rows[1].arrayBuffers, "materialization counted twice"); ++ externalDelta(sample(), rows[1], 0, "materialization"); ++ } ++ if (kind === "resizable") { ++ held.resize(12 * MiB); ++ assert.equal(sample().arrayBuffers, rows[0].arrayBuffers); ++ externalDelta(sample(), rows[0], 12 * MiB, "grown"); ++ held.resize(2 * MiB); ++ externalDelta(sample(), rows[0], 2 * MiB, "shrunk"); ++ globalThis.received = held.transferToFixedLength(); ++ assert.equal(held.byteLength, 0); ++ assert.equal(sample().arrayBuffers - rows[0].arrayBuffers, 2 * MiB, "fixed transfer allocates"); ++ externalDelta(sample(), rows[0], 2 * MiB, "fixed transfer"); ++ } ++ if (kind === "growable") { ++ held.grow(12 * MiB); ++ assert.equal(sample().arrayBuffers, rows[0].arrayBuffers); ++ externalDelta(sample(), rows[0], 0, "shared growth"); ++ } ++ if (kind === "detach") { ++ globalThis.received = structuredClone(held, { transfer: [held] }); ++ assert.equal(held.byteLength, 0); ++ assert.equal(sample().arrayBuffers, rows[1].arrayBuffers); ++ externalDelta(sample(), rows[1], 0, "local transfer"); ++ } ++ if (kind === "views") { ++ globalThis.aliases = [ ++ new Uint8Array(held), ++ new Uint32Array(held, 4, 64), ++ new DataView(held, 1, 128), ++ Buffer.from(held), ++ ]; ++ aliases.push(aliases[0].subarray(3, 64)); ++ assert.equal(sample().arrayBuffers, rows[1].arrayBuffers, "views share one allocation"); ++ externalDelta(sample(), rows[1], 0, "views share one holder"); ++ } ++ if (kind === "resize-transfer") { ++ globalThis.received = held.transfer(bytes / 2); ++ assert.equal(held.byteLength, 0); ++ assert.equal(sample().arrayBuffers - rows[0].arrayBuffers, bytes / 2, "resized transfer replaces allocation"); ++ externalDelta(sample(), rows[0], bytes / 2, "resized transfer"); ++ globalThis.received = received.transfer(0); ++ assert.equal(sample().arrayBuffers, rows[0].arrayBuffers, "zero transfer frees allocation"); ++ externalDelta(sample(), rows[0], 0, "zero transfer"); ++ } ++ if (isWasm) { ++ globalThis.materialized = held.buffer; ++ externalDelta(sample(), rows[0], expectedExternal, "wasm buffer exposure"); ++ held.grow(128); ++ const grownExternal = kind === "wasm" ? 12 * MiB : 0; ++ externalDelta(sample(), rows[0], grownExternal, "wasm growth before buffer exposure"); ++ globalThis.materialized = held.buffer; ++ globalThis.held = null; ++ await settle(); ++ externalDelta(sample(), rows[0], grownExternal, "wasm backing storage outlives memory wrapper"); ++ assert.equal(sample().arrayBuffers, rows[0].arrayBuffers, "wasm bypasses ArrayBuffer allocator"); ++ } ++ collect(); ++ rows.push(sample()); ++ assert.ok(rows[2].heapUsed - rows[0].heapUsed < 3 * MiB, `${kind} collected payload entered JS heap`); ++ globalThis.held = globalThis.materialized = globalThis.received = globalThis.aliases = null; ++ await settle(); ++ rows.push(sample()); ++ assert.equal(rows[3].arrayBuffers, rows[0].arrayBuffers, `${kind} release`); ++ externalDelta(rows[3], rows[0], 0, `${kind} release`); ++ globalThis.warm = null; ++ globalThis.warmWasm = null; ++ return rows; ++} ++ ++async function nativeOutput(kind) { ++ const isSerializer = kind === "serialize" || kind === "serialize-buffer"; ++ if (kind === "blob") globalThis.nativeInput = new Blob([new Uint8Array(bytes)]); ++ if (kind === "response") globalThis.nativeInput = new Response("x".repeat(bytes)); ++ if (kind === "serialize") globalThis.nativeInput = new Uint8Array(bytes); ++ if (kind === "serialize-buffer") globalThis.nativeInput = Buffer.alloc(bytes); ++ await settle(); ++ const before = sample(); ++ if (kind === "file") globalThis.held = readFileSync(isMainThread ? process.argv[3] : workerData.path); ++ if (kind === "blob" || kind === "response") globalThis.held = await nativeInput.arrayBuffer(); ++ if (isSerializer) globalThis.held = serialize(nativeInput); ++ const length = held.byteLength; ++ await settle(); ++ const retained = sample(); ++ if (isSerializer) { ++ assert.ok(held.buffer instanceof ArrayBuffer, "serializer Buffer has unshared backing storage"); ++ assert.equal(retained.arrayBuffers, before.arrayBuffers, "serializer storage is external to the allocator"); ++ externalDelta(retained, before, length, "serializer backing store"); ++ } else { ++ assert.ok(retained.arrayBuffers >= length, `${kind} native output is counted`); ++ if (kind !== "response") ++ assert.equal(retained.arrayBuffers - before.arrayBuffers, length, `${kind} native allocation origin`); ++ } ++ globalThis.nativeInput = null; ++ await settle(); ++ const isolated = sample(); ++ globalThis.held = null; ++ await settle(); ++ const released = sample(); ++ assert.equal( ++ isolated.arrayBuffers - released.arrayBuffers, ++ isSerializer ? 0 : length, ++ `${kind} origin release`, ++ ); ++ externalDelta(released, isolated, -length, `${kind} external release`); ++ return [before, retained, isolated, released]; ++} ++ ++const kind = isMainThread ? process.argv[2] : typeof workerData === "string" ? workerData : workerData.kind; ++if (!isMainThread && ["busy", "wait", "shutdown", "startup"].includes(kind)) { ++ // Finish startup work before comparing snapshots of a fresh allocation. ++ if (kind === "busy" || kind === "wait") await settle(); ++ globalThis.held = new Uint8Array(bytes); ++ const flag = new Int32Array(workerData.shared); ++ parentPort.postMessage(sample()); ++ if (kind === "wait") { ++ assert.equal(Atomics.wait(flag, 0, 0), "ok"); ++ } else { ++ while (!Atomics.load(flag, 0)) {} ++ } ++ parentPort.postMessage("released"); ++} else if (!isMainThread && kind === "transfer") { ++ await settle(); ++ parentPort.postMessage(sample()); ++ parentPort.on("message", async message => { ++ if (message instanceof ArrayBuffer) { ++ globalThis.held = message; ++ parentPort.postMessage(sample()); ++ } else if (message === "release") { ++ globalThis.held = null; ++ await settle(); ++ parentPort.postMessage(sample()); ++ } ++ }); ++} else if (!isMainThread && kind === "owner-exit") { ++ parentPort.once("message", () => { ++ globalThis.held = new ArrayBuffer(bytes); ++ parentPort.postMessage(held, [held]); ++ }); ++ parentPort.postMessage("ready"); ++} else if (!isMainThread) { ++ parentPort.postMessage(await lifecycle(kind)); ++} else if (kind === "shutdown" || kind === "startup") { ++ const shared = new SharedArrayBuffer(4); ++ const worker = new Worker(new URL(import.meta.url), { workerData: { kind, shared } }); ++ let timer; ++ try { ++ if (kind === "shutdown") await once(worker, "message"); ++ const requests = Array.from({ length: 32 }, () => ++ worker.getHeapStatistics().then( ++ stats => ({ used: stats.used_heap_size }), ++ error => ({ code: error.code }), ++ ), ++ ); ++ const ending = kind === "shutdown" ? worker.terminate() : undefined; ++ const results = await Promise.race([ ++ Promise.all(requests), ++ new Promise((_, reject) => { ++ timer = setTimeout(() => reject(new Error(`${kind} requests did not settle`)), 1500); ++ }), ++ ]); ++ for (const result of results) { ++ if ("code" in result) assert.equal(result.code, "ERR_WORKER_NOT_RUNNING"); ++ else assert.ok(result.used >= 0); ++ } ++ await ending; ++ console.log(JSON.stringify({ kind, results })); ++ } finally { ++ clearTimeout(timer); ++ Atomics.store(new Int32Array(shared), 0, 1); ++ await worker.terminate(); ++ } ++} else if (kind === "busy" || kind === "wait") { ++ const shared = new SharedArrayBuffer(4); ++ const flag = new Int32Array(shared); ++ const worker = new Worker(new URL(import.meta.url), { workerData: { kind, shared } }); ++ let timer; ++ try { ++ const self = (await once(worker, "message"))[0]; ++ const start = performance.now(); ++ const stats = await Promise.race([ ++ Promise.all(Array.from({ length: 32 }, () => worker.getHeapStatistics())), ++ new Promise((_, reject) => { ++ timer = setTimeout(() => reject(new Error(`${kind} worker snapshot blocked`)), 1500); ++ }), ++ ]); ++ clearTimeout(timer); ++ for (const stat of stats) { ++ assert.ok(stat.external_memory >= bytes, "fresh external payload"); ++ assert.ok( ++ Math.abs(stat.external_memory - self.external_memory) < 65536, ++ "worker-local and parent snapshot agree", ++ ); ++ assert.ok(stat.total_heap_size >= stat.used_heap_size); ++ } ++ const released = once(worker, "message"); ++ if (kind === "wait") { ++ // Keep the expected value unchanged until a notification reaches a parked waiter. ++ while (Atomics.notify(flag, 0, 1) !== 1) { ++ assert.ok(performance.now() - start < 1500, "worker did not enter its atomic wait"); ++ await tick(); ++ } ++ } else Atomics.store(flag, 0, 1); ++ assert.equal((await released)[0], "released", "inspection preserves wait result"); ++ console.log(JSON.stringify({ kind, elapsedMs: performance.now() - start, self, stats })); ++ } finally { ++ clearTimeout(timer); ++ Atomics.store(flag, 0, 1); ++ Atomics.notify(flag, 0); ++ await worker.terminate(); ++ } ++} else if (kind === "transfer") { ++ const worker = new Worker(new URL(import.meta.url), { workerData: kind }); ++ try { ++ const workerBefore = (await once(worker, "message"))[0]; ++ await settle(); ++ const before = sample(); ++ globalThis.held = new ArrayBuffer(bytes); ++ const response = once(worker, "message"); ++ worker.postMessage(held, [held]); ++ assert.equal(held.byteLength, 0); ++ const sent = sample(); ++ assert.equal(sent.arrayBuffers - before.arrayBuffers, bytes, "origin charge follows allocator"); ++ externalDelta(sent, before, 0, "sender after transfer"); ++ const received = (await response)[0]; ++ assert.equal(received.arrayBuffers, workerBefore.arrayBuffers, "receiver did not allocate"); ++ externalDelta(received, workerBefore, bytes, "receiver"); ++ assert.ok(received.heapUsed - workerBefore.heapUsed < 3 * MiB, "transfer pressure is not JS heap"); ++ assert.ok(received.used_heap_size - workerBefore.used_heap_size < 3 * MiB, "transfer pressure is not V8 heap"); ++ const parentRead = await worker.getHeapStatistics(); ++ assert.ok(Math.abs(parentRead.external_memory - received.external_memory) < 65536); ++ globalThis.held = null; ++ await settle(); ++ assert.equal(sample().arrayBuffers - before.arrayBuffers, bytes, "sender GC preserves origin"); ++ const release = once(worker, "message"); ++ worker.postMessage("release"); ++ await release; ++ await settle(); ++ assert.equal(sample().arrayBuffers, before.arrayBuffers, "receiver frees origin charge"); ++ console.log(JSON.stringify({ kind, before, sent, received, parentRead, after: sample() })); ++ } finally { ++ await worker.terminate(); ++ } ++} else if (kind === "owner-exit") { ++ const worker = new Worker(new URL(import.meta.url), { workerData: kind }); ++ try { ++ assert.equal((await once(worker, "message"))[0], "ready"); ++ await settle(); ++ const before = sample(); ++ let received = once(worker, "message"); ++ worker.postMessage("allocate"); ++ globalThis.held = (await received)[0]; ++ // The fulfilled events.once promise otherwise retains the transferred buffer. ++ received = null; ++ await worker.terminate(); ++ assert.equal(held.byteLength, bytes); ++ assert.equal(sample().arrayBuffers, before.arrayBuffers, "allocator exit does not move charge"); ++ externalDelta(sample(), before, bytes, "allocator exited"); ++ await settle(); ++ const retained = sample(); ++ globalThis.held = null; ++ await settle(); ++ externalDelta(sample(), retained, -bytes, "allocator exited and storage freed"); ++ console.log(JSON.stringify({ kind, before, retained, after: sample() })); ++ } finally { ++ await worker.terminate(); ++ } ++} else { ++ const main = await lifecycle(kind); ++ const worker = new Worker(new URL(import.meta.url), { workerData: { kind, path: process.argv[3] } }); ++ try { ++ const workerRows = (await once(worker, "message"))[0]; ++ console.log(JSON.stringify({ kind, main, worker: workerRows })); ++ } finally { ++ await worker.terminate(); ++ } ++} +diff --git a/test/js/node/process/process.test.js b/test/js/node/process/process.test.js +index 52aa6c46..303a069d 100644 +--- a/test/js/node/process/process.test.js ++++ b/test/js/node/process/process.test.js +@@ -4,6 +4,7 @@ import { memoryUsage as jscMemoryUsage } from "bun:jsc"; + import { describe, expect, it } from "bun:test"; + import { familySync } from "detect-libc"; + import { bunEnv, bunExe, isASAN, isDebug, isLinux, isMacOS, isWindows, tempDir, tmpdirSync } from "harness"; ++import { fileURLToPath } from "node:url"; + import { basename, join, resolve } from "path"; + import { getHeapStatistics } from "v8"; + +@@ -3016,6 +3017,67 @@ it("process.memoryUsage.arrayBuffers", () => { + expect(process.memoryUsage().arrayBuffers).toBeGreaterThanOrEqual(initial + 16 * 1024 * 1024); + }); + ++for (const kind of [ ++ "typed", ++ "buffer", ++ "arraybuffer", ++ "fast", ++ "resizable", ++ "shared", ++ "growable", ++ "detach", ++ "transfer", ++ "owner-exit", ++ "wasm", ++ "wasm-shared", ++ "views", ++ "resize-transfer", ++ "blob", ++ "response", ++ "serialize", ++ "serialize-buffer", ++]) { ++ it(`ArrayBuffer memory ownership: ${kind}`, async () => { ++ await using child = Bun.spawn({ ++ cmd: [ ++ bunExe(), ++ "--expose-gc", ++ fileURLToPath(new URL("./arraybuffer-accounting.fixture.mjs", import.meta.url)), ++ kind, ++ ], ++ env: bunEnv, ++ stdout: "pipe", ++ stderr: "pipe", ++ }); ++ const [stdout, stderr, exitCode] = await Promise.all([child.stdout.text(), child.stderr.text(), child.exited]); ++ expect(stderr).toBe(""); ++ expect(stdout).toContain(`"kind":"${kind}"`); ++ expect(exitCode).toBe(0); ++ }); ++} ++ ++it("ArrayBuffer memory ownership: native file buffers", async () => { ++ using dir = tempDir("arraybuffer-file", { "data.bin": "" }); ++ const path = join(String(dir), "data.bin"); ++ await Bun.write(path, new Uint8Array(16 * 1024 * 1024)); ++ await using child = Bun.spawn({ ++ cmd: [ ++ bunExe(), ++ "--expose-gc", ++ fileURLToPath(new URL("./arraybuffer-accounting.fixture.mjs", import.meta.url)), ++ "file", ++ path, ++ ], ++ env: bunEnv, ++ stdout: "pipe", ++ stderr: "pipe", ++ }); ++ const [stdout, stderr, exitCode] = await Promise.all([child.stdout.text(), child.stderr.text(), child.exited]); ++ expect(stderr).toBe(""); ++ expect(stdout).toContain('"kind":"file"'); ++ expect(exitCode).toBe(0); ++}); ++ + it("should handle user assigned `default` properties", async () => { + process.default = 1; + process.hello = 2; +diff --git a/test/js/node/v8/v8-serdes-buffer.test.ts b/test/js/node/v8/v8-serdes-buffer.test.ts +index eaf4cbc6..6ab42221 100644 +--- a/test/js/node/v8/v8-serdes-buffer.test.ts ++++ b/test/js/node/v8/v8-serdes-buffer.test.ts +@@ -6,6 +6,14 @@ import v8 from "node:v8"; + // serializer delegate behavior (DefaultSerializer host objects). Non-Buffer + // payloads keep the bare JSC-serialized format for backward compatibility. + describe("v8 serialize/deserialize Buffer identity", () => { ++ test.each([new Uint8Array([1, 2]), Buffer.from([1, 2])])("serialized output is transferable: %p", input => { ++ const serialized = v8.serialize(input); ++ expect(serialized.buffer).toBeInstanceOf(ArrayBuffer); ++ const transferred = structuredClone(serialized.buffer, { transfer: [serialized.buffer] }); ++ expect(serialized.byteLength).toBe(0); ++ expect(Array.from(v8.deserialize(Buffer.from(transferred)))).toEqual([1, 2]); ++ }); ++ + test("Buffer round-trips as Buffer", () => { + const out = v8.deserialize(v8.serialize(Buffer.from("hi"))); + expect(Buffer.isBuffer(out)).toBe(true); +diff --git a/test/js/node/worker_threads/worker_threads.test.ts b/test/js/node/worker_threads/worker_threads.test.ts +index 108d0e0b..037bd23b 100644 +--- a/test/js/node/worker_threads/worker_threads.test.ts ++++ b/test/js/node/worker_threads/worker_threads.test.ts +@@ -4,7 +4,7 @@ import { once } from "node:events"; + import fs from "node:fs"; + import { join, relative, resolve } from "node:path"; + import { Readable } from "node:stream"; +-import { pathToFileURL } from "node:url"; ++import { fileURLToPath, pathToFileURL } from "node:url"; + import wt, { + BroadcastChannel, + getEnvironmentData, +@@ -2336,6 +2336,7 @@ test("getHeapStatistics reports allocated capacity separately from used heap", a + const stats = await worker.getHeapStatistics(); + expect(stats.used_heap_size).toBeGreaterThan(0); + expect(stats.total_heap_size).toBeGreaterThan(stats.used_heap_size); ++ expect(stats.external_memory).toBeGreaterThan(0); + } finally { + await worker.terminate(); + } +@@ -2355,6 +2356,26 @@ test("getHeapStatistics settles when terminated mid-request", async () => { + ).resolves.toMatch(/^(ok|ERR_WORKER_NOT_RUNNING)$/); + }); + ++for (const kind of ["busy", "wait", "shutdown", "startup"]) { ++ test(`getHeapStatistics settles requests during worker ${kind}`, async () => { ++ await using child = Bun.spawn({ ++ cmd: [ ++ bunExe(), ++ "--expose-gc", ++ fileURLToPath(new URL("../process/arraybuffer-accounting.fixture.mjs", import.meta.url)), ++ kind, ++ ], ++ env: bunEnv, ++ stdout: "pipe", ++ stderr: "pipe", ++ }); ++ const [stdout, stderr, exitCode] = await Promise.all([child.stdout.text(), child.stderr.text(), child.exited]); ++ expect(stderr).toBe(""); ++ expect(stdout).toContain(`"kind":"${kind}"`); ++ expect(exitCode).toBe(0); ++ }); ++} ++ + test("*Internal introspection methods are DontEnum on Worker.prototype", () => { + const enumerable: string[] = []; + for (const k in globalThis.Worker.prototype) enumerable.push(k); +diff --git a/test/napi/napi.test.ts b/test/napi/napi.test.ts +index 851a60d0..53eceffd 100644 +--- a/test/napi/napi.test.ts ++++ b/test/napi/napi.test.ts +@@ -438,6 +438,24 @@ describe.concurrent.skipIf(!canBuildNodeAddons())("napi", () => { + }); + + describe("napi_create_external_arraybuffer", () => { ++ it("charges external payload without charging the ArrayBuffer allocator", async () => { ++ await using child = Bun.spawn({ ++ cmd: [ ++ bunExe(), ++ "--expose-gc", ++ join(import.meta.dir, "../js/node/process/arraybuffer-accounting.fixture.mjs"), ++ "native", ++ ], ++ env: bunEnv, ++ stdout: "pipe", ++ stderr: "pipe", ++ }); ++ const [stdout, stderr, exitCode] = await Promise.all([child.stdout.text(), child.stderr.text(), child.exited]); ++ expect(stderr).toBe(""); ++ expect(stdout).toContain('"kind":"native"'); ++ expect(exitCode).toBe(0); ++ }); ++ + it("wraps caller data and does not fire finalize_cb while the ArrayBuffer is alive", async () => { + const result = await checkSameOutput("test_external_arraybuffer_finalizer", []); + expect(result).toContain("PASS: napi_create_external_arraybuffer wraps caller data without copying"); diff --git a/.github/openclaw/qualification/stack-visitor.js b/.github/openclaw/qualification/stack-visitor.js new file mode 100644 index 0000000000000..bf5358a299ad7 --- /dev/null +++ b/.github/openclaw/qualification/stack-visitor.js @@ -0,0 +1,3 @@ +function witness() { jscStack(); } +function parent() { witness(); } +parent(); diff --git a/.github/openclaw/qualification/stack/README.md b/.github/openclaw/qualification/stack/README.md new file mode 100644 index 0000000000000..4c28b6b1a5713 --- /dev/null +++ b/.github/openclaw/qualification/stack/README.md @@ -0,0 +1,20 @@ +# Stack cache qualification + +The fixture and JSC mode/cache driver come from the landed stack-position work. +The runtime cache probe uses the prepared Bun source plus its matching adapter, +whose `RuntimeTranspilerCache::EXPECTED_VERSION` is 38. + +The pinned Bun `Metadata::encode` writes a u32 version, two u8 tags, then twelve +little-endian u64 values. Output offset/length/hash begin at bytes 30/38/46. +`RuntimeTranspilerCache::hash` uses Wyhash with seed 42, also available through +`Bun.hash.wyhash`. The proof checks the version and bounds before using this +layout and fails if the source format changes. + +Baseline cold/warm runs seed the prior format. The candidate must replace it +with version 38. Before its final warm run, the probe changes an equal-width +stderr marker only in the cached output and recomputes that output's checksum. +The original source, source map and all coordinate assertions stay unchanged. +The warm run must print the cache-only marker, preserve the marked entry, and +pass every coordinate assertion. Recompilation from the original source prints +the original marker and fails the gate. This proves cache execution rather than +inferring a cache hit from files merely existing. diff --git a/.github/openclaw/qualification/stack/check-jsc.sh b/.github/openclaw/qualification/stack/check-jsc.sh new file mode 100644 index 0000000000000..5a471af0bf5c7 --- /dev/null +++ b/.github/openclaw/qualification/stack/check-jsc.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +set -euo pipefail +: "${WEBKIT_CHECKOUT:?}" "${JSC:?}" "${RESULT_DIR:?}" +mkdir -p "$RESULT_DIR" +cd "$WEBKIT_CHECKOUT" +for mode in default interpreter optimizer eager-ftl; do + args=() + case "$mode" in + interpreter) args=(--useJIT=false);; + optimizer) args=(--useBytecodeOptimizer=true);; + eager-ftl) args=(--useConcurrentJIT=false --thresholdForOptimizeAfterWarmUp=10 --thresholdForFTLOptimizeAfterWarmUp=20);; + esac + for fixture in callsite-syntax-positions.js stack-position-regressions.js callsite-async-position.mjs; do + module=(); case "$fixture" in *.mjs) module=(-m);; esac + "$JSC" "${args[@]}" "${module[@]}" "JSTests/stress/$fixture" > "$RESULT_DIR/$mode-$fixture.log" 2>&1 + done + printf '%s pass\n' "$mode" | tee -a "$RESULT_DIR/modes.txt" +done +for kind in owned persistent; do + cache="$RESULT_DIR/cache-$kind";mkdir -p "$cache" + args=(--diskCachePath="$cache") + if test "$kind" = persistent; then args+=(--diskCachePayloadIsPersistentForTesting=true); fi + for repeat in cold warm; do + for fixture in callsite-syntax-positions.js stack-position-regressions.js callsite-async-position.mjs; do + module=(); case "$fixture" in *.mjs) module=(-m);; esac + "$JSC" "${args[@]}" "${module[@]}" "JSTests/stress/$fixture" > "$RESULT_DIR/cache-$kind-$repeat-$fixture.log" 2>&1 + done + done +done +python3 - "$RESULT_DIR" <<'COUNT' +from pathlib import Path +import json,sys +p=Path(sys.argv[1]);counts={} +for kind in ['owned','persistent']: + files=[f for f in (p/('cache-'+kind)).rglob('*') if f.is_file()] + counts[kind]={'files':len(files),'bytes':sum(f.stat().st_size for f in files)} + assert len(files)>=3,counts +(p/'cache-files.json').write_text(json.dumps(counts,indent=2)+'\n') +COUNT diff --git a/.github/openclaw/qualification/stack/check-runtime-cache.py b/.github/openclaw/qualification/stack/check-runtime-cache.py new file mode 100644 index 0000000000000..b5ff564dfab73 --- /dev/null +++ b/.github/openclaw/qualification/stack/check-runtime-cache.py @@ -0,0 +1,85 @@ +"""Prove cache version replacement and execution of cached transpiler output.""" +from pathlib import Path +import argparse +import hashlib +import json +import os +import struct +import subprocess + +p = argparse.ArgumentParser() +p.add_argument('--baseline', required=True) +p.add_argument('--candidate', required=True) +p.add_argument('--fixture', required=True, type=Path) +p.add_argument('--output', required=True, type=Path) +p.add_argument('--expected-version', required=True, type=int) +a = p.parse_args() +a.output.mkdir(parents=True, exist_ok=False) +source_marker = 'W152_CACHE_SOURCE' +replay_marker = 'W152_CACHE_REPLAY' +source = a.output / 'positions.js' +source.write_text(a.fixture.read_text() + '\nconsole.error("' + source_marker + '");\n/*' + (' cache proof ' * 6000) + '*/\n') +cache = a.output / 'cache' +cache.mkdir() +env = {**os.environ, 'BUN_RUNTIME_TRANSPILER_CACHE_PATH': str(cache)} +receipts = [] + + +def snapshot(): + files = sorted(f for f in cache.rglob('*') if f.is_file()) + assert files, 'cache was not populated' + return [{'name': str(f.relative_to(cache)), 'bytes': f.stat().st_size, + 'sha256': hashlib.sha256(f.read_bytes()).hexdigest(), + 'version': int.from_bytes(f.read_bytes()[:4], 'little')} for f in files] + + +def mark_cached_output(): + changed = [] + for file in sorted(cache.rglob('*.pile')): + data = bytearray(file.read_bytes()) + assert len(data) >= 102, 'truncated cache header' + assert int.from_bytes(data[:4], 'little') == a.expected_version, 'candidate did not replace old cache format' + # Metadata::encode: u32 version, two u8 tags, then twelve little-endian u64s. + offset, length = struct.unpack_from(' new Error("arrow"), 28, 25], + ['arrow-type', () => new TypeError("arrow"), 29, 26], + ['arrow-alias', () => new globalThis.Error("arrow"), 30, 27], + ['multiline-new', function () { +const error = new + Error("multiline"); return error; + }, 32, 15], + ['constructor-comment', function () { +const error = new /* comment */ Error("comment"); return error; + }, 36, 15], + ['null-dot', function () { +try { null.x; } catch (error) { return error; } + }, 39, 12], + ['undefined-dot', function () { +try { undefined.x; } catch (error) { return error; } + }, 42, 17], + ['null-length', function () { +try { null.length; } catch (error) { return error; } + }, 45, 12], + ['null-comment', function () { +try { null . /* comment */ x; } catch (error) { return error; } + }, 48, 28], + ['null-escape', function () { +try { null.\u0078; } catch (error) { return error; } + }, 51, 12], + ['null-multiline', function () { +try { null + .x; } catch (error) { return error; } + }, 55, 6], + ['null-bracket', function () { +try { null["x"]; } catch (error) { return error; } + }, 58, 11], + ['null-number', function () { +try { null[1]; } catch (error) { return error; } + }, 61, 11], + ['null-computed', function () { +try { null[name]; } catch (error) { return error; } + }, 64, 11], + ['null-call', function () { +try { null.x(); } catch (error) { return error; } + }, 67, 12], + ['null-parenthesized-call', function () { +try { (null.x)(); } catch (error) { return error; } + }, 70, 13], + ['null-bracket-call', function () { +try { null["x"](); } catch (error) { return error; } + }, 73, 11], + ['null-call-method', function () { +try { null.call(); } catch (error) { return error; } + }, 76, 12], + ['null-apply-method', function () { +try { null.apply(null, []); } catch (error) { return error; } + }, 79, 12], + ['null-has-own', function () { +try { null.hasOwnProperty(name); } catch (error) { return error; } + }, 82, 12], + ]; +function checkPosition(error, line, column, shape, structured) { + const frames = structured ? error.stack : error.stack.split("\n").filter(frame => /:(\d+):(\d+)\)?$/.test(frame)); + const actual = structured ? [frames[0].getLineNumber(), frames[0].getColumnNumber()] : /:(\d+):(\d+)\)?$/.exec(frames[0])?.slice(1).map(Number); + observations.push({shape,structured,frame:0,expected:[line,column],actual,trace:structured ? frames.map(String) : error.stack}); +} +for (let iteration = 0; iteration < 1; ++iteration) { + for (const [shape, create, line, column] of cases) { + checkPosition(create(), line, column, shape, false); + if (typeof process !== "undefined") { + const previous = Error.prepareStackTrace; + try { + Error.prepareStackTrace = (_, frames) => frames; + checkPosition(create(), line, column, shape, true); + } finally { + Error.prepareStackTrace = previous; + } + } + } +} + +if (typeof print === "function") print(JSON.stringify(observations)); else console.log(JSON.stringify(observations)); diff --git a/.github/openclaw/qualification/sync/inspect-stack-position.patch b/.github/openclaw/qualification/sync/inspect-stack-position.patch new file mode 100644 index 0000000000000..2f56cc8b8d4bd --- /dev/null +++ b/.github/openclaw/qualification/sync/inspect-stack-position.patch @@ -0,0 +1,11 @@ +--- a/test/js/bun/util/inspect.test.js ++++ b/test/js/bun/util/inspect.test.js +@@ -892,7 +892,7 @@ + NNN | lineno: 42, + NNN | colno: 10, + NNN | error: new Error("Test error"), +- ^ ++ ^ + error: Test error + at (file:NN:NN) + , diff --git a/.github/openclaw/qualification/verify-sync.py b/.github/openclaw/qualification/verify-sync.py index 30829451c9eab..ba51525209b73 100644 --- a/.github/openclaw/qualification/verify-sync.py +++ b/.github/openclaw/qualification/verify-sync.py @@ -10,7 +10,7 @@ assert rows and all(r['ok'] for r in rows),'sync CI failures' assert {'test/js/web/atomics.test.ts','test/js/bun/wasm/compile-rss.test.ts'}<=set(selected) counts={} -for name,minimum in [('memory-release',3),('proxy',4),('resource-limits',14),('namespace',41),('inspector',78),('als-suites',223)]: +for name,minimum in [('memory-release',3),('proxy',4),('resource-limits',14),('namespace',41),('inspector',78),('als-suites',223),('stack-positions',343),('stack-minifier',68),('arraybuffer',19),('worker-statistics',7),('arraybuffer-native',1),('arraybuffer-serializer',60),('inspection-idle',1)]: text=(out/(name+'.log')).read_text() passes=re.findall(r'(\d+) pass',text) assert passes and int(passes[-1])>=minimum,name+' incomplete' @@ -21,12 +21,22 @@ matrix=json.loads((out/(name+'.json')).read_text())['results'] assert len(matrix)==14 and all(r['exitCode']==0 for r in matrix),name assert json.loads((out/'als-retention.json').read_text())['passed'] is True +cache=json.loads((out/'stack-cache.json').read_text()) +assert len(cache)==4 and all(r['matches']==r['total']==56 for r in cache if r['arm']=='candidate') +assert cache[-1]['arm']=='candidate' and cache[-1]['repeat']=='warm' and cache[-1]['cache_hit_proven'] is True assert len((out/'engine-limits.log').read_text().splitlines())==7 engine=json.loads((root/'engine-gate.json').read_text()) assert engine['passed'] and engine['source']==sys.argv[2] +compatibility=json.loads((root/'compatibility-gate.json').read_text()) +assert compatibility['passed'] and compatibility['source']==sys.argv[2] +assert compatibility['base_commit']==sys.argv[3] and compatibility['prepared_tree']==sys.argv[4] +config=json.loads((Path(__file__).resolve().parents[1]/'variants.json').read_text())['upstream_sync'] +assert compatibility['paired_tree']==config['paired_tree'] +assert compatibility['required_namespace_adapter_sha256']==config['required_namespace_adapter_sha256'] +assert compatibility['only_manifest_changed_after_pairing'] and compatibility['memory_tests']==3 and compatibility['startup'] is True record={'passed':True,'base_commit':sys.argv[3],'prepared_tree':sys.argv[4], 'selected_files':len(selected),'result_rows':len(rows),'regressions':0,'tests':counts, 'adapter_sha256':hashlib.sha256((root/'bun-sync-adapters.patch').read_bytes()).hexdigest()} (root/'sync-gate.json').write_text(json.dumps(record,indent=2)+'\n') -gate=json.loads((root/'gate.json').read_text());gate['upstream_sync']=record;gate['engine']=engine +gate=json.loads((root/'gate.json').read_text());gate['upstream_sync']=record;gate['engine']=engine;gate['compatibility']=compatibility (root/'gate.json').write_text(json.dumps(gate,indent=2)+'\n') diff --git a/.github/openclaw/qualify-engine.sh b/.github/openclaw/qualify-engine.sh index a45ae797c1fb3..8f70de85ef506 100644 --- a/.github/openclaw/qualify-engine.sh +++ b/.github/openclaw/qualify-engine.sh @@ -8,9 +8,11 @@ tar -xzf "$ARTIFACT_DIR/bun-webkit-linux-amd64.tar.gz" -C "$QUALIFICATION_DIR/en jsc="$QUALIFICATION_DIR/engine/bun-webkit/bin/jsc" "$jsc" --useDollarVM=true -e 'if(typeof $vm.moduleNamespaceForRequire!=="function")throw Error("namespace hook absent")' Tools/Scripts/run-jsc-stress-tests JSTests/stress --jsc "$jsc" --child-processes 16 \ - --filter 'promise|microtask|dynamic-import|bun-async-context|module-loader|top-level-await|async-module|module-namespace|intl-segmenter' \ + --filter 'promise|microtask|dynamic-import|bun-async-context|module-loader|top-level-await|async-module|module-namespace|intl-segmenter|callsite-syntax-positions|stack-position-regressions' \ --output-dir "$QUALIFICATION_DIR/jsc-stress" --no-retry > "$QUALIFICATION_DIR/jsc-stress.log" 2>&1 -python3 .github/openclaw/verify-jsc-results.py "$QUALIFICATION_DIR/jsc-stress" +python3 .github/openclaw/verify-jsc-results.py "$QUALIFICATION_DIR/jsc-stress" --require module-namespace-esmodule-marker.js --require callsite-syntax-positions.js --require stack-position-regressions.js +python3 .github/openclaw/check-stack-visitor.py "$jsc" > "$QUALIFICATION_DIR/stack-visitor.log" +WEBKIT_CHECKOUT="$PWD" JSC="$jsc" RESULT_DIR="$QUALIFICATION_DIR/jsc-stack" bash .github/openclaw/qualification/stack/check-jsc.sh Tools/Scripts/run-jsc-stress-tests JSTests/modules.yaml --jsc "$jsc" --child-processes 16 \ --output-dir "$QUALIFICATION_DIR/jsc-modules" --no-retry > "$QUALIFICATION_DIR/jsc-modules.log" 2>&1 "$jsc" --useDollarVM=true --useImportDefer=true .github/openclaw/qualification/als/jsc/module-context.js > "$QUALIFICATION_DIR/jsc-context.log" 2>&1 @@ -20,6 +22,13 @@ common=(--useDollarVM=true --useGenerationalGC=true --forceMiniVMMode=false) "$jsc" "${common[@]}" --useJIT=false "$fixture" > "$QUALIFICATION_DIR/sampling-interpreter.log" 2>&1 "$jsc" "${common[@]}" --useDFGJIT=false --useFTLJIT=false "$fixture" > "$QUALIFICATION_DIR/sampling-baseline.log" 2>&1 "$jsc" "${common[@]}" --useConcurrentJIT=false --thresholdForOptimizeAfterWarmUp=10 --thresholdForFTLOptimizeAfterWarmUp=20 "$fixture" > "$QUALIFICATION_DIR/sampling-ftl.log" 2>&1 +fixture=JSTests/stress/array-buffer-accounting.js +common=(--useGenerationalGC=true --forceMiniVMMode=false) +"$jsc" "${common[@]}" --useJIT=false "$fixture" > "$QUALIFICATION_DIR/accounting-interpreter.log" 2>&1 +"$jsc" "${common[@]}" --useDFGJIT=false --useFTLJIT=false "$fixture" > "$QUALIFICATION_DIR/accounting-baseline.log" 2>&1 +"$jsc" "${common[@]}" --useFTLJIT=false --useConcurrentJIT=false "$fixture" > "$QUALIFICATION_DIR/accounting-dfg.log" 2>&1 +"$jsc" "${common[@]}" --useConcurrentJIT=false --thresholdForJITAfterWarmUp=10 --thresholdForOptimizeAfterWarmUp=20 --thresholdForFTLOptimizeAfterWarmUp=20 "$fixture" > "$QUALIFICATION_DIR/accounting-ftl.log" 2>&1 +"$jsc" "${common[@]}" --gcAtEnd=true --useConcurrentGC=true "$fixture" > "$QUALIFICATION_DIR/accounting-concurrent.log" 2>&1 python3 - "$QUALIFICATION_DIR" "$source_sha" <<'PY' import json,sys from pathlib import Path @@ -29,5 +38,5 @@ for name,minimum in [('jsc-stress',1400),('jsc-modules',1600)]: assert len(lines)>=minimum,(name,len(lines)) assert all(line.endswith(': PASS') for line in lines),name+' failures' counts[name]=len(lines) -(root/'engine-gate.json').write_text(json.dumps({'passed':True,'source':sys.argv[2],'stress_counts':counts,'sampling_tiers':4},indent=2)+'\n') +(root/'engine-gate.json').write_text(json.dumps({'passed':True,'source':sys.argv[2],'stress_counts':counts,'sampling_tiers':4,'arraybuffer_execution_modes':5,'stack_execution_modes':4,'stack_cache':json.loads((root/'jsc-stack/cache-files.json').read_text())},indent=2)+'\n') PY diff --git a/.github/openclaw/qualify-sync-compatibility.sh b/.github/openclaw/qualify-sync-compatibility.sh new file mode 100644 index 0000000000000..01c3738bb26fa --- /dev/null +++ b/.github/openclaw/qualify-sync-compatibility.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +set -euo pipefail +pipeline=$(pwd) +inputs="$pipeline/.github/openclaw/qualification" +: "${ARTIFACT_DIR:?}" "${QUALIFICATION_DIR:?}" +QUALIFICATION_DIR=$(realpath "$QUALIFICATION_DIR") +SOURCE_SHA=$(git rev-parse HEAD) +BUN_BASE=d2d2a26ef973cdd97b37953f5dba58052acad74f +SYNC_TREE=d9ec052fccc81afbf43a5344e52bc980f439d587 +PAIRED_TREE=$(python3 -c 'import json;print(json.load(open(".github/openclaw/variants.json"))["upstream_sync"]["paired_tree"])') +NAMESPACE_SHA=$(python3 -c 'import json;print(json.load(open(".github/openclaw/variants.json"))["upstream_sync"]["required_namespace_adapter_sha256"])') +bootstrap=$(command -v bun) +export CI=1 PUPPETEER_SKIP_DOWNLOAD=1 +repo="$QUALIFICATION_DIR/bun-sync-compatibility" +results="$QUALIFICATION_DIR/compatibility" +mkdir -p "$results/home" "$results/tmp" +chmod 700 "$results/home" "$results/tmp" +git clone --depth=1 --filter=blob:none --no-checkout https://github.com/openclaw/bun.git "$repo" +cd "$repo" +git fetch --depth=1 origin "$BUN_BASE" +git checkout --detach "$BUN_BASE" +git apply --index "$inputs/sync/upstream-sync.patch" +test "$(git write-tree)" = "$SYNC_TREE" +namespace_patch="$inputs/patches/008-namespace.patch" +test "$(sha256sum "$namespace_patch" | cut -d' ' -f1)" = "$NAMESPACE_SHA" +git apply --index --exclude=CHANGELOG.md --exclude='docs/*' "$namespace_patch" +test "$(git write-tree)" = "$PAIRED_TREE" +git diff --binary "$SYNC_TREE" > "$QUALIFICATION_DIR/bun-required-namespace-pair.patch" +python3 "$inputs/stage-sync-artifact.py" "$ARTIFACT_DIR/bun-webkit-linux-amd64.tar.gz" --version "$SOURCE_SHA" --output "$QUALIFICATION_DIR/compatibility-input" +cp "$QUALIFICATION_DIR/compatibility-input/artifacts.json" scripts/build/deps/webkit-artifacts.json +# The namespace API requires its existing Bun counterpart; no other feature adapters enter this arm. +test "$(git diff --name-only)" = scripts/build/deps/webkit-artifacts.json +"$bootstrap" install --frozen-lockfile +(cd test && "$bootstrap" install --frozen-lockfile) +export BUN_BUILD_PREFETCH_DIR="$QUALIFICATION_DIR/compatibility-input/prefetch" +export BUN_BUILD_CACHE_DIR="$QUALIFICATION_DIR/cache-sync-compatibility" +env -u GITHUB_SHA -u BUILDKITE_COMMIT -u GIT_SHA "$bootstrap" run build:release --lto=off --buildDir="$repo/build/qualify-sync-compatibility" --timings > "$QUALIFICATION_DIR/build-sync-compatibility.log" 2>&1 +test "$(git write-tree)" = "$PAIRED_TREE" +test "$(git diff --name-only)" = scripts/build/deps/webkit-artifacts.json +git diff --binary "$PAIRED_TREE" > "$QUALIFICATION_DIR/bun-sync-compatibility.patch" +candidate="$repo/build/qualify-sync-compatibility/bun" +"$candidate" -e 'if(process.versions.webkit!==process.argv[1] || Bun.revision!==process.argv[2]) throw new Error("wrong engine or Bun source revision");' "$SOURCE_SHA" "$BUN_BASE" +sha256sum "$candidate" > "$QUALIFICATION_DIR/bun-sync-compatibility.sha256" +unset BUN_BUILD_PREFETCH_DIR BUN_BUILD_CACHE_DIR BUN_WEBKIT_ARTIFACT_MANIFEST +export HOME="$results/home" TMPDIR="$results/tmp" +"$candidate" --revision > "$results/revision.log" +"$candidate" -e 'const server=Bun.serve({port:0,fetch(){return new Response("ready")}});try{if(await(await fetch(server.url)).text()!=="ready")throw Error("startup");console.log("build/startup/http passed")}finally{await server.stop(true)}' > "$results/startup.log" 2>&1 +"$candidate" test test/js/web/atomics.test.ts test/js/bun/wasm/compile-rss.test.ts -t 'Atomics.wait lets mimalloc|a notify that arrives while Atomics.wait|WebAssembly.compile does not retain' > "$results/memory-release.log" 2>&1 +python3 - "$QUALIFICATION_DIR" "$SOURCE_SHA" "$BUN_BASE" "$SYNC_TREE" "$PAIRED_TREE" "$NAMESPACE_SHA" <<'PY' +import hashlib,json,re,sys +from pathlib import Path +root=Path(sys.argv[1]);text=(root/'compatibility/memory-release.log').read_text() +assert re.search(r'\b3 pass\b',text) and re.search(r'\b0 fail\b',text) +assert not re.search(r'\b[1-9]\d* skip\b',text) +record={'passed':True,'source':sys.argv[2],'base_commit':sys.argv[3],'prepared_tree':sys.argv[4], + 'paired_tree':sys.argv[5],'required_namespace_adapter_sha256':sys.argv[6], + 'only_manifest_changed_after_pairing':True,'memory_tests':3,'startup':True, + 'manifest_patch_sha256':hashlib.sha256((root/'bun-sync-compatibility.patch').read_bytes()).hexdigest()} +(root/'compatibility-gate.json').write_text(json.dumps(record,indent=2)+'\n') +PY diff --git a/.github/openclaw/qualify-sync.sh b/.github/openclaw/qualify-sync.sh index 1e232ad308b41..8ed0695cbebe5 100644 --- a/.github/openclaw/qualify-sync.sh +++ b/.github/openclaw/qualify-sync.sh @@ -23,9 +23,10 @@ test "$(git write-tree)" = "$SYNC_TREE" for patch in "$inputs"/patches/00[3-7]-*.patch; do git apply --index --exclude=CHANGELOG.md --exclude='docs/*' --exclude='test/js/node/worker_threads/*' "$patch" done -for patch in 008-namespace.patch 009-allocation-sampling.patch 010-als-regressions.patch; do +for patch in 008-namespace.patch 009-allocation-sampling.patch 010-als-regressions.patch 009-stack-positions.patch 010-arraybuffer-accounting.patch; do git apply --index --exclude=CHANGELOG.md --exclude='docs/*' "$inputs/patches/$patch" done +git apply --index "$inputs/sync/inspect-stack-position.patch" cp "$inputs/worker-resource-limits.test.ts" test/js/node/worker_threads/openclaw-resource-limits.test.ts git add test/js/node/worker_threads/openclaw-resource-limits.test.ts python3 "$inputs/stage-sync-artifact.py" "$ARTIFACT_DIR/bun-webkit-linux-amd64.tar.gz" --version "$SOURCE_SHA" --output "$QUALIFICATION_DIR/sync-input" @@ -49,14 +50,29 @@ export HOME="$results/home" TMPDIR="$results/tmp" PATH="$results/bin:$PATH" OPEN # Recompute the engine/nightly selection and retain every W150 upstream-sync case. "$candidate" --eval 'import {selectTests} from "./scripts/openclaw-ci/tests.ts"; import{execFileSync}from"node:child_process";import{readFileSync,writeFileSync,mkdirSync}from"node:fs";const tracked=execFileSync("git",["ls-files","-z","test"],{encoding:"utf8"}).split("\0").filter(Boolean);const original=JSON.parse(readFileSync(process.argv[1],"utf8"));const selected=[...new Set([...original,...selectTests(["scripts/build/deps/webkit.ts"],tracked,true,"linux")])].sort();for(const file of selected)if(!tracked.includes(file))throw Error("missing selected test "+file);mkdirSync("build/openclaw-ci",{recursive:true});writeFileSync("build/openclaw-ci/selected.json",JSON.stringify(selected,null,2)+"\n");' "$inputs/sync/selected.json" cp build/openclaw-ci/selected.json "$results/selected.json" -"$candidate" scripts/openclaw-ci/tests.ts test > "$results/tests.log" 2>&1 -cp build/openclaw-ci/results.json "$results/results.json" +if "$candidate" scripts/openclaw-ci/tests.ts test > "$results/tests.log" 2>&1; then + selection_exit=0 +else + selection_exit=$? +fi +if test -f build/openclaw-ci/results.json; then + cp build/openclaw-ci/results.json "$results/results.json" +fi +if test "$selection_exit" -ne 0; then exit "$selection_exit"; fi # Run these unchanged upstream tests explicitly as well, so skips cannot satisfy the gate. "$candidate" test test/js/web/atomics.test.ts test/js/bun/wasm/compile-rss.test.ts -t 'Atomics.wait lets mimalloc|a notify that arrives while Atomics.wait|WebAssembly.compile does not retain' > "$results/memory-release.log" 2>&1 +"$candidate" test --expose-internals test/js/node/v8/capture-stack-trace.test.js test/js/node/util/util.test.js test/js/bun/sourcemap > "$results/stack-positions.log" 2>&1 +"$candidate" test test/bundler/bundler_minify.test.ts > "$results/stack-minifier.log" 2>&1 +python3 "$inputs/stack/check-runtime-cache.py" --baseline "$QUALIFICATION_DIR/bun/build/qualify-baseline/bun" --candidate "$candidate" --fixture "$inputs/stack/observe-regressions.js" --output "$results/runtime-cache" --expected-version 38 > "$results/stack-cache.json" "$candidate" "$inputs/segmenter.js" > "$results/segmenter.log" 2>&1 "$candidate" test test/js/node/vm/vm.test.ts -t 'a Proxy in the prototype chain' > "$results/proxy.log" 2>&1 "$candidate" test test/js/node/worker_threads/openclaw-resource-limits.test.ts -t resourceLimits > "$results/resource-limits.log" 2>&1 "$candidate" test test/js/bun/resolve/esModule.test.ts test/js/bun/namespace-prototype-pollution.test.ts test/js/bun/resolve/require-esm-evaluating-cycle.test.ts test/js/bun/import-attributes/import-attributes.test.ts test/js/node/module/require-extensions.test.ts > "$results/namespace.log" 2>&1 +"$candidate" test test/js/node/process/process.test.js -t 'ArrayBuffer memory ownership' > "$results/arraybuffer.log" 2>&1 +"$candidate" test test/js/node/worker_threads/worker_threads.test.ts -t 'getHeapStatistics' > "$results/worker-statistics.log" 2>&1 +"$candidate" test test/napi/napi.test.ts -t 'charges external payload without charging the ArrayBuffer allocator' > "$results/arraybuffer-native.log" 2>&1 +"$candidate" test --expose-internals test/js/bun/jsc/bun-jsc.test.ts test/js/node/v8/v8-serdes-buffer.test.ts > "$results/arraybuffer-serializer.log" 2>&1 +"$candidate" test "$inputs/inspection-idle.test.ts" > "$results/inspection-idle.log" 2>&1 "$candidate" test test/js/node/inspector/inspector-profiler.test.ts test/js/node/inspector/inspector.test.ts > "$results/inspector.log" 2>&1 "$candidate" test test/js/node/async_hooks test/js/bun/resolve/import-defer.test.ts test/js/bun/resolve/dynamic-import-tla-cycle.test.ts test/js/bun/resolve/require-esm-gc-roots.test.ts test/js/bun/resolve/require-esm-microtask-order.test.ts test/js/bun/resolve/require-esm-evaluating-cycle.test.ts test/js/bun/resolve/require-esm-transitive-tla.test.ts > "$results/als-suites.log" 2>&1 "$candidate" "$inputs/module-context.mjs" matrix > "$results/als-plugin.json" diff --git a/.github/openclaw/qualify.sh b/.github/openclaw/qualify.sh index a99f800511136..6d680d26b2354 100644 --- a/.github/openclaw/qualify.sh +++ b/.github/openclaw/qualify.sh @@ -28,6 +28,8 @@ for arm in baseline candidate; do git apply --exclude=CHANGELOG.md --exclude='docs/*' --exclude='test/js/node/worker_threads/*' "$patch" done git apply --exclude=CHANGELOG.md --exclude='docs/*' "$inputs/patches/008-namespace.patch" + git apply --exclude=CHANGELOG.md --exclude='docs/*' "$inputs/patches/009-stack-positions.patch" + git apply --exclude=CHANGELOG.md --exclude='docs/*' "$inputs/patches/010-arraybuffer-accounting.patch" cp "$inputs/worker-resource-limits.test.ts" test/js/node/worker_threads/openclaw-resource-limits.test.ts git diff --check git diff --binary HEAD > "$QUALIFICATION_DIR/bun-adapters.patch" @@ -61,8 +63,11 @@ python3 "$inputs/compare-fork-ci.py" "$QUALIFICATION_DIR/baseline" "$QUALIFICATI candidate="$QUALIFICATION_DIR/bun/build/qualify-candidate/bun" unset BUN_WEBKIT_ARTIFACT_MANIFEST BUN_BUILD_CACHE_DIR BUN_BUILD_PREFETCH_DIR "$candidate" test test/js/bun/resolve/esModule.test.ts test/js/bun/namespace-prototype-pollution.test.ts test/js/bun/resolve/require-esm-evaluating-cycle.test.ts test/js/bun/import-attributes/import-attributes.test.ts test/js/node/module/require-extensions.test.ts > "$QUALIFICATION_DIR/namespace.log" 2>&1 +"$candidate" test --expose-internals test/js/node/v8/capture-stack-trace.test.js test/js/bun/sourcemap/internal-sourcemap.test.ts > "$QUALIFICATION_DIR/stack-positions.log" 2>&1 "$candidate" "$inputs/segmenter.js" > "$QUALIFICATION_DIR/segmenter.log" 2>&1 "$candidate" test test/js/node/vm/vm.test.ts -t 'a Proxy in the prototype chain' > "$QUALIFICATION_DIR/proxy.log" 2>&1 +"$candidate" test test/js/node/process/process.test.js -t 'ArrayBuffer memory ownership' > "$QUALIFICATION_DIR/arraybuffer.log" 2>&1 +"$candidate" test test/js/node/worker_threads/worker_threads.test.ts -t 'getHeapStatistics' > "$QUALIFICATION_DIR/worker-statistics.log" 2>&1 "$candidate" test test/js/node/worker_threads/openclaw-resource-limits.test.ts -t 'resourceLimits' > "$QUALIFICATION_DIR/resource-limits.log" 2>&1 "$candidate" "$inputs/module-context.mjs" matrix > "$QUALIFICATION_DIR/als-plugin.json" "$candidate" "$inputs/module-context.mjs" matrix --native-hooks > "$QUALIFICATION_DIR/als-native.json" @@ -76,7 +81,7 @@ assert faith['selected_files']==44 and faith['result_files']==46 for name in ['als-plugin','als-native']: rows=json.loads((root/(name+'.json')).read_text())['results'] assert len(rows)==14 and all(r['exitCode']==0 for r in rows) -for name,minimum in [('proxy',4),('resource-limits',14),('namespace',41)]: +for name,minimum in [('proxy',4),('resource-limits',14),('namespace',41),('arraybuffer',19),('worker-statistics',7)]: text=(root/(name+'.log')).read_text() passes=re.search(r'(\d+) pass',text) assert passes and int(passes[1])>=minimum, name+' missing passing cases' diff --git a/.github/openclaw/release-notes.md b/.github/openclaw/release-notes.md index 153f8cf150ab8..5616a9cfa535e 100644 --- a/.github/openclaw/release-notes.md +++ b/.github/openclaw/release-notes.md @@ -4,10 +4,13 @@ This release rebases the qualified OpenClaw engine patches onto upstream `5718a6 - Preserve Segmenter surrogate boundaries, opt-in Proxy global prototypes, module-loader AsyncLocalStorage context, and per-VM worker heap/stack budgets; thanks @steipete and @robobun. - Give require(esm) a separate live namespace with its own `__esModule` marker while preserving unmarked import namespaces; thanks @robobun. - Add VM-owned byte-based allocation sampling, allocating-stack profiles, and independent major/minor GC inclusion. +- Preserve ArrayBuffer allocation ownership across transfers, separate backing storage from JavaScript heap statistics, and inspect worker memory on its owning VM during execution and atomic waits; thanks @robobun for the upstream accounting investigation. +- Synchronize the accounting-related parallel-helper assertion and visited-memory counters. +- Preserve syntax-selected stack positions for calls, constructors, property reads and async continuations, including live/captured stacks and cache replay; thanks @robobun for the upstream source-position work. -Qualification includes all nine artifact builds, Linux JSC module/promise/namespace/Segmenter and allocation-sampling regressions, the W113 paired Bun selection, and a separate build of the prepared upstream-synced Bun source with the complete fork selection, patch regressions, startup checks, and unchanged upstream memory-release tests. Other targets have build/provenance proof; native runtime qualification is Linux x64. +Qualification includes all nine artifact builds, Linux JSC module/promise/namespace/Segmenter and allocation-sampling regressions, the W113 paired Bun selection, a build of the exact prepared upstream-synced Bun runtime with its required namespace API adapter and candidate manifest with startup and unchanged upstream memory-release tests, and a separate build with feature adapters for the complete fork selection and patch regressions. Syntax-selected stack positions are checked across execution tiers, live/captured stacks, and cache replay. Other targets have build/provenance proof; native runtime qualification is Linux x64. -The new engine requires Bun's updated mimalloc idle hook and matching embedding adapters. Consumers must rebuild with the manifest's exact headers and libraries; an older executable is not interchangeable. +The new engine requires Bun's updated mimalloc idle hook and matching embedding adapters. A manifest-only update of the original sync tree does not compile: its old namespace marker access must be replaced by the namespace adapter (Bun #106), paired atomically with this engine. Consumers must rebuild with the manifest's exact headers and libraries; an older executable is not interchangeable. Source and licenses: LICENSE-SOURCES.txt. Checksums: SHA256SUMS and manifest.json. Build and qualification evidence: provenance.tar.gz. diff --git a/.github/openclaw/release.py b/.github/openclaw/release.py index 9768e85acb658..a9e94ee9729c1 100644 --- a/.github/openclaw/release.py +++ b/.github/openclaw/release.py @@ -32,6 +32,12 @@ def assemble(inputs,out,source): if (sync.get('passed') is not True or any(sync.get(k)!=config['upstream_sync'][k] for k in ('base_commit','prepared_tree')) or engine.get('passed') is not True or engine.get('source')!=source): raise ValueError('missing or mismatched upstream-sync/engine qualification') + compatibility=gate.get('compatibility',{}) + if (compatibility.get('passed') is not True or compatibility.get('source')!=source + or any(compatibility.get(k)!=config['upstream_sync'][k] for k in ('base_commit','prepared_tree','paired_tree','required_namespace_adapter_sha256')) + or compatibility.get('only_manifest_changed_after_pairing') is not True or compatibility.get('memory_tests')!=3 + or compatibility.get('startup') is not True): + raise ValueError('missing or mismatched namespace-paired Bun compatibility gate') artifacts={}; lanes=[] for lane in config['lanes']: label=lane['label']; name=label+'.tar.gz'; directory=inputs/label diff --git a/.github/openclaw/test_release.py b/.github/openclaw/test_release.py index 36d561ea6c8d6..828db29d91b2f 100644 --- a/.github/openclaw/test_release.py +++ b/.github/openclaw/test_release.py @@ -17,7 +17,7 @@ def setUp(self): self.root=Path(self.temp.name);self.inputs=self.root/'inputs';self.inputs.mkdir() self.out=self.root/'release' config=json.loads((release.ROOT/'.github/openclaw/variants.json').read_text()) - q=self.inputs/'qualification';q.mkdir();(q/'gate.json').write_text(json.dumps({'passed':True,'source':SOURCE,'bun_commit':config['bun_commit'],'upstream_sync':{'passed':True,**config['upstream_sync']},'engine':{'passed':True,'source':SOURCE}})) + q=self.inputs/'qualification';q.mkdir();(q/'gate.json').write_text(json.dumps({'passed':True,'source':SOURCE,'bun_commit':config['bun_commit'],'upstream_sync':{'passed':True,**config['upstream_sync']},'engine':{'passed':True,'source':SOURCE},'compatibility':{'passed':True,'source':SOURCE,**config['upstream_sync'],'only_manifest_changed_after_pairing':True,'memory_tests':3,'startup':True}})) for lane in config['lanes']: label=lane['label'];d=self.inputs/label;d.mkdir();(d/'provenance').mkdir() f=d/(label+'.tar.gz');f.write_bytes(label.encode()) @@ -97,6 +97,33 @@ def test_authorization_accepts_protected_batch_branch(self): def test_missing_upstream_sync_gate_rejected(self): p=self.inputs/'qualification/gate.json';m=json.loads(p.read_text());del m['upstream_sync'];p.write_text(json.dumps(m)) with self.assertRaisesRegex(ValueError,'upstream-sync'):self.assemble() + def test_missing_paired_compatibility_rejected(self): + p=self.inputs/'qualification/gate.json';m=json.loads(p.read_text());del m['compatibility'];p.write_text(json.dumps(m)) + with self.assertRaisesRegex(ValueError,'namespace-paired'):self.assemble() + def test_missing_or_failed_startup_rejected(self): + p=self.inputs/'qualification/gate.json' + for value in [None,False]: + with self.subTest(startup=value): + m=json.loads(p.read_text()) + if value is None:m['compatibility'].pop('startup',None) + else:m['compatibility']['startup']=value + p.write_text(json.dumps(m)) + with self.assertRaisesRegex(ValueError,'namespace-paired'):self.assemble() + if self.out.exists():self.out.rmdir() + def test_wrong_paired_tree_or_namespace_adapter_rejected(self): + p=self.inputs/'qualification/gate.json' + original=json.loads(p.read_text()) + for key in ['paired_tree','required_namespace_adapter_sha256']: + with self.subTest(field=key): + m=json.loads(json.dumps(original));m['compatibility'][key]='b'*40;p.write_text(json.dumps(m)) + with self.assertRaisesRegex(ValueError,'namespace-paired'):self.assemble() + if self.out.exists():self.out.rmdir() + def test_wrong_prepared_tree_rejected(self): + p=self.inputs/'qualification/gate.json';m=json.loads(p.read_text());m['compatibility']['prepared_tree']='b'*40;p.write_text(json.dumps(m)) + with self.assertRaisesRegex(ValueError,'namespace-paired'):self.assemble() + def test_extra_runtime_changes_rejected(self): + p=self.inputs/'qualification/gate.json';m=json.loads(p.read_text());m['compatibility']['only_manifest_changed_after_pairing']=False;p.write_text(json.dumps(m)) + with self.assertRaisesRegex(ValueError,'namespace-paired'):self.assemble() def test_wrong_sync_tree_rejected(self): p=self.inputs/'qualification/gate.json';m=json.loads(p.read_text());m['upstream_sync']['prepared_tree']='b'*40;p.write_text(json.dumps(m)) with self.assertRaisesRegex(ValueError,'upstream-sync'):self.assemble() diff --git a/.github/openclaw/test_verify_jsc_results.py b/.github/openclaw/test_verify_jsc_results.py new file mode 100644 index 0000000000000..6c2e7b911aab6 --- /dev/null +++ b/.github/openclaw/test_verify_jsc_results.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +import json +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +SCRIPT = Path(__file__).with_name("verify-jsc-results.py") + + +class JSCResultTests(unittest.TestCase): + def run_check(self, rows, *args): + with tempfile.TemporaryDirectory() as directory: + (Path(directory) / "results").write_text("\n".join(rows)) + return subprocess.run([sys.executable, str(SCRIPT), directory, *args], capture_output=True, text=True) + + def test_default_requires_namespace(self): + self.assertNotEqual(self.run_check(["stress/another.js.default: PASS"]).returncode, 0) + result = self.run_check(["stress/module-namespace-esmodule-marker.js.default: PASS"]) + self.assertEqual(result.returncode, 0, result.stderr) + + def test_all_requested_regressions_must_execute(self): + result = self.run_check(["stress/callsite.js.default: PASS"], "--require", "callsite.js", "--require", "property.js") + self.assertNotEqual(result.returncode, 0) + self.assertIn("property.js", result.stderr) + + def test_any_failed_configuration_rejects_results(self): + result = self.run_check(["stress/callsite.js.default: PASS", "stress/callsite.js.ftl-eager: FAIL"], "--require", "callsite.js") + self.assertNotEqual(result.returncode, 0) + self.assertIn("ftl-eager: FAIL", result.stderr) + self.assertNotEqual(self.run_check([], "--require", "callsite.js").returncode, 0) + + def test_reports_all_passing_variants(self): + result = self.run_check(["stress/callsite.js.default: PASS", "stress/callsite.js.ftl-eager: PASS", "stress/property.js.default: PASS"], "--require", "callsite.js", "--require", "property.js") + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(json.loads(result.stdout), {"tests": 3, "required_variants": {"callsite.js": 2, "property.js": 1}, "passed": True}) + + +if __name__ == "__main__": + unittest.main() diff --git a/.github/openclaw/variants.json b/.github/openclaw/variants.json index f6075d745dd8d..9965abafd9726 100644 --- a/.github/openclaw/variants.json +++ b/.github/openclaw/variants.json @@ -44,6 +44,8 @@ "upstream_sync": { "base_commit": "d2d2a26ef973cdd97b37953f5dba58052acad74f", "upstream_commit": "1878660bb47a6a87ebe518cea0581ef4bf9b71f8", - "prepared_tree": "d9ec052fccc81afbf43a5344e52bc980f439d587" + "prepared_tree": "d9ec052fccc81afbf43a5344e52bc980f439d587", + "paired_tree": "aee7cd52d7efcb9835b434a24fa47de06cc664f6", + "required_namespace_adapter_sha256": "6fdc093961584d2bd77176dcbaf487ab7ec07e14e5a19d11f881d751976a569d" } } diff --git a/.github/openclaw/verify-jsc-results.py b/.github/openclaw/verify-jsc-results.py index e6bfcea1bb07b..9fa754c51001a 100644 --- a/.github/openclaw/verify-jsc-results.py +++ b/.github/openclaw/verify-jsc-results.py @@ -1,17 +1,23 @@ #!/usr/bin/env python3 """The stress runner's results file, not its exit status, is authoritative.""" +import argparse import json -import sys from pathlib import Path -root = Path(sys.argv[1]) +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument("root", type=Path) +parser.add_argument("--require", action="append", dest="required", help="Regression filename that must execute (repeatable)") +args = parser.parse_args() +root = args.root lines = (root / "results").read_text().splitlines() if not lines: raise SystemExit("JSC produced no test results") -namespace = [line for line in lines if "module-namespace-esmodule-marker.js" in line] -if not namespace: - raise SystemExit("The namespace marker regression did not execute") +required = args.required or ["module-namespace-esmodule-marker.js"] +variants = {name: sum(name in line for line in lines) for name in required} +missing = [name for name, count in variants.items() if not count] +if missing: + raise SystemExit("JSC regressions did not execute: " + ", ".join(missing)) failures = [line for line in lines if not line.endswith(": PASS")] if failures: raise SystemExit("JSC failures:\n" + "\n".join(failures)) -print(json.dumps({"tests": len(lines), "marker_variants": len(namespace), "passed": True})) +print(json.dumps({"tests": len(lines), "required_variants": variants, "passed": True})) diff --git a/.github/workflows/openclaw-artifacts.yml b/.github/workflows/openclaw-artifacts.yml index 41249f59d89f7..f566c8a84bf51 100644 --- a/.github/workflows/openclaw-artifacts.yml +++ b/.github/workflows/openclaw-artifacts.yml @@ -25,7 +25,7 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with: - fetch-depth: 16 + fetch-depth: 64 persist-credentials: false sparse-checkout-cone-mode: false sparse-checkout: | @@ -40,7 +40,9 @@ jobs: with: node-version: '26.3.0' - name: Test publication integrity gates - run: python3 .github/openclaw/test_release.py + run: | + python3 .github/openclaw/test_release.py + python3 .github/openclaw/test_verify_jsc_results.py - name: Measure runner capacity and Docker support run: bash .github/openclaw/preflight.sh - name: Validate reviewed source and exact consumer matrix @@ -59,7 +61,7 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with: - fetch-depth: 16 + fetch-depth: 64 persist-credentials: false sparse-checkout-cone-mode: false sparse-checkout: | @@ -130,6 +132,7 @@ jobs: run: | bash .github/openclaw/qualify-engine.sh bash .github/openclaw/qualify.sh + bash .github/openclaw/qualify-sync-compatibility.sh bash .github/openclaw/qualify-sync.sh - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a if: always() @@ -144,9 +147,13 @@ jobs: ${{ runner.temp }}/qualification/baseline/*.log ${{ runner.temp }}/qualification/candidate/*.json ${{ runner.temp }}/qualification/candidate/*.log + ${{ runner.temp }}/qualification/compatibility/*.log ${{ runner.temp }}/qualification/sync/*.json ${{ runner.temp }}/qualification/sync/*.log ${{ runner.temp }}/qualification/jsc-*/results + ${{ runner.temp }}/qualification/jsc-stack/*.log + ${{ runner.temp }}/qualification/jsc-stack/*.json + ${{ runner.temp }}/qualification/sync/runtime-cache/*.json if-no-files-found: error retention-days: 7 assemble: diff --git a/.github/workflows/openclaw-checks.yml b/.github/workflows/openclaw-checks.yml index beda0ee11d813..72d2a1b97eadf 100644 --- a/.github/workflows/openclaw-checks.yml +++ b/.github/workflows/openclaw-checks.yml @@ -16,7 +16,7 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with: - fetch-depth: 16 + fetch-depth: 64 persist-credentials: false sparse-checkout-cone-mode: false sparse-checkout: | @@ -37,6 +37,7 @@ jobs: - name: Verify pipeline integrity and runner support run: | python3 .github/openclaw/test_release.py + python3 .github/openclaw/test_verify_jsc_results.py bash .github/openclaw/preflight.sh - name: Build native Linux engine with the unchanged lane env: @@ -61,6 +62,7 @@ jobs: run: | bash .github/openclaw/qualify-engine.sh bash .github/openclaw/qualify.sh + bash .github/openclaw/qualify-sync-compatibility.sh bash .github/openclaw/qualify-sync.sh - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a if: always() @@ -75,8 +77,12 @@ jobs: ${{ runner.temp }}/qualification/baseline/*.log ${{ runner.temp }}/qualification/candidate/*.json ${{ runner.temp }}/qualification/candidate/*.log + ${{ runner.temp }}/qualification/compatibility/*.log ${{ runner.temp }}/qualification/sync/*.json ${{ runner.temp }}/qualification/sync/*.log ${{ runner.temp }}/qualification/jsc-*/results + ${{ runner.temp }}/qualification/jsc-stack/*.log + ${{ runner.temp }}/qualification/jsc-stack/*.json + ${{ runner.temp }}/qualification/sync/runtime-cache/*.json if-no-files-found: error retention-days: 7 diff --git a/JSTests/stress/array-buffer-accounting.js b/JSTests/stress/array-buffer-accounting.js new file mode 100644 index 0000000000000..81563bb56c98e --- /dev/null +++ b/JSTests/stress/array-buffer-accounting.js @@ -0,0 +1,177 @@ +function equal(actual, expected, label) { + if (actual !== expected) + throw new Error(`${label}: ${actual} !== ${expected}`); +} + +function memoryUsage() { + var result = arrayBufferMemoryUsage(); + // JIT and native object allocations are independent of the backing payload contract. + result.external = result.arrayBufferPayload; + return result; +} +var emptyBefore = arrayBufferMemoryUsage(); +var emptyShared = new SharedArrayBuffer(0); +var emptyAfter = arrayBufferMemoryUsage(); +if (emptyAfter.arrayBufferObjects <= emptyBefore.arrayBufferObjects) + throw new Error("missing native ArrayBuffer object accounting"); +equal(emptyAfter.external - emptyBefore.external, + emptyAfter.arrayBufferObjects - emptyBefore.arrayBufferObjects, "native object external charge"); +equal(emptyAfter.arrayBuffers, emptyBefore.arrayBuffers, "empty backing payload"); + +var retained = []; +function allocate(size) { return new Uint8Array(size); } +noInline(allocate); +for (var i = 0; i < testLoopCount; ++i) + retained.push(allocate(8)); +fullGC(); + +var before = memoryUsage(); +var sizes = [0, 17, 512, 1000, 1001, 4096]; +var bytes = 0; +for (var i = 0; i < sizes.length; ++i) { + retained.push(allocate(sizes[i])); + bytes += sizes[i]; + equal(memoryUsage().arrayBuffers - before.arrayBuffers, bytes, "allocation"); +} +var allocated = memoryUsage(); +equal(allocated.external - before.external, bytes, "external allocation"); +for (var view of retained) + view.buffer; +equal(memoryUsage().arrayBuffers, allocated.arrayBuffers, "materialization"); +edenGC(); +equal(memoryUsage().arrayBuffers, allocated.arrayBuffers, "eden"); +fullGC(); +equal(memoryUsage().arrayBuffers, allocated.arrayBuffers, "full"); + +var rabBefore = memoryUsage(); +var resizable = new ArrayBuffer(4096, { maxByteLength: 32768 }); +equal(memoryUsage().arrayBuffers, rabBefore.arrayBuffers, "resizable allocator"); +equal(memoryUsage().external - rabBefore.external, 4096, "resizable external"); +resizable.resize(16384); +equal(memoryUsage().external - rabBefore.external, 16384, "grow"); +resizable.resize(1024); +equal(memoryUsage().external - rabBefore.external, 1024, "shrink"); +var fixed = resizable.transferToFixedLength(); +equal(resizable.byteLength, 0, "detach"); +equal(memoryUsage().arrayBuffers - rabBefore.arrayBuffers, 1024, "fixed transfer allocator"); +equal(memoryUsage().external - rabBefore.external, 1024, "fixed transfer external"); +var transferred = fixed.transfer(); +equal(memoryUsage().arrayBuffers - rabBefore.arrayBuffers, 1024, "same-size transfer allocator"); +equal(memoryUsage().external - rabBefore.external, 1024, "same-size transfer external"); + +var sharedBefore = memoryUsage(); +var shared = new SharedArrayBuffer(8192); +equal(memoryUsage().arrayBuffers - sharedBefore.arrayBuffers, 8192, "shared allocator"); +equal(memoryUsage().external, sharedBefore.external, "shared external"); +var growable = new SharedArrayBuffer(4096, { maxByteLength: 32768 }); +growable.grow(16384); +equal(memoryUsage().arrayBuffers - sharedBefore.arrayBuffers, 8192, "growable allocator"); +equal(memoryUsage().external, sharedBefore.external, "growable external"); + +// Reconcile dead young vectors before their cells are reused, including oversize finalizers. +var liveBytes = memoryUsage().arrayBuffers; +var temporary = []; +function garbage() { + for (var i = 0; i < 100; ++i) { + temporary.push(new Uint8Array(512)); + temporary.push(new Uint8Array(2048)); + } +} +noInline(garbage); +garbage(); +equal(memoryUsage().arrayBuffers - liveBytes, 256000, "young vector allocation"); +temporary = null; +edenGC(); +equal(memoryUsage().arrayBuffers, liveBytes, "young vector reclamation"); +fullGC(); +equal(memoryUsage().arrayBuffers, liveBytes, "oversize finalization"); + +var oldBefore = memoryUsage().arrayBuffers; +var oldViews = []; +for (var i = 0; i < 128; ++i) + oldViews.push(new Uint8Array(512)); +var trackedAllocations = oldViews.map(view => ({ view: new WeakRef(view), bytes: view.byteLength })); +function liveTrackedBytes() { + var bytes = 0; + for (var entry of trackedAllocations) { + if (entry.view.deref() || (entry.buffer && entry.buffer.deref())) + bytes += entry.bytes; + } + return bytes; +} +fullGC(); +for (var round = 0; round < 8; ++round) { + trackedAllocations[round].buffer = new WeakRef(oldViews[round].buffer); + temporary = []; + garbage(); + for (var index = 0; index < temporary.length; ++index) + trackedAllocations.push({ view: new WeakRef(temporary[index]), bytes: temporary[index].byteLength }); + var trackedBytes = liveTrackedBytes(); + equal(memoryUsage().arrayBuffers, oldBefore + trackedBytes, "old and young vectors"); + temporary = null; + $262.clearKeptObjects(); + edenGC(); + // Compare with the collector's live targets, including any promoted temporaries. + trackedBytes = liveTrackedBytes(); + equal(memoryUsage().arrayBuffers, oldBefore + trackedBytes, "eden matches live old and young storage"); +} +oldViews = null; +$262.clearKeptObjects(); +fullGC(); +trackedBytes = liveTrackedBytes(); +equal(memoryUsage().arrayBuffers, oldBefore + trackedBytes, "full collection prunes dead old views"); +equal(testArrayBufferInspectionWhileWaiting(), true, "inspection of a confirmed parked waiter"); + +function near(actual, expected, label) { + if (Math.abs(actual - expected) > 65536) + throw new Error(`${label}: ${actual} is not near ${expected}`); +} +var warmMemory = new WebAssembly.Memory({ initial: 1, maximum: 256 }); +warmMemory.buffer; +fullGC(); +var wasmBefore = memoryUsage(); +var memory = new WebAssembly.Memory({ initial: 64, maximum: 256 }); +near(memoryUsage().external - wasmBefore.external, 4 * 1024 * 1024, "wasm allocation"); +equal(memoryUsage().arrayBuffers, wasmBefore.arrayBuffers, "wasm allocation origin"); +var memoryBuffer = memory.buffer; +near(memoryUsage().external - wasmBefore.external, 4 * 1024 * 1024, "wasm exposure"); +memory.grow(64); +near(memoryUsage().external - wasmBefore.external, 8 * 1024 * 1024, "wasm grow"); +memoryBuffer = memory.buffer; +memory = null; +fullGC(); +near(memoryUsage().external - wasmBefore.external, 8 * 1024 * 1024, "retained wasm backing store"); +if (memoryUsage().heapUsed - wasmBefore.heapUsed > 1024 * 1024) + throw new Error("wasm payload entered JavaScript heap"); +var sharedMemory = new WebAssembly.Memory({ initial: 64, maximum: 256, shared: true }); +sharedMemory.buffer; +sharedMemory.grow(64); +fullGC(); +near(memoryUsage().external - wasmBefore.external, 8 * 1024 * 1024, "shared wasm excluded"); +equal(memoryUsage().arrayBuffers, wasmBefore.arrayBuffers, "shared wasm allocation origin"); + +// Mode loads in the interpreter and JITs must still guard resizable view bounds. +var modeBuffer = new ArrayBuffer(32, { maxByteLength: 128 }); +var modeAuto = new Uint8Array(modeBuffer); +var modeFixed = new Uint8Array(modeBuffer, 0, 32); +var modeData = new DataView(modeBuffer); +modeAuto[16] = 73; +function readModeView(view) { return view[16]; } +noInline(readModeView); +for (var i = 0; i < testLoopCount; ++i) { + equal(readModeView(modeAuto), 73, "resizable auto view"); + equal(readModeView(modeFixed), 73, "resizable fixed view"); +} +modeBuffer.resize(8); +equal(modeAuto.length, 8, "resized auto length"); +equal(modeFixed.length, 0, "resized fixed bounds"); +equal(readModeView(modeAuto), undefined, "resized auto indexed bounds"); +equal(readModeView(modeFixed), undefined, "resized fixed indexed bounds"); +var modeRangeError = false; +try { modeData.getUint8(16); } catch (error) { modeRangeError = error instanceof RangeError; } +equal(modeRangeError, true, "resized DataView bounds"); +var modeShared = new SharedArrayBuffer(16, { maxByteLength: 128 }); +var modeSharedView = new Uint8Array(modeShared); +modeShared.grow(32); +equal(modeSharedView.length, 32, "grown shared auto length"); +equal(modeSharedView[24], 0, "grown shared indexed bounds"); diff --git a/JSTests/stress/async-stack-trace-promise-all-basic.js b/JSTests/stress/async-stack-trace-promise-all-basic.js index ec393eb69eeae..5e5c2e61e017d 100644 --- a/JSTests/stress/async-stack-trace-promise-all-basic.js +++ b/JSTests/stress/async-stack-trace-promise-all-basic.js @@ -1,5 +1,5 @@ //@ requireOptions("--useAsyncStackTrace=1") - +// Stack columns name syntax tokens, not call parentheses. const source = "async-stack-trace-promise-all-basic.js"; function nop() {} @@ -71,12 +71,12 @@ function shouldThrowAsync(run, errorType, message, stackFunctions) { await run(); }, Error, "error", [ - ["thrower", "65:59"], - ["async run", "66:43"], - ["async test", "71:25"], + ["thrower", "65:50"], + ["async run", "66:26"], + ["async test", "71:16"], ["drainMicrotasks", "[native code]"], - ["shouldThrowAsync", "19:20"], - ["global code", "69:24"] + ["shouldThrowAsync", "19:5"], + ["global code", "69:8"] ], ); drainMicrotasks(); @@ -101,12 +101,12 @@ function shouldThrowAsync(run, errorType, message, stackFunctions) { await runner(); }, Error, "task2 error", [ - ["task2", "93:20"], - ["async runner", "96:46"], - ["async test", "101:28"], + ["task2", "93:11"], + ["async runner", "96:29"], + ["async test", "101:16"], ["drainMicrotasks", "[native code]"], - ["shouldThrowAsync", "19:20"], - ["global code", "99:24"] + ["shouldThrowAsync", "19:5"], + ["global code", "99:8"] ], ); drainMicrotasks(); @@ -140,15 +140,15 @@ function shouldThrowAsync(run, errorType, message, stackFunctions) { await runner(); }, Error, "task2 error", [ - ["baz", "131:20"], - ["async bar", "127:22"], - ["async foo", "123:14"], - ["async task2", "120:14"], - ["async runner", "135:46"], - ["async test", "140:28"], + ["baz", "131:11"], + ["async bar", "127:5"], + ["async foo", "123:5"], + ["async task2", "120:5"], + ["async runner", "135:29"], + ["async test", "140:16"], ["drainMicrotasks", "[native code]"], - ["shouldThrowAsync", "19:20"], - ["global code", "138:24"] + ["shouldThrowAsync", "19:5"], + ["global code", "138:8"] ], ); drainMicrotasks(); diff --git a/JSTests/stress/async-stack-trace-promise-allSettled-basic.js b/JSTests/stress/async-stack-trace-promise-allSettled-basic.js index 356cafea43ed3..0a14f3ee06b44 100644 --- a/JSTests/stress/async-stack-trace-promise-allSettled-basic.js +++ b/JSTests/stress/async-stack-trace-promise-allSettled-basic.js @@ -1,5 +1,5 @@ //@ requireOptions("--useAsyncStackTrace=1") - +// Stack columns name syntax tokens, not call parentheses. const source = "async-stack-trace-promise-allSettled-basic.js"; function nop() {} @@ -60,11 +60,11 @@ function unwrap(promise) { shouldBe(results.length, 1); shouldBe(results[0].status, "rejected"); testStack(results[0].reason, [ - ["thrower", "53:59"], - ["async run", "55:36"], + ["thrower", "53:50"], + ["async run", "55:12"], ["drainMicrotasks", "[native code]"], - ["unwrap", "47:18"], - ["global code", "59:27"] + ["unwrap", "47:3"], + ["global code", "59:21"] ]); } } @@ -98,25 +98,25 @@ function unwrap(promise) { shouldBe(results[3].status, "rejected"); shouldBe(results[4].status, "rejected"); testStack(results[1].reason, [ - ["task1", "78:20"], - ["async run", "89:36"], + ["task1", "78:11"], + ["async run", "89:12"], ["drainMicrotasks", "[native code]"], - ["unwrap", "47:18"], - ["global code", "93:27"] + ["unwrap", "47:3"], + ["global code", "93:21"] ]); testStack(results[3].reason, [ - ["task2", "85:20"], - ["async run", "89:36"], + ["task2", "85:11"], + ["async run", "89:12"], ["drainMicrotasks", "[native code]"], - ["unwrap", "47:18"], - ["global code", "93:27"] + ["unwrap", "47:3"], + ["global code", "93:21"] ]); testStack(results[4].reason, [ - ["task3", "87:52"], - ["async run", "89:36"], + ["task3", "87:43"], + ["async run", "89:12"], ["drainMicrotasks", "[native code]"], - ["unwrap", "47:18"], - ["global code", "93:27"] + ["unwrap", "47:3"], + ["global code", "93:21"] ]); } } diff --git a/JSTests/stress/async-stack-trace-promise-any-basic.js b/JSTests/stress/async-stack-trace-promise-any-basic.js index 0e0f8ff7eeb5c..045388da0f120 100644 --- a/JSTests/stress/async-stack-trace-promise-any-basic.js +++ b/JSTests/stress/async-stack-trace-promise-any-basic.js @@ -1,5 +1,5 @@ //@ requireOptions("--useAsyncStackTrace=1") - +// Stack columns name syntax tokens, not call parentheses. const source = "async-stack-trace-promise-any-basic.js"; function nop() {} @@ -78,12 +78,12 @@ function shouldThrowAsync(run, errorType) { shouldBe(aggregateError.errors.length, 1); testStack(aggregateError.errors[0], [ - ["thrower", "68:59"], - ["async run", "69:43"], - ["async test", "74:18"], + ["thrower", "68:50"], + ["async run", "69:26"], + ["async test", "74:9"], ["drainMicrotasks", "[native code]"], - ["shouldThrowAsync", "56:18"], - ["global code", "72:44"] + ["shouldThrowAsync", "56:3"], + ["global code", "72:28"] ] ); } @@ -116,32 +116,32 @@ function shouldThrowAsync(run, errorType) { shouldBe(aggregateError.errors.length, 3); testStack(aggregateError.errors[0], [ - ["task1", "97:20"], - ["async run", "107:43"], - ["async test", "112:18"], + ["task1", "97:11"], + ["async run", "107:26"], + ["async test", "112:9"], ["drainMicrotasks", "[native code]"], - ["shouldThrowAsync", "56:18"], - ["global code", "110:44"] + ["shouldThrowAsync", "56:3"], + ["global code", "110:28"] ] ); testStack(aggregateError.errors[1], [ - ["task2", "104:20"], - ["async run", "107:43"], - ["async test", "112:18"], + ["task2", "104:11"], + ["async run", "107:26"], + ["async test", "112:9"], ["drainMicrotasks", "[native code]"], - ["shouldThrowAsync", "56:18"], - ["global code", "110:44"] + ["shouldThrowAsync", "56:3"], + ["global code", "110:28"] ] ); testStack(aggregateError.errors[2], [ - ["task3", "106:52"], - ["async run", "107:43"], - ["async test", "112:18"], + ["task3", "106:43"], + ["async run", "107:26"], + ["async test", "112:9"], ["drainMicrotasks", "[native code]"], - ["shouldThrowAsync", "56:18"], - ["global code", "110:44"] + ["shouldThrowAsync", "56:3"], + ["global code", "110:28"] ] ); } diff --git a/JSTests/stress/async-stack-trace-promise-race-basic.js b/JSTests/stress/async-stack-trace-promise-race-basic.js index 4773978522d09..9a05b9f023b94 100644 --- a/JSTests/stress/async-stack-trace-promise-race-basic.js +++ b/JSTests/stress/async-stack-trace-promise-race-basic.js @@ -1,5 +1,5 @@ //@ requireOptions("--useAsyncStackTrace=1") - +// Stack columns name syntax tokens, not call parentheses. const source = "async-stack-trace-promise-race-basic.js"; function nop() {} @@ -70,12 +70,12 @@ function shouldThrowAsync(run, errorType, message, stackFunctions) { async function test() { await run(); }, Error, 'error', [ - ["thrower", "65:59"], - ["async run", "66:44"], - ["async test", "71:18"], + ["thrower", "65:50"], + ["async run", "66:26"], + ["async test", "71:9"], ["drainMicrotasks", "[native code]"], - ["shouldThrowAsync", "19:20"], - ["global code", "69:21"] + ["shouldThrowAsync", "19:5"], + ["global code", "69:5"] ] ); drainMicrotasks(); @@ -104,12 +104,12 @@ function shouldThrowAsync(run, errorType, message, stackFunctions) { async function test() { await run(); }, Error, 'error from task1' , [ - ["task1", "90:20"], - ["async run", "100:44"], - ["async test", "105:18"], + ["task1", "90:11"], + ["async run", "100:26"], + ["async test", "105:9"], ["drainMicrotasks", "[native code]"], - ["shouldThrowAsync", "19:20"], - ["global code", "103:21"] + ["shouldThrowAsync", "19:5"], + ["global code", "103:5"] ] ); drainMicrotasks(); diff --git a/JSTests/stress/callsite-async-position.mjs b/JSTests/stress/callsite-async-position.mjs new file mode 100644 index 0000000000000..84cfc6db79100 --- /dev/null +++ b/JSTests/stress/callsite-async-position.mjs @@ -0,0 +1,12 @@ +async function leaf() { +await Promise.resolve(); +return new Error().stack; +} +async function parent() { +return await leaf(); +} +const trace = await parent(); +const parentFrame = trace.split("\n").find(line => /\bparent(?:\s|@|\()/.test(line)); +const position = /:(\d+):(\d+)\)?$/.exec(parentFrame); +if (!position || +position[1] !== 6 || +position[2] !== 8) + throw new Error(JSON.stringify({ expected: [6, 8], trace })); diff --git a/JSTests/stress/callsite-syntax-positions.js b/JSTests/stress/callsite-syntax-positions.js new file mode 100644 index 0000000000000..32eed3eed0dbc --- /dev/null +++ b/JSTests/stress/callsite-syntax-positions.js @@ -0,0 +1,116 @@ +// Keep call positions independent of exception-expression divots. +let trace; +function capture() { const stack = new Error().stack; trace = stack; return stack; } +function Capture() { this.stack = new Error().stack; trace = this.stack; } +function factory() { return capture; } +const object = { capture }; +const name = "capture"; +const cases = [ + [function () { +capture() + }, 10, 1, "capture()"], + [function () { +capture /* comment */ () + }, 13, 1, "capture /* comment */ ()"], + [function () { +capture +() + }, 16, 1, "capture\n()"], + [function () { +factory()() + }, 20, 10, "factory()()"], + [function () { +factory() +() + }, 24, 1, "factory()\n()"], + [function () { +factory() /* comment */ () + }, 27, 25, "factory() /* comment */ ()"], + [function () { +object.capture() + }, 30, 8, "object.capture()"], + [function () { +object +.capture() + }, 34, 2, "object\n.capture()"], + [function () { +object . capture() + }, 37, 10, "object . capture()"], + [function () { +object. /* comment */ capture() + }, 40, 23, "object. /* comment */ capture()"], + [function () { +object.\u0063apture() + }, 43, 8, "object.\\u0063apture()"], + [function () { +object[name]() + }, 46, 13, "object[name]()"], + [function () { +object["capture"]() + }, 49, 18, "object[\"capture\"]()"], + [function () { +object[ +name +] +() + }, 55, 1, "object[\nname\n]\n()"], + [function () { +(capture)() + }, 58, 10, "(capture)()"], + [function () { +((capture))() + }, 61, 12, "((capture))()"], + [function () { +(object.capture)() + }, 64, 17, "(object.capture)()"], + [function () { +(object[name])() + }, 67, 15, "(object[name])()"], + [function () { +(0, capture)() + }, 70, 13, "(0, capture)()"], + [function () { +capture?.() + }, 73, 10, "capture?.()"], + [function () { +object?.capture() + }, 76, 9, "object?.capture()"], + [function () { +object.capture?.() + }, 79, 17, "object.capture?.()"], + [function () { +object?.capture?.() + }, 82, 18, "object?.capture?.()"], + [function () { +this() + }, 85, 5, "this()"], + [function () { +capture`text` + }, 88, 8, "capture`text`"], + [function () { +Reflect.apply(capture, null, []) + }, 91, 9, "Reflect.apply(capture, null, [])"], + [function () { +new Capture().stack + }, 94, 1, "new Capture().stack"], + [function () { +new +Capture().stack + }, 97, 1, "new\nCapture().stack"], + [function () { +new (Capture)().stack + }, 101, 1, "new (Capture)().stack"], + [function () { +Reflect.construct(Capture, []).stack + }, 104, 9, "Reflect.construct(Capture, []).stack"], +]; +for (let iteration = 0; iteration < Math.min(typeof testLoopCount === "number" ? testLoopCount : 20, 100); ++iteration) { + for (const [fn, line, column, body] of cases) { + trace = ""; + fn.call(capture); + const frames = trace.split("\n").filter(line => /:(\d+):(\d+)\)?$/.test(line)); + const position = /:(\d+):(\d+)\)?$/.exec(frames[1]); + if (!position || +position[1] !== line || +position[2] !== column) + throw new Error(JSON.stringify({ body, expected: [line, column], trace })); + } +} diff --git a/JSTests/stress/stack-position-regressions.js b/JSTests/stress/stack-position-regressions.js new file mode 100644 index 0000000000000..fe71747d9f95d --- /dev/null +++ b/JSTests/stress/stack-position-regressions.js @@ -0,0 +1,104 @@ +"use strict"; +const name = "x"; +const cases = [ + ['Error', function () { +const error = new Error("test"); return error; + }, 5, 15], + ['TypeError', function () { +const error = new TypeError("test"); return error; + }, 8, 15], + ['SyntaxError', function () { +const error = new SyntaxError("test"); return error; + }, 11, 15], + ['RangeError', function () { +const error = new RangeError("test"); return error; + }, 14, 15], + ['ReferenceError', function () { +const error = new ReferenceError("test"); return error; + }, 17, 15], + ['EvalError', function () { +const error = new EvalError("test"); return error; + }, 20, 15], + ['URIError', function () { +const error = new URIError("test"); return error; + }, 23, 15], + ['AggregateError', function () { +const error = new AggregateError([], "test"); return error; + }, 26, 15], + ['arrow-new', () => new Error("arrow"), 28, 25], + ['arrow-type', () => new TypeError("arrow"), 29, 26], + ['arrow-alias', () => new globalThis.Error("arrow"), 30, 27], + ['multiline-new', function () { +const error = new + Error("multiline"); return error; + }, 32, 15], + ['constructor-comment', function () { +const error = new /* comment */ Error("comment"); return error; + }, 36, 15], + ['null-dot', function () { +try { null.x; } catch (error) { return error; } + }, 39, 12], + ['undefined-dot', function () { +try { undefined.x; } catch (error) { return error; } + }, 42, 17], + ['null-length', function () { +try { null.length; } catch (error) { return error; } + }, 45, 12], + ['null-comment', function () { +try { null . /* comment */ x; } catch (error) { return error; } + }, 48, 28], + ['null-escape', function () { +try { null.\u0078; } catch (error) { return error; } + }, 51, 12], + ['null-multiline', function () { +try { null + .x; } catch (error) { return error; } + }, 55, 6], + ['null-bracket', function () { +try { null["x"]; } catch (error) { return error; } + }, 58, 11], + ['null-number', function () { +try { null[1]; } catch (error) { return error; } + }, 61, 11], + ['null-computed', function () { +try { null[name]; } catch (error) { return error; } + }, 64, 11], + ['null-call', function () { +try { null.x(); } catch (error) { return error; } + }, 67, 12], + ['null-parenthesized-call', function () { +try { (null.x)(); } catch (error) { return error; } + }, 70, 13], + ['null-bracket-call', function () { +try { null["x"](); } catch (error) { return error; } + }, 73, 11], + ['null-call-method', function () { +try { null.call(); } catch (error) { return error; } + }, 76, 12], + ['null-apply-method', function () { +try { null.apply(null, []); } catch (error) { return error; } + }, 79, 12], + ['null-has-own', function () { +try { null.hasOwnProperty(name); } catch (error) { return error; } + }, 82, 12], + ]; +function checkPosition(error, line, column, shape, structured) { + const frames = structured ? error.stack : error.stack.split("\n").filter(frame => /:(\d+):(\d+)\)?$/.test(frame)); + const actual = structured ? [frames[0].getLineNumber(), frames[0].getColumnNumber()] : /:(\d+):(\d+)\)?$/.exec(frames[0])?.slice(1).map(Number); + if (!actual || actual[0] !== line || actual[1] !== column) + throw new Error(JSON.stringify({ shape, structured, expected: [line, column], actual, trace: structured ? frames.map(String) : error.stack })); +} +for (let iteration = 0; iteration < Math.min(typeof testLoopCount === "number" ? testLoopCount : 20, 100); ++iteration) { + for (const [shape, create, line, column] of cases) { + checkPosition(create(), line, column, shape, false); + if (typeof process !== "undefined") { + const previous = Error.prepareStackTrace; + try { + Error.prepareStackTrace = (_, frames) => frames; + checkPosition(create(), line, column, shape, true); + } finally { + Error.prepareStackTrace = previous; + } + } + } +} diff --git a/OPENCLAW.md b/OPENCLAW.md index 8fa72b4c68dd0..f25f99405a4f1 100644 --- a/OPENCLAW.md +++ b/OPENCLAW.md @@ -54,8 +54,13 @@ Batch 1 is published from `openclaw/batch-1` so concurrently reviewed batch-2 engine changes can land on `openclaw/main` without entering this release. All publication branches require the same protected environment and full-SHA/green-job checks. -After a green build/test dispatch, dispatch the same full SHA with `publish: -true`. This repeats the build and qualification, then waits at the protected +Pull requests run **OpenClaw engine checks**, a read-only Linux build using the same +lane recipe and Bun qualification gate. It also runs the ArrayBuffer accounting +and confirmed parked-waiter inspection regression with LLInt, DFG, FTL and +concurrent GC. This workflow provides the pre-merge check; it cannot publish. + +For final publication, dispatch the reviewed full SHA with `publish: true`. +This builds every lane and runs all qualification gates before waiting at the protected `openclaw-artifacts` environment. Only the publication job has repository write permission. It assembles exactly the nine archives, `manifest.json`, `SHA256SUMS`, `provenance.tar.gz`, and `LICENSE-SOURCES.txt`. @@ -75,7 +80,8 @@ still checks the published release's immutable state. Publication first atomical creates `autobuild-`, creates a draft, uploads every asset, verifies GitHub's asset digests, and publishes it. It then requires `immutable: true`, downloads every public asset, and checks the manifest and SHA256SUMS again. -Existing tags, drafts and releases are never resumed or overwritten. A partial +A separate `publish: false` dispatch is an optional dry run; it never substitutes +for the final publication run’s own green gates. Existing tags, drafts and releases are never resumed or overwritten. A partial publication needs investigation and a new source commit/tag; there is no replacement option. Inspect the publication receipt before updating a consumer. @@ -126,14 +132,33 @@ artifact as namespace qualification. It does not invoke artifact publication. The separate artifact workflow remains the coordinator's build/test/publish entrypoint. +## ArrayBuffer ownership and worker snapshots + +The accounting adapter in `.github/openclaw/qualification/patches/010-arraybuffer-accounting.patch` +uses the engine's allocation-origin and holder accounts for process, V8 and worker +statistics. The qualification gate exercises its Node-compatible ownership cases +and worker inspections in addition to the existing fork selection. Keep the adapter +in sync with the Bun integration until that integration becomes the qualification +baseline. Engine and Bun headers and libraries must be rebuilt together. + ## Unreleased +No changes yet. + +## Rebased engine batch + +- Keep syntax-selected stack positions separate from exception ranges for calls, constructors, property reads, and async continuations. Preserve the positions across bytecode rewriting, optimization, and cache replay. + - Add real byte-based JSC heap allocation sampling with weighted allocating-stack profiles and independent major/minor collection inclusion. - Check allocation sampling across interpreter and JIT tiers before merging engine pull requests. +- Preserve ArrayBuffer allocator attribution across worker transfers, report fresh holder external bytes, and exclude backing storage from JS heap statistics and worker limits; builds on oven-sh/WebKit#303, thanks @robobun. +- Inspect worker memory on its owning VM thread during JavaScript execution and atomic waits; the matching Bun reporting adapter must ship with this engine. +- Synchronize the parallel-helper activity assertion with task claims and completion; fixes a race exposed by ThreadSanitizer during accounting qualification. +- Use atomic loads and resets for visited extra/external-memory totals updated by parallel markers. ## Rebased release qualification -- Rebase the four batch-1 engine patches and the landed namespace and allocation-sampling patches onto oven-sh/WebKit `5718a6ec579b98362ea7276a426deedcc6281ef5` (upstream #768). Thanks @Jarred-Sumner for idle compiler-thread and Atomics.wait memory release. +- Rebase the four batch-1 engine patches and the landed namespace, allocation-sampling, stack-position and ArrayBuffer-accounting patches onto oven-sh/WebKit `5718a6ec579b98362ea7276a426deedcc6281ef5` (upstream #768). Thanks @Jarred-Sumner for idle compiler-thread and Atomics.wait memory release. - Retain the published batch-1 release. Publish this separately qualified batch only through the protected `openclaw/release-5718a6ec` environment policy; updating the Bun main manifest is a separate integration task. The existing qualification baseline remains upstream `1600131e46b5af48bbda3559af8d8a3327230b6e` with its committed archive checksums. The source/provenance base advances to `5718a6ec579b98362ea7276a426deedcc6281ef5`; the Docker and lane recipes are unchanged. Unlanded engine work is not included in this rebase. @@ -167,3 +192,33 @@ ALS hook/continuous-GC matrices, realm/retention probes, and Bun inspector tests The upstream delta changes only waiting/compiler-thread allocator release and its bridge; module-loader, namespace, managed-heap and inspector patches remain patch-identical on the new base. +## Stack positions + +`CodeBlock::callSitePositionForBytecodeIndex` returns an optional absolute source offset for an exact instruction or async resume label. Stack readers use this position while exception expression ranges and debugger line tables retain their existing divots. RareData owns the two sparse vectors, including decoded cache entries; the bytecode cache revision prevents older layouts from being reused. + +Bun must be rebuilt with its matching stack-reader adapter. It must also preserve built-in error constructor syntax: rewriting `new Error()` as `Error()` changes the position and allows a returning arrow to become a proper tail call. The engine's proper-tail-call behavior is unchanged. The integration continues @robobun’s upstream Bun source-position work in [#35179](https://github.com/oven-sh/bun/pull/35179), [#37396](https://github.com/oven-sh/bun/pull/37396), and [#41580](https://github.com/oven-sh/bun/pull/41580). + +The release gate includes the landed stack-position adapter, the complete CallSite/util/source-map and minifier regressions, live stack visitor, async positions, and owned/persistent JSC cache replay. + + +The compatibility arm first reproduces that exact W150 tree, then applies only +its required namespace API counterpart from `patches/008-namespace.patch`. +The adapter SHA-256 and resulting tree `aee7cd52d7efcb9835b434a24fa47de06cc664f6` +are pinned in `variants.json`. A manifest-only attempt against the original +prepared tree failed to compile because it accessed the removed public +`TriState` namespace marker. **Bun #106 (or this exact adapter) must accompany +the manifest update.** No legacy marker shim is restored in the engine. + +After that explicit pairing, the index must retain the pinned paired tree +before and after compilation, and the manifest must be the sole tracked +worktree change. Startup and all three unchanged memory-release tests must pass. +The separate feature build applies the remaining adapters for the full regression +selection. Both gates are mandatory for assembly and publication; the failed +manifest-only result is retained as dependency evidence. + +The ArrayBuffer patch is qualified in five interpreter/JIT/concurrent-GC modes, +with process ownership, worker snapshot, native external-buffer and serializer +regressions. A combined regression inspects a worker during the new idle-memory +purge and notification windows. VM inspection runs before the idle-purge branch, +with the waiter-list lock dropped and VM heap access restored only for inspection; +both paths recheck notification/termination before waiting again. diff --git a/ReadMe.md b/ReadMe.md index e2ee4f6d42d7a..13a377163aa2e 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -20,6 +20,50 @@ The changes to WebKit are as follows: Still need to figure out how to get the remote inspector to work. +### ArrayBuffer accounting in Bun builds + +`Heap::arrayBufferSize()` reports payload bytes allocated for fixed-length +ArrayBuffers and typed-array vectors by this VM. The allocation charge travels +with `ArrayBufferContents` through transfer and is released when the backing store +is destroyed, even if that happens after the allocating VM exits. Shared fixed +storage is charged once, to the first VM exposing the allocation. Resizable and +growable storage, WebAssembly memory, and externally supplied native storage do +not use the ArrayBuffer allocator and receive no allocation-origin charge. + +`externalMemorySizeForReporting()` tracks unshared backing storage in the VM that +currently holds it, including resized payload lengths. Shared storage has no +holder-external charge. Native storage supplied through `createFromBytes` receives +only a holder charge; `createAdopted` transfers ownership of a typed-array vector. +Native adopters select `ArrayBufferAllocationMode::RuntimeOwned` for new buffer +allocations whose ownership passes to JavaScript. The default `External` mode +keeps foreign/N-API backing storage out of the allocation-origin counter. +Non-buffer native memory adds reported allocations since the last completed +collection to that collection's estimate. +The actual C++ `ArrayBuffer` object storage contributes separately to external +memory while it is attached to a VM. It is never counted as backing payload in +`arrayBuffers`, and a detached or shared buffer retains only this native object +charge. Reattaching a native buffer after its GC references are gone moves its +holder charge without changing allocation origin. + +Unshared WebAssembly memory charges its memory handle, before `.buffer` is +exposed and as pages grow. Its ArrayBuffer wrappers do not add another charge. +The handle retains that charge while a buffer outlives its memory wrapper; +shared WebAssembly memory contributes to neither payload counter. + +`jsHeapSizeForReporting()` and `jsHeapCapacityForReporting()` exclude backing +storage using the same allocation and collection epochs as the GC's totals. +Fast typed arrays have a distinct auxiliary subspace, and their inline DFG/FTL +allocations register in a weak list pruned before sweeping. Materialization +replaces the vector charge with a backing-store charge; finalizers do not retire +the charge a second time. + +These statistics are read on the VM's owning thread. Embedders may register a +non-JavaScript inspection with `VMTraps::requestVMInspection()` while protecting +the VM's lifetime, and also schedule `runVMInspections()` on their event loop for +idle VMs. Traps and event-loop delivery drain the same queue exactly once. VM +teardown drops unrun callbacks; the embedder owns request cancellation and promise +settlement. Inspections must not run JavaScript or initiate a collection. + _NOTE_: If you make a change to this repo make sure to update the commit hash in `cmake/tools/SetupWebKit.cmake` in Bun. --- diff --git a/Source/JavaScriptCore/CMakeLists.txt b/Source/JavaScriptCore/CMakeLists.txt index 11876f9f6ceb4..97e4cf8da305a 100644 --- a/Source/JavaScriptCore/CMakeLists.txt +++ b/Source/JavaScriptCore/CMakeLists.txt @@ -1145,6 +1145,7 @@ set(JavaScriptCore_PRIVATE_FRAMEWORK_HEADERS runtime/ArgumentsMode.h runtime/ArityCheckMode.h runtime/ArrayBuffer.h + runtime/ArrayBufferMemoryAccounting.h runtime/ArrayBufferSharingMode.h runtime/ArrayBufferView.h runtime/ArrayConstructor.h diff --git a/Source/JavaScriptCore/bytecode/BytecodeOptimizer.cpp b/Source/JavaScriptCore/bytecode/BytecodeOptimizer.cpp index 69ba7bd481d58..b00e8aca93cca 100644 --- a/Source/JavaScriptCore/bytecode/BytecodeOptimizer.cpp +++ b/Source/JavaScriptCore/bytecode/BytecodeOptimizer.cpp @@ -2144,6 +2144,18 @@ void BytecodeOptimizerAccess::emit() return newOffsetForOld(probe); }); + for (auto* positions : { &m_codeBlock->m_callSitePositions, &m_codeBlock->m_asyncResumePositions }) { + positions->removeAllMatching([&](auto& position) { + if (position.first >= m_offsetToIndex.size()) + return true; + unsigned index = m_offsetToIndex[position.first]; + if (index == UINT_MAX || !m_insns[index].live) + return true; + position.first = m_insns[index].newOffset; + return false; + }); + } + // All jumps were sized to fit; nothing is out of line anymore. m_codeBlock->replaceOutOfLineJumpTargets(); diff --git a/Source/JavaScriptCore/bytecode/BytecodeRewriter.h b/Source/JavaScriptCore/bytecode/BytecodeRewriter.h index ab3537881dd50..57feb9a95213b 100644 --- a/Source/JavaScriptCore/bytecode/BytecodeRewriter.h +++ b/Source/JavaScriptCore/bytecode/BytecodeRewriter.h @@ -206,6 +206,17 @@ WTF_MAKE_NONCOPYABLE(BytecodeRewriter); return adjustJumpTarget(InsertionPoint(0, Position::Entrypoint), InsertionPoint(absoluteOffset, Position::LabelPoint)); } + std::optional adjustInstructionOffset(JSInstructionStream::Offset offset) + { + for (const auto& insertion : m_insertions) { + if (insertion.type == Insertion::Type::Remove + && offset >= static_cast(insertion.index.bytecodeOffset) + && offset - insertion.index.bytecodeOffset < insertion.removeLength) + return std::nullopt; + } + return adjustJumpTarget(InsertionPoint(0, Position::Entrypoint), InsertionPoint(offset, Position::OriginalBytecodePoint)); + } + int32_t adjustJumpTarget(JSInstructionStream::Offset originalBytecodeOffset, int32_t originalJumpTarget) { return adjustJumpTarget(InsertionPoint(originalBytecodeOffset, Position::LabelPoint), InsertionPoint(originalJumpTarget, Position::LabelPoint)); diff --git a/Source/JavaScriptCore/bytecode/CodeBlock.cpp b/Source/JavaScriptCore/bytecode/CodeBlock.cpp index ca3f29cb81722..c960c1d248072 100644 --- a/Source/JavaScriptCore/bytecode/CodeBlock.cpp +++ b/Source/JavaScriptCore/bytecode/CodeBlock.cpp @@ -2592,6 +2592,14 @@ ExpressionInfo::Entry CodeBlock::expressionInfoForBytecodeIndex(BytecodeIndex by return entry; } +std::optional CodeBlock::callSitePositionForBytecodeIndex(BytecodeIndex index, bool isAsync) const +{ + auto offset = m_unlinkedCode->callSitePositionForBytecodeIndex(index, isAsync); + if (!offset) + return std::nullopt; + return *offset + sourceOffset(); +} + bool CodeBlock::hasOpDebugForLineAndColumn(unsigned line, std::optional column) { const auto& instructionStream = instructions(); diff --git a/Source/JavaScriptCore/bytecode/CodeBlock.h b/Source/JavaScriptCore/bytecode/CodeBlock.h index 483afcf94e818..25ad03afe38bc 100644 --- a/Source/JavaScriptCore/bytecode/CodeBlock.h +++ b/Source/JavaScriptCore/bytecode/CodeBlock.h @@ -289,6 +289,7 @@ class CodeBlock : public JSCell { // For a thread that runs beside the mutator. It keeps nothing, so it decodes the expression info on every call. LineColumn lineColumnForBytecodeIndexConcurrently(BytecodeIndex) const; ExpressionInfo::Entry expressionInfoForBytecodeIndex(BytecodeIndex) const; + JS_EXPORT_PRIVATE std::optional callSitePositionForBytecodeIndex(BytecodeIndex, bool isAsync = false) const; std::optional bytecodeIndexFromCallSiteIndex(CallSiteIndex); diff --git a/Source/JavaScriptCore/bytecode/UnlinkedCodeBlock.cpp b/Source/JavaScriptCore/bytecode/UnlinkedCodeBlock.cpp index d7d0085565b7b..021ffa3a70673 100644 --- a/Source/JavaScriptCore/bytecode/UnlinkedCodeBlock.cpp +++ b/Source/JavaScriptCore/bytecode/UnlinkedCodeBlock.cpp @@ -153,6 +153,19 @@ size_t UnlinkedCodeBlock::estimatedSize(JSCell* cell, VM& vm) return Base::estimatedSize(cell, vm) + extraSize; } +std::optional UnlinkedCodeBlock::callSitePositionForBytecodeIndex(BytecodeIndex index, bool isAsync) const +{ + if (!m_rareData) + return std::nullopt; + const auto& positions = isAsync ? m_rareData->m_asyncResumePositions : m_rareData->m_callSitePositions; + auto it = std::lower_bound(positions.begin(), positions.end(), index.offset(), [](const auto& position, unsigned offset) { + return position.first < offset; + }); + if (it == positions.end() || it->first != index.offset()) + return std::nullopt; + return it->second; +} + size_t UnlinkedCodeBlock::RareData::sizeInBytes(const AbstractLocker&) const { size_t size = sizeof(RareData); @@ -162,6 +175,8 @@ size_t UnlinkedCodeBlock::RareData::sizeInBytes(const AbstractLocker&) const size += m_unlinkedStringSwitchJumpTables.byteSize(); size += m_typeProfilerInfoMap.capacity() * sizeof(decltype(m_typeProfilerInfoMap)::KeyValuePairType); size += m_opProfileControlFlowBytecodeOffsets.byteSize(); + size += m_callSitePositions.byteSize(); + size += m_asyncResumePositions.byteSize(); size += m_bitVectors.byteSize(); // FIXME: account for each bit vector. size += m_constantIdentifierSets.byteSize(); diff --git a/Source/JavaScriptCore/bytecode/UnlinkedCodeBlock.h b/Source/JavaScriptCore/bytecode/UnlinkedCodeBlock.h index 4cca7ec3062aa..b841129dbb51d 100644 --- a/Source/JavaScriptCore/bytecode/UnlinkedCodeBlock.h +++ b/Source/JavaScriptCore/bytecode/UnlinkedCodeBlock.h @@ -486,6 +486,9 @@ class UnlinkedCodeBlock : public JSCell { public: using OutOfLineJumpTargets = UncheckedKeyHashMap; + using CallSitePosition = std::pair; + std::optional callSitePositionForBytecodeIndex(BytecodeIndex, bool isAsync) const; + struct RareData { WTF_DEPRECATED_MAKE_STRUCT_FAST_ALLOCATED_WITH_HEAP_IDENTIFIER(RareData, UnlinkedCodeBlock_RareData); @@ -504,6 +507,8 @@ class UnlinkedCodeBlock : public JSCell { }; UncheckedKeyHashMap m_typeProfilerInfoMap; FixedVector m_opProfileControlFlowBytecodeOffsets; + FixedVector m_callSitePositions; + FixedVector m_asyncResumePositions; FixedVector m_bitVectors; FixedVector m_constantIdentifierSets; diff --git a/Source/JavaScriptCore/bytecode/UnlinkedCodeBlockGenerator.cpp b/Source/JavaScriptCore/bytecode/UnlinkedCodeBlockGenerator.cpp index de910491fd379..9ec22d3fea41e 100644 --- a/Source/JavaScriptCore/bytecode/UnlinkedCodeBlockGenerator.cpp +++ b/Source/JavaScriptCore/bytecode/UnlinkedCodeBlockGenerator.cpp @@ -81,6 +81,8 @@ bool UnlinkedCodeBlockGenerator::finalize(std::unique_ptr i || !m_unlinkedStringSwitchJumpTables.isEmpty() || !m_typeProfilerInfoMap.isEmpty() || !m_opProfileControlFlowBytecodeOffsets.isEmpty() + || !m_callSitePositions.isEmpty() + || !m_asyncResumePositions.isEmpty() || !m_bitVectors.isEmpty() || !m_constantIdentifierSets.isEmpty()) m_codeBlock->createRareDataIfNecessary(locker); @@ -92,6 +94,8 @@ bool UnlinkedCodeBlockGenerator::finalize(std::unique_ptr i m_codeBlock->m_rareData->m_unlinkedStringSwitchJumpTables = WTF::move(m_unlinkedStringSwitchJumpTables); m_codeBlock->m_rareData->m_typeProfilerInfoMap = WTF::move(m_typeProfilerInfoMap); m_codeBlock->m_rareData->m_opProfileControlFlowBytecodeOffsets = WTF::move(m_opProfileControlFlowBytecodeOffsets); + m_codeBlock->m_rareData->m_callSitePositions = WTF::move(m_callSitePositions); + m_codeBlock->m_rareData->m_asyncResumePositions = WTF::move(m_asyncResumePositions); m_codeBlock->m_rareData->m_bitVectors = WTF::move(m_bitVectors); m_codeBlock->m_rareData->m_constantIdentifierSets = WTF::move(m_constantIdentifierSets); } @@ -138,6 +142,18 @@ void UnlinkedCodeBlockGenerator::applyModification(BytecodeRewriter& rewriter) m_typeProfilerInfoMap.swap(adjustedTypeProfilerInfoMap); } + m_callSitePositions.removeAllMatching([&](auto& position) { + auto offset = rewriter.adjustInstructionOffset(position.first); + if (!offset) + return true; + position.first = *offset; + return false; + }); + + // Async stack frames use the state switch's resume label, including injected restore instructions. + for (auto& position : m_asyncResumePositions) + position.first = rewriter.adjustAbsoluteOffset(position.first); + Vector bytecodeOffsetAdjustments; rewriter.forEachLabelPoint([&] (int32_t bytecodeOffset) { bytecodeOffsetAdjustments.append(bytecodeOffset); diff --git a/Source/JavaScriptCore/bytecode/UnlinkedCodeBlockGenerator.h b/Source/JavaScriptCore/bytecode/UnlinkedCodeBlockGenerator.h index 13515ff946fe4..a05b48d9c1c15 100644 --- a/Source/JavaScriptCore/bytecode/UnlinkedCodeBlockGenerator.h +++ b/Source/JavaScriptCore/bytecode/UnlinkedCodeBlockGenerator.h @@ -76,6 +76,16 @@ class UnlinkedCodeBlockGenerator { UnlinkedMetadataTable& metadata() { return m_codeBlock->metadata(); } void addExpressionInfo(unsigned instructionOffset, unsigned divot, unsigned startOffset, unsigned endOffset); + void addCallSitePosition(unsigned instructionOffset, unsigned sourceOffset) + { + ASSERT(m_callSitePositions.isEmpty() || m_callSitePositions.last().first < instructionOffset); + m_callSitePositions.append({ instructionOffset, sourceOffset }); + } + void addAsyncResumePosition(unsigned instructionOffset, unsigned sourceOffset) + { + ASSERT(m_asyncResumePositions.isEmpty() || m_asyncResumePositions.last().first < instructionOffset); + m_asyncResumePositions.append({ instructionOffset, sourceOffset }); + } void addTypeProfilerExpressionInfo(unsigned instructionOffset, unsigned startDivot, unsigned endDivot); void addOpProfileControlFlowBytecodeOffset(JSInstructionStream::Offset offset) { @@ -210,6 +220,8 @@ class UnlinkedCodeBlockGenerator { Vector m_unlinkedStringSwitchJumpTables; UncheckedKeyHashMap m_typeProfilerInfoMap; Vector m_opProfileControlFlowBytecodeOffsets; + Vector m_callSitePositions; + Vector m_asyncResumePositions; Vector m_bitVectors; Vector m_constantIdentifierSets; unsigned m_numBinaryArithProfiles { 0 }; diff --git a/Source/JavaScriptCore/bytecompiler/BytecodeGenerator.cpp b/Source/JavaScriptCore/bytecompiler/BytecodeGenerator.cpp index 6ffe83b8db654..bf1b54116836e 100644 --- a/Source/JavaScriptCore/bytecompiler/BytecodeGenerator.cpp +++ b/Source/JavaScriptCore/bytecompiler/BytecodeGenerator.cpp @@ -3838,30 +3838,30 @@ RegisterID* BytecodeGenerator::emitAsyncIteratorNext(RegisterID* dst, RegisterID return dst; } -RegisterID* BytecodeGenerator::emitCall(RegisterID* dst, RegisterID* func, ExpectedFunction expectedFunction, CallArguments& callArguments, const JSTextPosition& divot, const JSTextPosition& divotStart, const JSTextPosition& divotEnd, DebuggableCall debuggableCall) +RegisterID* BytecodeGenerator::emitCall(RegisterID* dst, RegisterID* func, ExpectedFunction expectedFunction, CallArguments& callArguments, const JSTextPosition& divot, const JSTextPosition& divotStart, const JSTextPosition& divotEnd, DebuggableCall debuggableCall, std::optional callSitePosition) { - return emitCall(dst, func, expectedFunction, callArguments, divot, divotStart, divotEnd, debuggableCall); + return emitCall(dst, func, expectedFunction, callArguments, divot, divotStart, divotEnd, debuggableCall, callSitePosition); } -void BytecodeGenerator::emitCallIgnoreResult(RegisterID* dst, RegisterID* func, ExpectedFunction expectedFunction, CallArguments& callArguments, const JSTextPosition& divot, const JSTextPosition& divotStart, const JSTextPosition& divotEnd, DebuggableCall debuggableCall) +void BytecodeGenerator::emitCallIgnoreResult(RegisterID* dst, RegisterID* func, ExpectedFunction expectedFunction, CallArguments& callArguments, const JSTextPosition& divot, const JSTextPosition& divotStart, const JSTextPosition& divotEnd, DebuggableCall debuggableCall, std::optional callSitePosition) { - emitCall(dst, func, expectedFunction, callArguments, divot, divotStart, divotEnd, debuggableCall); + emitCall(dst, func, expectedFunction, callArguments, divot, divotStart, divotEnd, debuggableCall, callSitePosition); } -RegisterID* BytecodeGenerator::emitCallInTailPosition(RegisterID* dst, RegisterID* func, ExpectedFunction expectedFunction, CallArguments& callArguments, const JSTextPosition& divot, const JSTextPosition& divotStart, const JSTextPosition& divotEnd, DebuggableCall debuggableCall) +RegisterID* BytecodeGenerator::emitCallInTailPosition(RegisterID* dst, RegisterID* func, ExpectedFunction expectedFunction, CallArguments& callArguments, const JSTextPosition& divot, const JSTextPosition& divotStart, const JSTextPosition& divotEnd, DebuggableCall debuggableCall, std::optional callSitePosition) { if (m_allowTailCallOptimization) { m_codeBlock->setHasTailCalls(); - return emitCall(dst, func, expectedFunction, callArguments, divot, divotStart, divotEnd, debuggableCall); + return emitCall(dst, func, expectedFunction, callArguments, divot, divotStart, divotEnd, debuggableCall, callSitePosition); } if (m_allowCallIgnoreResultOptimization) - return emitCall(dst, func, expectedFunction, callArguments, divot, divotStart, divotEnd, debuggableCall); - return emitCall(dst, func, expectedFunction, callArguments, divot, divotStart, divotEnd, debuggableCall); + return emitCall(dst, func, expectedFunction, callArguments, divot, divotStart, divotEnd, debuggableCall, callSitePosition); + return emitCall(dst, func, expectedFunction, callArguments, divot, divotStart, divotEnd, debuggableCall, callSitePosition); } -RegisterID* BytecodeGenerator::emitCallDirectEval(RegisterID* dst, RegisterID* func, CallArguments& callArguments, const JSTextPosition& divot, const JSTextPosition& divotStart, const JSTextPosition& divotEnd, DebuggableCall debuggableCall) +RegisterID* BytecodeGenerator::emitCallDirectEval(RegisterID* dst, RegisterID* func, CallArguments& callArguments, const JSTextPosition& divot, const JSTextPosition& divotStart, const JSTextPosition& divotEnd, DebuggableCall debuggableCall, std::optional callSitePosition) { - return emitCall(dst, func, NoExpectedFunction, callArguments, divot, divotStart, divotEnd, debuggableCall); + return emitCall(dst, func, NoExpectedFunction, callArguments, divot, divotStart, divotEnd, debuggableCall, callSitePosition); } ExpectedFunction BytecodeGenerator::expectedFunctionForIdentifier(const Identifier& identifier) @@ -3939,7 +3939,7 @@ LexicallyScopedFeatures BytecodeGenerator::computeFeaturesForCallDirectEval() } template -RegisterID* BytecodeGenerator::emitCall(RegisterID* dst, RegisterID* func, ExpectedFunction expectedFunction, CallArguments& callArguments, const JSTextPosition& divot, const JSTextPosition& divotStart, const JSTextPosition& divotEnd, DebuggableCall debuggableCall) +RegisterID* BytecodeGenerator::emitCall(RegisterID* dst, RegisterID* func, ExpectedFunction expectedFunction, CallArguments& callArguments, const JSTextPosition& divot, const JSTextPosition& divotStart, const JSTextPosition& divotEnd, DebuggableCall debuggableCall, std::optional callSitePosition) { constexpr auto opcodeID = CallOp::opcodeID; ASSERT(opcodeID == op_call || opcodeID == op_call_direct_eval || opcodeID == op_tail_call || opcodeID == op_call_ignore_result); @@ -3962,12 +3962,12 @@ RegisterID* BytecodeGenerator::emitCall(RegisterID* dst, RegisterID* func, Expec emitExpressionInfo(spread->divot(), spread->divotStart(), spread->divotEnd()); OpSpread::emit(this, argumentRegister.get(), argumentRegister.get()); - return emitCallVarargs::type>(dst, func, callArguments.thisRegister(), argumentRegister.get(), newTemporary(), 0, divot, divotStart, divotEnd, debuggableCall); + return emitCallVarargs::type>(dst, func, callArguments.thisRegister(), argumentRegister.get(), newTemporary(), 0, divot, divotStart, divotEnd, debuggableCall, callSitePosition); } } RefPtr argumentRegister; argumentRegister = expression->emitBytecode(*this, callArguments.argumentRegister(0)); - return emitCallVarargs::type>(dst, func, callArguments.thisRegister(), argumentRegister.get(), newTemporary(), 0, divot, divotStart, divotEnd, debuggableCall); + return emitCallVarargs::type>(dst, func, callArguments.thisRegister(), argumentRegister.get(), newTemporary(), 0, divot, divotStart, divotEnd, debuggableCall, callSitePosition); } for (; n; n = n->m_next) emitNode(callArguments.argumentRegister(argument++), n); @@ -3989,6 +3989,8 @@ RegisterID* BytecodeGenerator::emitCall(RegisterID* dst, RegisterID* func, Expec if (opcodeID == op_tail_call) emitLogShadowChickenTailIfNecessary(); + emitCallSitePosition(callSitePosition); + // Emit call. ASSERT(dst); ASSERT(dst != ignoredResult()); @@ -4009,21 +4011,21 @@ RegisterID* BytecodeGenerator::emitCall(RegisterID* dst, RegisterID* func, Expec return dst; } -RegisterID* BytecodeGenerator::emitCallVarargs(RegisterID* dst, RegisterID* func, RegisterID* thisRegister, RegisterID* arguments, RegisterID* firstFreeRegister, int32_t firstVarArgOffset, const JSTextPosition& divot, const JSTextPosition& divotStart, const JSTextPosition& divotEnd, DebuggableCall debuggableCall) +RegisterID* BytecodeGenerator::emitCallVarargs(RegisterID* dst, RegisterID* func, RegisterID* thisRegister, RegisterID* arguments, RegisterID* firstFreeRegister, int32_t firstVarArgOffset, const JSTextPosition& divot, const JSTextPosition& divotStart, const JSTextPosition& divotEnd, DebuggableCall debuggableCall, std::optional callSitePosition) { - return emitCallVarargs(dst, func, thisRegister, arguments, firstFreeRegister, firstVarArgOffset, divot, divotStart, divotEnd, debuggableCall); + return emitCallVarargs(dst, func, thisRegister, arguments, firstFreeRegister, firstVarArgOffset, divot, divotStart, divotEnd, debuggableCall, callSitePosition); } -RegisterID* BytecodeGenerator::emitCallVarargsInTailPosition(RegisterID* dst, RegisterID* func, RegisterID* thisRegister, RegisterID* arguments, RegisterID* firstFreeRegister, int32_t firstVarArgOffset, const JSTextPosition& divot, const JSTextPosition& divotStart, const JSTextPosition& divotEnd, DebuggableCall debuggableCall) +RegisterID* BytecodeGenerator::emitCallVarargsInTailPosition(RegisterID* dst, RegisterID* func, RegisterID* thisRegister, RegisterID* arguments, RegisterID* firstFreeRegister, int32_t firstVarArgOffset, const JSTextPosition& divot, const JSTextPosition& divotStart, const JSTextPosition& divotEnd, DebuggableCall debuggableCall, std::optional callSitePosition) { if (m_allowTailCallOptimization) - return emitCallVarargs(dst, func, thisRegister, arguments, firstFreeRegister, firstVarArgOffset, divot, divotStart, divotEnd, debuggableCall); - return emitCallVarargs(dst, func, thisRegister, arguments, firstFreeRegister, firstVarArgOffset, divot, divotStart, divotEnd, debuggableCall); + return emitCallVarargs(dst, func, thisRegister, arguments, firstFreeRegister, firstVarArgOffset, divot, divotStart, divotEnd, debuggableCall, callSitePosition); + return emitCallVarargs(dst, func, thisRegister, arguments, firstFreeRegister, firstVarArgOffset, divot, divotStart, divotEnd, debuggableCall, callSitePosition); } -RegisterID* BytecodeGenerator::emitConstructVarargs(RegisterID* dst, RegisterID* func, RegisterID* thisRegister, RegisterID* arguments, RegisterID* firstFreeRegister, int32_t firstVarArgOffset, const JSTextPosition& divot, const JSTextPosition& divotStart, const JSTextPosition& divotEnd, DebuggableCall debuggableCall) +RegisterID* BytecodeGenerator::emitConstructVarargs(RegisterID* dst, RegisterID* func, RegisterID* thisRegister, RegisterID* arguments, RegisterID* firstFreeRegister, int32_t firstVarArgOffset, const JSTextPosition& divot, const JSTextPosition& divotStart, const JSTextPosition& divotEnd, DebuggableCall debuggableCall, std::optional callSitePosition) { - return emitCallVarargs(dst, func, thisRegister, arguments, firstFreeRegister, firstVarArgOffset, divot, divotStart, divotEnd, debuggableCall); + return emitCallVarargs(dst, func, thisRegister, arguments, firstFreeRegister, firstVarArgOffset, divot, divotStart, divotEnd, debuggableCall, callSitePosition); } RegisterID* BytecodeGenerator::emitSuperConstructVarargs(RegisterID* dst, RegisterID* func, RegisterID* thisRegister, RegisterID* arguments, RegisterID* firstFreeRegister, int32_t firstVarArgOffset, const JSTextPosition& divot, const JSTextPosition& divotStart, const JSTextPosition& divotEnd, DebuggableCall debuggableCall) @@ -4032,7 +4034,7 @@ RegisterID* BytecodeGenerator::emitSuperConstructVarargs(RegisterID* dst, Regist } template -RegisterID* BytecodeGenerator::emitCallVarargs(RegisterID* dst, RegisterID* func, RegisterID* thisRegister, RegisterID* arguments, RegisterID* firstFreeRegister, int32_t firstVarArgOffset, const JSTextPosition& divot, const JSTextPosition& divotStart, const JSTextPosition& divotEnd, DebuggableCall debuggableCall) +RegisterID* BytecodeGenerator::emitCallVarargs(RegisterID* dst, RegisterID* func, RegisterID* thisRegister, RegisterID* arguments, RegisterID* firstFreeRegister, int32_t firstVarArgOffset, const JSTextPosition& divot, const JSTextPosition& divotStart, const JSTextPosition& divotEnd, DebuggableCall debuggableCall, std::optional callSitePosition) { if (shouldEmitDebugHooks() && debuggableCall == DebuggableCall::Yes) emitDebugHook(WillExecuteExpression, divotStart); @@ -4042,6 +4044,8 @@ RegisterID* BytecodeGenerator::emitCallVarargs(RegisterID* dst, RegisterID* func if (VarargsOp::opcodeID == op_tail_call_varargs) emitLogShadowChickenTailIfNecessary(); + emitCallSitePosition(callSitePosition); + // Emit call. ASSERT(dst != ignoredResult()); if constexpr (VarargsOp::opcodeID == op_tail_call_varargs) @@ -4155,7 +4159,7 @@ RegisterID* BytecodeGenerator::emitReturn(RegisterID* src) } template -RegisterID* BytecodeGenerator::emitConstructImpl(RegisterID* dst, RegisterID* func, RegisterID* lazyThis, ExpectedFunction expectedFunction, CallArguments& callArguments, const JSTextPosition& divot, const JSTextPosition& divotStart, const JSTextPosition& divotEnd, bool isDefaultDerivedConstructorCall) +RegisterID* BytecodeGenerator::emitConstructImpl(RegisterID* dst, RegisterID* func, RegisterID* lazyThis, ExpectedFunction expectedFunction, CallArguments& callArguments, const JSTextPosition& divot, const JSTextPosition& divotStart, const JSTextPosition& divotEnd, bool isDefaultDerivedConstructorCall, std::optional callSitePosition) { ASSERT(func->refCount()); @@ -4180,13 +4184,13 @@ RegisterID* BytecodeGenerator::emitConstructImpl(RegisterID* dst, RegisterID* fu } move(callArguments.thisRegister(), lazyThis); - return emitCallVarargs::type>(dst, func, callArguments.thisRegister(), argumentRegister.get(), newTemporary(), 0, divot, divotStart, divotEnd, DebuggableCall::No); + return emitCallVarargs::type>(dst, func, callArguments.thisRegister(), argumentRegister.get(), newTemporary(), 0, divot, divotStart, divotEnd, DebuggableCall::No, callSitePosition); } } RefPtr argumentRegister; argumentRegister = expression->emitBytecode(*this, callArguments.argumentRegister(0)); move(callArguments.thisRegister(), lazyThis); - return emitCallVarargs::type>(dst, func, callArguments.thisRegister(), argumentRegister.get(), newTemporary(), 0, divot, divotStart, divotEnd, DebuggableCall::No); + return emitCallVarargs::type>(dst, func, callArguments.thisRegister(), argumentRegister.get(), newTemporary(), 0, divot, divotStart, divotEnd, DebuggableCall::No, callSitePosition); } for (ArgumentListNode* n = argumentsNode->m_listNode; n; n = n->m_next) @@ -4205,6 +4209,7 @@ RegisterID* BytecodeGenerator::emitConstructImpl(RegisterID* dst, RegisterID* fu Ref