From 9f0ea1adf18736fb2bd06cc3a2ad5dee68f68480 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 25 Sep 2026 04:29:54 +0000 Subject: [PATCH 1/3] [JSC] Keep the divot of an instruction that a stack trace asked for Upstream c76c52f5b1 removed ExpressionInfo::m_cachedLineColumns together with the line and column fields. CodeBlock::lineColumnForBytecodeIndex() then decodes the expression info from the start of the chapter on every call. For one frame that is 1,051 instructions where it was 78, and 30 us for a frame late in a function of 1,000 statements. ExpressionInfo::divotForInstPC() keeps the divot of each instruction it was asked for, as lineColumnForInstPC() kept the line and column. --- ...tack-position-is-the-same-on-every-read.js | 27 +++++++++++++++++++ Source/JavaScriptCore/bytecode/CodeBlock.cpp | 4 +++ .../bytecode/ExpressionInfo.cpp | 13 +++++++++ .../JavaScriptCore/bytecode/ExpressionInfo.h | 10 +++++++ .../bytecode/UnlinkedCodeBlock.cpp | 7 +++++ .../bytecode/UnlinkedCodeBlock.h | 5 +++- 6 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 JSTests/stress/stack-position-is-the-same-on-every-read.js diff --git a/JSTests/stress/stack-position-is-the-same-on-every-read.js b/JSTests/stress/stack-position-is-the-same-on-every-read.js new file mode 100644 index 0000000000000..3bdb909e97948 --- /dev/null +++ b/JSTests/stress/stack-position-is-the-same-on-every-read.js @@ -0,0 +1,27 @@ +function shouldBe(actual, expected) { + if (actual !== expected) + throw new Error(`expected ${JSON.stringify(expected)} but got ${JSON.stringify(actual)}`); +} + +function makeStack(which) { + if (which === 0) return new Error("a").stack; + if (which === 1) + return new Error("b").stack; + const error = ( + new Error("c") + ); + return error.stack; +} + +function positionOfFirstFrame(stack) { + const match = /^makeStack@.*:(\d+):(\d+)$/m.exec(stack); + return [Number(match[1]), Number(match[2])]; +} + +const expected = [[7, 38], [9, 25], [11, 18]]; +for (let which = 0; which < 3; which++) { + const first = makeStack(which); + shouldBe(JSON.stringify(positionOfFirstFrame(first)), JSON.stringify(expected[which])); + for (let i = 0; i < 100; i++) + shouldBe(makeStack(which), first); +} diff --git a/Source/JavaScriptCore/bytecode/CodeBlock.cpp b/Source/JavaScriptCore/bytecode/CodeBlock.cpp index 784fea3c81fbc..21dc1bb1d8ee2 100644 --- a/Source/JavaScriptCore/bytecode/CodeBlock.cpp +++ b/Source/JavaScriptCore/bytecode/CodeBlock.cpp @@ -2576,8 +2576,12 @@ void CodeBlock::removeExceptionHandlerForCallSite(DisposableCallSiteIndex callSi LineColumn CodeBlock::lineColumnForBytecodeIndex(BytecodeIndex bytecodeIndex) const { RELEASE_ASSERT(bytecodeIndex.offset() < instructions().size()); +#if USE(BUN_JSC_ADDITIONS) + unsigned divotInProvider = sourceOffset() + m_unlinkedCode->divotForBytecodeIndex(bytecodeIndex); +#else auto entry = m_unlinkedCode->expressionInfoForBytecodeIndex(bytecodeIndex); unsigned divotInProvider = sourceOffset() + entry.divot; +#endif return source().provider()->documentLineColumnForOffset(divotInProvider); } diff --git a/Source/JavaScriptCore/bytecode/ExpressionInfo.cpp b/Source/JavaScriptCore/bytecode/ExpressionInfo.cpp index 65522c791d885..b8448573ce7fc 100644 --- a/Source/JavaScriptCore/bytecode/ExpressionInfo.cpp +++ b/Source/JavaScriptCore/bytecode/ExpressionInfo.cpp @@ -890,6 +890,19 @@ auto ExpressionInfo::entryForInstPC(InstPC instPC) -> Entry return decoder.entry(); } +#if USE(BUN_JSC_ADDITIONS) +unsigned ExpressionInfo::divotForInstPC(InstPC instPC) +{ + auto iter = m_cachedDivots.find(instPC); + if (iter != m_cachedDivots.end()) + return iter->value; + + unsigned divot = entryForInstPC(instPC).divot; + m_cachedDivots.add(instPC, divot); + return divot; +} +#endif + template void ExpressionInfo::print(PrintStream& out, FieldID fieldID, unsigned value) { diff --git a/Source/JavaScriptCore/bytecode/ExpressionInfo.h b/Source/JavaScriptCore/bytecode/ExpressionInfo.h index d839a8a320dcb..ccbc48f29c030 100644 --- a/Source/JavaScriptCore/bytecode/ExpressionInfo.h +++ b/Source/JavaScriptCore/bytecode/ExpressionInfo.h @@ -183,6 +183,12 @@ class ExpressionInfo { Entry NODELETE entryForInstPC(InstPC); +#if USE(BUN_JSC_ADDITIONS) + // entryForInstPC() decodes from the start of the chapter on every call, and a stack trace + // asks for the same instructions again. This keeps the divot of each instruction it was asked for. + unsigned divotForInstPC(InstPC); +#endif + bool isEmpty() const { return !m_numberOfEncodedInfo; }; size_t NODELETE byteSize() const; // owned by this object size_t NODELETE byteSizeForGCPacing() const; // what a generated (non-borrowed) one this size would own @@ -312,7 +318,11 @@ class ExpressionInfo { static constexpr unsigned numberOfWordsBetweenChapters = 10000; +#if USE(BUN_JSC_ADDITIONS) + using DivotMap = UncheckedKeyHashMap, WTF::UnsignedWithZeroKeyHashTraits>; + DivotMap m_cachedDivots; +#endif unsigned m_numberOfChapters; unsigned m_numberOfEncodedInfo; unsigned m_numberOfEncodedInfoExtensions; diff --git a/Source/JavaScriptCore/bytecode/UnlinkedCodeBlock.cpp b/Source/JavaScriptCore/bytecode/UnlinkedCodeBlock.cpp index fcb9469ab93d0..a266c33008743 100644 --- a/Source/JavaScriptCore/bytecode/UnlinkedCodeBlock.cpp +++ b/Source/JavaScriptCore/bytecode/UnlinkedCodeBlock.cpp @@ -187,6 +187,13 @@ ExpressionInfo::Entry UnlinkedCodeBlock::expressionInfoForBytecodeIndex(Bytecode return expressionInfo().entryForInstPC(bytecodeIndex.offset()); } +#if USE(BUN_JSC_ADDITIONS) +unsigned UnlinkedCodeBlock::divotForBytecodeIndex(BytecodeIndex bytecodeIndex) +{ + return expressionInfo().divotForInstPC(bytecodeIndex.offset()); +} +#endif + #ifndef NDEBUG static void dumpExpressionInfoDetails(size_t index, const JSInstructionStream& instructionStream, unsigned instructionOffset, unsigned divot, unsigned startOffset, unsigned endOffset) { diff --git a/Source/JavaScriptCore/bytecode/UnlinkedCodeBlock.h b/Source/JavaScriptCore/bytecode/UnlinkedCodeBlock.h index 384757482bcc6..206201308fe62 100644 --- a/Source/JavaScriptCore/bytecode/UnlinkedCodeBlock.h +++ b/Source/JavaScriptCore/bytecode/UnlinkedCodeBlock.h @@ -183,7 +183,7 @@ class UnlinkedCodeBlock : public JSCell { bool hasExpressionInfo() { return !expressionInfo().isEmpty(); } // Null while the expression info is still in the cache payload. For a caller that cannot take m_lock or allocate // (a sampling hook inside malloc): unlike expressionInfo() it never decodes. On the result use entryForInstPC(), - // which does neither; lineColumnForInstPC() fills a cache. + // which does neither; divotForInstPC() fills a cache. ExpressionInfo* expressionInfoIfDecoded() const { return m_expressionInfo.get(); } bool hasCheckpoints() const { return m_hasCheckpoints; } @@ -291,6 +291,9 @@ class UnlinkedCodeBlock : public JSCell { bool hasRareData() const { return m_rareData.get(); } ExpressionInfo::Entry expressionInfoForBytecodeIndex(BytecodeIndex); +#if USE(BUN_JSC_ADDITIONS) + unsigned divotForBytecodeIndex(BytecodeIndex); +#endif bool typeProfilerExpressionInfoForBytecodeOffset(unsigned bytecodeOffset, unsigned& startDivot, unsigned& endDivot); From 9589701aa46b8da0000f5db1b66d6c048406b6fd Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 25 Sep 2026 04:59:09 +0000 Subject: [PATCH 2/3] Compare the position of the frame in the new test, not the whole stack The calls to makeStack() are on different lines, so the frames of the caller differ between two stacks. --- .../stack-position-is-the-same-on-every-read.js | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/JSTests/stress/stack-position-is-the-same-on-every-read.js b/JSTests/stress/stack-position-is-the-same-on-every-read.js index 3bdb909e97948..17d38b3ce7049 100644 --- a/JSTests/stress/stack-position-is-the-same-on-every-read.js +++ b/JSTests/stress/stack-position-is-the-same-on-every-read.js @@ -1,6 +1,6 @@ function shouldBe(actual, expected) { if (actual !== expected) - throw new Error(`expected ${JSON.stringify(expected)} but got ${JSON.stringify(actual)}`); + throw new Error(`expected ${expected} but got ${actual}`); } function makeStack(which) { @@ -15,13 +15,11 @@ function makeStack(which) { function positionOfFirstFrame(stack) { const match = /^makeStack@.*:(\d+):(\d+)$/m.exec(stack); - return [Number(match[1]), Number(match[2])]; + return `${match[1]}:${match[2]}`; } -const expected = [[7, 38], [9, 25], [11, 18]]; -for (let which = 0; which < 3; which++) { - const first = makeStack(which); - shouldBe(JSON.stringify(positionOfFirstFrame(first)), JSON.stringify(expected[which])); - for (let i = 0; i < 100; i++) - shouldBe(makeStack(which), first); +const expected = ["7:38", "9:25", "11:18"]; +for (let i = 0; i < 100; i++) { + for (let which = 0; which < 3; which++) + shouldBe(positionOfFirstFrame(makeStack(which)), expected[which]); } From f44d2ea8dae91b78786c9e746697b64c2a249a73 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 25 Sep 2026 05:33:21 +0000 Subject: [PATCH 3/3] [JSC] Let a source provider find the line and column of an offset SourceProvider::lineAndColumnForOffset() is a virtual that the two documentLineColumnForOffset functions ask first. The default returns false, and the line start table answers as before. Bun's provider overrides it, so that the first position in a source does not scan the whole source and does not keep one entry per line. --- Source/JavaScriptCore/parser/SourceProvider.h | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/Source/JavaScriptCore/parser/SourceProvider.h b/Source/JavaScriptCore/parser/SourceProvider.h index 8e3305dadb56e..d7a2e805ce4f8 100644 --- a/Source/JavaScriptCore/parser/SourceProvider.h +++ b/Source/JavaScriptCore/parser/SourceProvider.h @@ -124,6 +124,9 @@ class JS_EXPORT_PRIVATE SourceProvider : public ThreadSafeRefCounted shifts every line of its document, but shifts the column only on its first // line, since later lines begin where their own line begins. LineColumn documentLineColumnForOffset(unsigned offset) { - auto info = positionInfoForOffset(offset); + auto info = lineAndColumnInfoForOffset(offset); return { m_startPosition.m_line.oneBasedInt() + info.line0Based, info.line0Based ? info.column0Based + 1 : m_startPosition.m_column.oneBasedInt() + info.column0Based, @@ -202,7 +218,7 @@ class JS_EXPORT_PRIVATE SourceProvider : public ThreadSafeRefCounted