Skip to content

FTL: store OSRExitDescriptor values as a compact byte stream - #349

Open
robobun wants to merge 1 commit into
mainfrom
farm/f7f38f94/ftl-compact-osr-exit-descriptors
Open

FTL: store OSRExitDescriptor values as a compact byte stream#349
robobun wants to merge 1 commit into
mainfrom
farm/f7f38f94/ftl-compact-osr-exit-descriptors

Conversation

@robobun

@robobun robobun commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator

Third instalment of the optimizing-tier metadata compaction, following #334 (DFG VariableEventStream) and #335 (DFG OSREntryData::m_expectedValues).

What

Each FTL OSRExitDescriptor currently holds a dense FixedOperands<ExitValue>: one 9-byte ExitValue per frame operand (arguments + locals + tmps of the whole inlined frame). Most entries are ExitValueDead and the remainder are small (a virtual register, a stackmap argument index, a constant, or a materialization pointer), so a large inlined function with many exits multiplies a mostly-dead table across hundreds of descriptors. On JS-heavy workloads this was the single largest piece of resident FTL metadata.

This replaces m_values with a compact byte stream held in a FixedVector<uint8_t> plus a small FixedVector<ExitTimeObjectMaterialization*> side table:

  • one tag byte per entry: ExitValueKind in the low 4 bits, a short Dead run length in the high 4 bits (overflow continues as an unsigned LEB varint);
  • ExitValueInJSStack*: zig-zag varint of the virtual-register offset;
  • ExitValueArgument: one DataFormat byte + varint argument index;
  • ExitValueConstant: 8 raw EncodedJSValue bytes;
  • ExitValueMaterializeNewObject: varint index into the side table.

The descriptor keeps m_numberOfArguments / m_numberOfLocals / m_numberOfTmps so size() / numberOfLocals() / tmpIndex() remain derivable. decodeValues() reconstructs the dense Operands<ExitValue> on demand.

Producers and consumers

  • LowerDFGToB3::buildExitArguments fills a local Operands<ExitValue> and calls setValues() once.
  • FTLOSRExitCompiler::compileStub decodes once at the top and uses the dense copy exactly as before, so exit-thunk codegen is untouched.
  • OSRExitDescriptor::validateReferences and the verboseOSR / FINALIZE_CODE_IF dump paths decode on demand.
  • FTLCompile.cpp no longer rewrites every value with withLocalsOffset(localsOffset); instead setLocalsOffset(localsOffset) stores the offset and decodeValues() applies it lazily to InJSStack* values whose virtual register is a local, which is exactly what ExitValue::withLocalsOffset did. ExitTimeObjectMaterialization::accountForLocalsOffset() is still applied eagerly in the same loop.

A debug-only round-trip check in setValues() asserts encode/decode equivalence for every ExitValueKind.

USE(JSVALUE32_64) builds are unaffected: all of this is inside #if ENABLE(FTL_JIT), which PlatformEnable.h forces off on 32-bit.

Byte totals

Process-lifetime totals for OSRExitDescriptor value storage, counted at setValues() time (release x64 jsc, 4 runs each):

workload descriptors operands dense FixedOperands<ExitValue> encoded
JetStream3 --tag=octane ~38.1 k ~2.27 M 20.5 MB 1.80 MB
JetStream3 --tag=typescript ~30.8 k ~2.18 M 19.6 MB 1.77 MB

Roughly an 11× reduction. JetStream3 Octane score is within noise of baseline on a non-isolated host (baseline 212-338 pts, patched 304-328 pts over 4 runs each).

Tests

368 FTL/OSR-focused JSTests/stress files (ftl-*, *osr*, *material*, *sink*, *checkpoint*, *phantom*) under two configurations (eager tier-up: --useConcurrentJIT=0 --thresholdForOptimizeAfterWarmUp=10 --thresholdForFTLOptimizeAfterWarmUp=20, and default thresholds):

debug + ASAN release
baseline 680 pass / 30 fail 694 pass / 16 fail
patched 681 pass / 29 fail 693 pass / 17 fail

Failure sets are identical modulo one timeout/flake each way; the one release discrepancy (check-is-constant-non-cell-should-not-array-profile-during-osr-exit.js under the eager config) reproduces 5/5 on both binaries when re-run in isolation. The round-trip assertion held across every descriptor encoded in the debug runs.

Each FTL OSRExitDescriptor held a dense FixedOperands<ExitValue> with
one 9-byte ExitValue per frame operand (arguments + locals + tmps of
the whole inlined frame). Most of those entries are ExitValueDead and
the rest are small (a stack slot, a stackmap argument index, a
constant), so a large inlined function with many exits multiplies a
mostly-dead table across hundreds of descriptors.

Replace m_values with a compact byte encoding held in a
FixedVector<uint8_t>: a tag byte per entry carrying the ExitValueKind
in the low four bits and a short Dead run length in the high four
(overflow continues as a varint), a zig-zag varint virtual register
for InJSStack* kinds, a DataFormat byte plus varint argument index for
ExitValueArgument, eight raw bytes for ExitValueConstant, and a varint
index into a small FixedVector<ExitTimeObjectMaterialization*> side
table for ExitValueMaterializeNewObject. The descriptor keeps the
operand counts so size()/numberOfLocals()/tmpIndex() remain
derivable, and decodeValues() reconstructs the dense
Operands<ExitValue> on demand.

buildExitArguments now fills a local dense array and calls setValues()
once; compileStub decodes once at the top and uses that copy
unchanged; validateReferences and the dump paths decode on demand. The
FTLCompile.cpp loop that eagerly rewrote every value with
withLocalsOffset(localsOffset) becomes a stored m_localsOffset applied
during decode (InJSStack* values whose virtual register is a local),
which is exactly what ExitValue::withLocalsOffset did.
ExitTimeObjectMaterialization::accountForLocalsOffset() is still
applied eagerly. A debug-only round-trip check in setValues() asserts
encode/decode equivalence for every kind.
@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Changes

OSR exit value encoding

Layer / File(s) Summary
Descriptor encoding and decoding
Source/JavaScriptCore/ftl/FTLOSRExit.h, Source/JavaScriptCore/ftl/FTLOSRExit.cpp
OSRExitDescriptor stores compact encoded values, materialization references, operand counts, and locals-offset metadata, then reconstructs values on demand.
Lowering and locals-offset integration
Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp, Source/JavaScriptCore/ftl/FTLCompile.cpp
Lowering populates a local operand container before calling setValues, and compilation applies locals offsets through the descriptor.
Decoded values in OSR exit compilation
Source/JavaScriptCore/ftl/FTLOSRExitCompiler.cpp
OSR exit compilation uses decoded values for buffer sizing, value handling, restoration, materialization, and diagnostics.

Suggested reviewers: constellation, geoffreygaren

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description explains the change well but does not follow the required template or include the Bugzilla link, review line, or changed-file list. Add the commit-message template sections: bug title, Bugzilla URL, Reviewed by line, explanation, and a bullet list of changed paths.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change to compactly store OSRExitDescriptor values.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Source/JavaScriptCore/ftl/FTLOSRExit.cpp`:
- Around line 138-142: Update the ExitValueMaterializeNewObject handling to
deduplicate objectMaterialization() pointers before appending to
materializations: reuse the existing index for a previously seen pointer and
append/store a new index only for unique pointers. Add or use a pointer-to-index
map in the surrounding encoding routine while preserving the encoded index
semantics for all materialization references.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 21f23dfc-b587-445f-bab8-1ebb8393b73c

📥 Commits

Reviewing files that changed from the base of the PR and between d103ebd and 79d22f7.

📒 Files selected for processing (5)
  • Source/JavaScriptCore/ftl/FTLCompile.cpp
  • Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp
  • Source/JavaScriptCore/ftl/FTLOSRExit.cpp
  • Source/JavaScriptCore/ftl/FTLOSRExit.h
  • Source/JavaScriptCore/ftl/FTLOSRExitCompiler.cpp

Comment on lines +138 to +142
case ExitValueMaterializeNewObject:
bytes.append(static_cast<uint8_t>(value.kind()));
encodeVarint(bytes, materializations.size());
materializations.append(value.objectMaterialization());
break;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Dedup repeated materialization pointers instead of storing them once per occurrence.

Each time an ExitValueMaterializeNewObject operand is encountered, its pointer is unconditionally appended to materializations, so a materialized object referenced by several locals/tmps/heap slots gets stored redundantly (8 bytes per occurrence) rather than once. Since deduplicating shared materializations is exactly the kind of storage this PR is trying to cut, mapping each unique pointer to a single index would avoid this.

♻️ Proposed dedup via index map
     Vector<uint8_t> bytes;
     Vector<ExitTimeObjectMaterialization*> materializations;
+    UncheckedKeyHashMap<ExitTimeObjectMaterialization*, unsigned> materializationIndices;
         case ExitValueMaterializeNewObject:
             bytes.append(static_cast<uint8_t>(value.kind()));
-            encodeVarint(bytes, materializations.size());
-            materializations.append(value.objectMaterialization());
+            encodeVarint(bytes, materializationIndices.ensure(value.objectMaterialization(), [&] {
+                materializations.append(value.objectMaterialization());
+                return materializations.size() - 1;
+            }).iterator->value);
             break;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
case ExitValueMaterializeNewObject:
bytes.append(static_cast<uint8_t>(value.kind()));
encodeVarint(bytes, materializations.size());
materializations.append(value.objectMaterialization());
break;
Vector<uint8_t> bytes;
Vector<ExitTimeObjectMaterialization*> materializations;
UncheckedKeyHashMap<ExitTimeObjectMaterialization*, unsigned> materializationIndices;
case ExitValueMaterializeNewObject:
bytes.append(static_cast<uint8_t>(value.kind()));
encodeVarint(bytes, materializationIndices.ensure(value.objectMaterialization(), [&] {
materializations.append(value.objectMaterialization());
return materializations.size() - 1;
}).iterator->value);
break;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Source/JavaScriptCore/ftl/FTLOSRExit.cpp` around lines 138 - 142, Update the
ExitValueMaterializeNewObject handling to deduplicate objectMaterialization()
pointers before appending to materializations: reuse the existing index for a
previously seen pointer and append/store a new index only for unique pointers.
Add or use a pointer-to-index map in the surrounding encoding routine while
preserving the encoded index semantics for all materialization references.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't find any bugs, but this is a ~250-line change to FTL OSR exit descriptor storage — a correctness-critical path where encoding mistakes silently miscompile JS — so it deserves a human pass.

What was reviewed:

  • Encode/decode symmetry for all ExitValueKind cases, dead-run length encoding (short + LEB overflow), and zigzag varint for virtual registers.
  • Lazy m_localsOffset application in decodeValues() matches ExitValue::withLocalsOffset exactly (only InJSStack* with isLocal()).
  • All former m_values consumers (compileStub, validateReferences, dump paths, FTLCompile) now go through decodeValues() / setLocalsOffset(); no stale references remain.
  • ExitValueKind max value (8) fits the 4-bit tag; ExitArgument::argument() is unsigned so the uint32 varint is lossless.
Extended reasoning...

Overview

This PR replaces OSRExitDescriptor::m_values (a dense FixedOperands<ExitValue>) with a compact byte-stream encoding: a FixedVector<uint8_t> holding tag-prefixed variable-length records plus a side table of materialization pointers. Producers (LowerDFGToB3::buildExitArguments) encode once via setValues(); consumers (compileStub, validateReferences, verbose dumps) decode on demand via decodeValues(). The eager per-value withLocalsOffset rewrite in FTLCompile.cpp becomes a stored m_localsOffset applied lazily during decode. Five files touched, all under ftl/.

Security risks

None in the traditional sense — no untrusted input reaches the encoder/decoder; the byte stream is self-produced compiler metadata. The real risk is correctness: an encode/decode mismatch would corrupt the operand map used to reconstruct baseline frame state on OSR exit, producing wrong values in locals/tmps after deoptimization. That's a silent-miscompilation class of bug, not an exploit surface, but it's the highest-impact failure mode in a JIT.

Level of scrutiny

High. FTL OSR exit is on the critical path for correctness of all optimized code — every speculation failure flows through compileStub, which now depends on decodeValues() reproducing exactly what setValues() was given (modulo the lazy locals-offset). The change is well-structured and the debug round-trip assertion is strong evidence, but ~250 lines of new hand-rolled binary encoding in this layer warrants a human reviewer who knows the FTL exit machinery signing off.

Other factors

Points that raised my confidence: the static_assert on ExitValueMaterializeNewObject <= mask guards the 4-bit tag; zigzag encode/decode are the standard formulas; the dead-run scheme correctly handles the run == maxShortDeadRun boundary (emits varint 0); decodeValues() RELEASE_ASSERTs both index == size and offset == bytes.size() so over/under-consumption crashes rather than silently corrupts; I confirmed ExitValue::withLocalsOffset only touches InJSStack* locals, matching the lazy path; and grep shows no remaining m_values references in ftl/. The PR description reports identical stress-test failure sets vs baseline under debug+ASAN with the round-trip assertion active. Still, given the blast radius, I'm deferring rather than approving.

@github-actions

Copy link
Copy Markdown

Preview Builds

Commit Release Date
79d22f77 autobuild-preview-pr-349-79d22f77 2026-07-26 06:43:06 UTC

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants