Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
4451d0d
[JSC] Bytecode cache: encode the modules of a link into one payload l…
Jarred-Sumner Sep 22, 2026
d3fbdd4
[JSC] Bytecode cache: record what a run reads out of its persistent p…
Jarred-Sumner Sep 22, 2026
d9636b2
[JSC] Bytecode cache: digestOfAllCachedCode, for checking one payload…
Jarred-Sumner Sep 22, 2026
611b83f
[JSC] Bytecode cache: the link encoder puts functions an order file's…
Jarred-Sumner Sep 22, 2026
15e9a39
[JSC] Bytecode cache: the link encoder writes expression info in the …
Jarred-Sumner Sep 22, 2026
9540056
[JSC] Bytecode cache: an order recording covers every VM of the process
Jarred-Sumner Sep 22, 2026
55fe005
[JSC] Bytecode cache: an embedder's builtin functions can be modules …
Jarred-Sumner Sep 22, 2026
64e1935
[JSC] Bytecode cache: the order hash looks a reserved word up among t…
Jarred-Sumner Sep 22, 2026
d53caab
[JSC] Bytecode cache: the link encoder roots function code instead of…
Jarred-Sumner Sep 22, 2026
f93888a
[JSC] Bytecode cache: count the function bodies decoded out of a link…
Jarred-Sumner Sep 22, 2026
10bd0e3
[JSC] Bytecode cache: to the order hash only words that are never a b…
Jarred-Sumner Sep 22, 2026
9a6139a
[JSC] Bytecode cache: an order file names code by its own text, neste…
Jarred-Sumner Sep 22, 2026
6a9f789
[JSC] Bytecode cache: an order file names code by its own text and th…
Jarred-Sumner Sep 22, 2026
26f15fd
[JSC] Bytecode cache: naming an order file's code shares nothing with…
Jarred-Sumner Sep 23, 2026
d9658ce
[JSC] Bytecode cache: an order file expects names only of payloads th…
Jarred-Sumner Sep 23, 2026
84c0ab1
[JSC] ModuleProgramExecutable::tryCreate checks the exception getUnli…
Jarred-Sumner Sep 23, 2026
7c3ac6a
[JSC] Bytecode cache: a link and a recording keep unlinked code; reco…
Jarred-Sumner Sep 23, 2026
63ad314
Merge remote-tracking branch 'origin/main' into claude/bytecode-link-…
Jarred-Sumner Sep 23, 2026
d410ec1
[JSC] Bytecode order: the embedder names code, JSC reports what it de…
Jarred-Sumner Sep 23, 2026
f74048b
[JSC] Bytecode order: a VM stops keeping unlinked code when its recor…
Jarred-Sumner Sep 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion Source/JavaScriptCore/bytecode/UnlinkedFunctionExecutable.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -261,8 +261,24 @@ UnlinkedFunctionCodeBlock* UnlinkedFunctionExecutable::unlinkedCodeBlockFor(
VM& vm, const SourceCode& source, CodeSpecializationKind specializationKind,
OptionSet<CodeGenerationMode> codeGenerationMode, ParserError& error, SourceParseMode parseMode, OptimizeBytecode optimize)
{
if (m_isCached)
if (m_isCached) {
#if USE(BUN_JSC_ADDITIONS)
// Code of a payload that outlives the program, about to be run: what a payload order file is about. (Not what
// codeBlocksDecodingCached decodes, for a link; the callers that generate code without running it, CodeCache's and
// the shell's, have executables that were just parsed.)
auto* recorder = BytecodeOrderRecorder::ofVM(vm);
std::optional<RecordedOrderSource> recordedSource;
if (recorder && m_decoder->canBorrowPayload()) [[unlikely]]
recordedSource = m_decoder->orderSource();
#endif
decodeCachedCodeBlocks(vm);
#if USE(BUN_JSC_ADDITIONS)
if (recordedSource) [[unlikely]] {
if (auto key = orderFunctionKey(*this, source))
recorder->didDecodeFunction(*recordedSource, *key);
}
#endif
}
switch (specializationKind) {
case CodeSpecializationKind::CodeForCall:
if (UnlinkedFunctionCodeBlock* codeBlock = m_unlinkedCodeBlockForCall.get())
Expand Down Expand Up @@ -295,6 +311,15 @@ UnlinkedFunctionCodeBlock* UnlinkedFunctionExecutable::unlinkedCodeBlockFor(
return result;
}

#if USE(BUN_JSC_ADDITIONS)
std::pair<UnlinkedFunctionCodeBlock*, UnlinkedFunctionCodeBlock*> UnlinkedFunctionExecutable::codeBlocksDecodingCached(VM& vm)
{
if (m_isCached)
decodeCachedCodeBlocks(vm);
return { m_unlinkedCodeBlockForCall.get(), m_unlinkedCodeBlockForConstruct.get() };
}
#endif

void UnlinkedFunctionExecutable::decodeCachedCodeBlocks(VM& vm)
{
ASSERT(m_isCached);
Expand Down
11 changes: 11 additions & 0 deletions Source/JavaScriptCore/bytecode/UnlinkedFunctionExecutable.h
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,11 @@ class UnlinkedFunctionExecutable final : public JSCell {
bool isBuiltinFunction() const { return m_isBuiltinFunction; }
// The code blocks are still (or again) in the bytecode cache this was decoded from.
bool isCached() const { return m_isCached; }
#if USE(BUN_JSC_ADDITIONS)
// The code blocks for call and construct as they are once whatever the bytecode cache holds for this function is
// decoded; never generates one (unlinkedCodeBlockFor does).
JS_EXPORT_PRIVATE std::pair<UnlinkedFunctionCodeBlock*, UnlinkedFunctionCodeBlock*> codeBlocksDecodingCached(VM&);
#endif
ConstructAbility constructAbility() const { return static_cast<ConstructAbility>(m_constructAbility); }
JSParserScriptMode scriptMode() const { return static_cast<JSParserScriptMode>(m_scriptMode); }
bool isClassConstructorFunction() const
Expand All @@ -229,6 +234,12 @@ class UnlinkedFunctionExecutable final : public JSCell {
}
bool isClass() const { return m_isClass; }
bool isBuiltinDefaultClassConstructor() const { return m_isBuiltinDefaultClassConstructor; }
#if USE(BUN_JSC_ADDITIONS)
// Where classSource() starts and where the first of classElementDefinitions() is, for orderFunctionKey: read out
// of the cached record as they lie, so that nothing of a function that was decoded from a cache is materialized.
std::optional<uint32_t> classSourceStartWithoutMaterializing() const;
std::optional<uint32_t> firstClassElementOffsetWithoutMaterializing() const;
#endif

RefPtr<TDZEnvironmentLink> parentScopeTDZVariables() const
{
Expand Down
2 changes: 1 addition & 1 deletion Source/JavaScriptCore/debugger/Debugger.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -562,7 +562,7 @@ void Debugger::toggleBreakpoint(Breakpoint& breakpoint, Debugger::BreakpointStat

void Debugger::recompileAllJSFunctions()
{
m_vm.deleteAllCode(PreventCollectionAndDeleteAllCode);
m_vm.deleteAllCodeToGenerateItAgain(PreventCollectionAndDeleteAllCode);
}

DebuggerParseData& Debugger::debuggerParseData(SourceID sourceID, SourceProvider* provider)
Expand Down
3 changes: 3 additions & 0 deletions Source/JavaScriptCore/heap/Heap.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1205,6 +1205,9 @@ void Heap::deleteAllUnlinkedCodeBlocks(DeleteAllCodeEffort effort, OptionSet<Unl
return;

VM& vm = this->vm();
// Shared Baseline code, below, still goes.
if (vm.keepsUnlinkedCode()) [[unlikely]]
which.remove({ UnlinkedCodeToDelete::Generated, UnlinkedCodeToDelete::RecoverableFromCache });
PreventCollectionScope preventCollectionScope(*this);

RELEASE_ASSERT(!m_collectionScope);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,7 @@ void InspectorRuntimeAgent::setTypeProfilerEnabledState(bool isTypeProfilingEnab
vm.whenIdle([&vm, isTypeProfilingEnabled] () {
bool shouldRecompileFromTypeProfiler = (isTypeProfilingEnabled ? vm.enableTypeProfiler() : vm.disableTypeProfiler());
if (shouldRecompileFromTypeProfiler)
vm.deleteAllCode(PreventCollectionAndDeleteAllCode);
vm.deleteAllCodeToGenerateItAgain(PreventCollectionAndDeleteAllCode);
});
}

Expand All @@ -523,7 +523,7 @@ void InspectorRuntimeAgent::setControlFlowProfilerEnabledState(bool isControlFlo
bool shouldRecompileFromControlFlowProfiler = (isControlFlowProfilingEnabled ? vm.enableControlFlowProfiler() : vm.disableControlFlowProfiler());

if (shouldRecompileFromControlFlowProfiler)
vm.deleteAllCode(PreventCollectionAndDeleteAllCode);
vm.deleteAllCodeToGenerateItAgain(PreventCollectionAndDeleteAllCode);
});
}

Expand Down
137 changes: 137 additions & 0 deletions Source/JavaScriptCore/runtime/CachedBytecode.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
#include "UnlinkedCodeBlock.h"
#include "UnlinkedFunctionExecutable.h"
#include "WeakInlines.h"
#include <wtf/NeverDestroyed.h>
#include <wtf/TZoneMallocInlines.h>

WTF_ALLOW_UNSAFE_BUFFER_USAGE_BEGIN
Expand All @@ -40,6 +41,142 @@ namespace JSC {

WTF_MAKE_TZONE_ALLOCATED_IMPL(PersistentBytecodePayloads);

#if USE(BUN_JSC_ADDITIONS)
WTF_MAKE_TZONE_ALLOCATED_IMPL(BytecodeOrderRecorder);

BytecodeOrderRecorder::BytecodeOrderRecorder(bool isOver)
: m_isOver(isOver)
{
}
BytecodeOrderRecorder::~BytecodeOrderRecorder() = default;

static Lock s_orderRecordersLock;
static bool s_orderRecordingIsOver WTF_GUARDED_BY_LOCK(s_orderRecordersLock) { false };
static Vector<Ref<BytecodeOrderRecorder>>& orderRecorders() WTF_REQUIRES_LOCK(s_orderRecordersLock)
{
static NeverDestroyed<Vector<Ref<BytecodeOrderRecorder>>> recorders;
return recorders;
}

Ref<BytecodeOrderRecorder> BytecodeOrderRecorder::create()
{
Locker locker { s_orderRecordersLock };
Ref recorder = adoptRef(*new BytecodeOrderRecorder(s_orderRecordingIsOver));
orderRecorders().append(recorder);
return recorder;
}

Vector<Ref<BytecodeOrderRecorder>> BytecodeOrderRecorder::endRecordingInProcess()
{
Locker locker { s_orderRecordersLock };
s_orderRecordingIsOver = true;
return orderRecorders();
}

BytecodeOrderRecorder* BytecodeOrderRecorder::ofVM(VM& vm)
{
auto* payloads = vm.persistentBytecodePayloadsIfExists();
return payloads ? payloads->orderRecorderIfRecording() : nullptr;
}

unsigned BytecodeOrderRecorder::indexOf(RecordedOrderSource source)
{
// Most of what is decoded comes out of the source the last thing came out of.
if (m_lastSource < m_recorded.sources.size() && m_recorded.sources[m_lastSource] == source)
return m_lastSource;
auto result = m_sources.add({ source.payload, source.entryOffset }, m_recorded.sources.size());
if (result.isNewEntry)
m_recorded.sources.append(source);
m_lastSource = result.iterator->value;
return m_lastSource;
}

void BytecodeOrderRecorder::didDecodeFunction(RecordedOrderSource source, OrderFunctionKey key)
{
if (isOver())
return;
Locker locker { m_lock };
if (m_isOver)
return;
unsigned index = indexOf(source);
// Two bits of kind, 32 of start, 30 of source; the top two values are the table's own.
RELEASE_ASSERT(index < (1u << 30) - 1);
if (m_seenFunctions.add(static_cast<uint64_t>(index) << 34 | static_cast<uint64_t>(key.start) << 2 | static_cast<uint64_t>(key.kind)).isNewEntry)
m_recorded.functions.append({ index, key });
}

void BytecodeOrderRecorder::didDecodeModule(RecordedOrderSource source)
{
if (isOver())
return;
Locker locker { m_lock };
if (m_isOver)
return;
unsigned index = indexOf(source);
if (!m_seenModules.set(index))
m_recorded.modules.append(index);
}

void BytecodeOrderRecorder::didRejectModule(RecordedOrderSource source)
{
if (isOver())
return;
Locker locker { m_lock };
if (m_isOver)
return;
unsigned index = indexOf(source);
if (!m_seenRejectedModules.set(index))
m_recorded.rejectedModules.append(index);
}

void BytecodeOrderRecorder::didReadString(std::span<const uint8_t> stringTable, uint32_t ordinal)
{
if (isOver())
return;
Locker locker { m_lock };
if (m_isOver || m_seenStrings.set(ordinal))
return;
// Ordinals mean something in one table only: a VM has one for as long as it records.
RELEASE_ASSERT(m_recorded.stringOrdinals.isEmpty() || m_recorded.stringTable.data() == stringTable.data());
m_recorded.stringTable = stringTable;
m_recorded.stringOrdinals.append(ordinal);
}

auto BytecodeOrderRecorder::take() -> Snapshot
{
Locker locker { m_lock };
m_isOver.store(true);
m_sources.clear();
m_seenFunctions.clear();
return std::exchange(m_recorded, { });
}

void PersistentBytecodePayloads::setLinkedPayload(std::span<const uint8_t> payload, const std::array<uint32_t, BytecodeLinkRegions::Count>& regionEnds)
{
m_linkedPayloadBase = payload.data();
m_linkedPayloadStatistics = { };
m_linkedPayloadStatistics.regionEnds = regionEnds;
}

BytecodeOrderRecorder& PersistentBytecodePayloads::enableOrderRecording()
{
if (!m_orderRecorder) {
m_orderRecorder = BytecodeOrderRecorder::create();
// Each function's code is decoded once: a recording is the same whenever the collector runs.
m_vm.keepUnlinkedCodeUntil(m_orderRecorder->isOverFlag());
}
return *m_orderRecorder;
}
Comment thread
Jarred-Sumner marked this conversation as resolved.

BytecodeOrderRecorder* PersistentBytecodePayloads::orderRecorderIfRecording()
{
// (The process keeps every recorder for good: VM::keepUnlinkedCodeUntil.)
if (m_orderRecorder && m_orderRecorder->isOver()) [[unlikely]]
m_orderRecorder = nullptr;
return m_orderRecorder.get();
}
#endif

PersistentBytecodePayloads::PersistentBytecodePayloads(VM& vm)
: m_vm(vm)
{
Expand Down
Loading
Loading