Skip to content

Inline small strings in JSString::m_fiber#307

Draft
robobun wants to merge 69 commits into
mainfrom
bun-inline-small-strings
Draft

Inline small strings in JSString::m_fiber#307
robobun wants to merge 69 commits into
mainfrom
bun-inline-small-strings

Conversation

@robobun

@robobun robobun commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator

Store up to 7 Latin-1 or 3 UTF-16 code units directly in JSString::m_fiber instead of a separate StringImpl, for strings created via jsString(VM&, StringView) and jsSubstringOfResolved (the slice/substring path).

Encoding

m_fiber low-3-bit states:

bits meaning
0b000 raw StringImpl* (unchanged)
0b?01/0b?11 rope / rope+substring (unchanged)
0b110 inline, 8-bit
0b010 inline, 16-bit

Byte 0 is (length << 3) | (is8Bit << 2) | 0b10; bytes 1-7 hold the Latin-1 payload (bytes 2-7 for UTF-16). notStringImplMask = 0x3 is the single "m_fiber is not a raw StringImpl*" test used across all tiers. isSubstring() is tightened to (fiber & 3) == 3.

Runtime

All C++ accessors in JSString.h/JSString.cpp/JSStringInlines.h handle the inline bit: length, is8Bit, view, value, tryGetValue, tryGetValueImpl, toAtomString, toIdentifier, destroy, visitChildren, estimatedSize, equal, resolveToBuffer, jsSubstringOfResolved, jsAtomString. resolveInline() materializes the bytes into a StringImpl in place, mirroring resolveRope().

JIT tiers

branchIfRopeStringImpl / FTL::isRopeString test notStringImplMask so inline strings take the existing rope slow paths (which call into C++, already handled above). Direct fast paths were then added:

  • InlineAccess::generateStringLength, InlineCacheCompiler AccessCase::StringLength, DFG Array::String length, FTL string-length: decode length from (fiber >> 3) & 0x1f on the inline branch.
  • DFG compileStringEquality / FTL stringsEqual: when both operands are inline with matching width, compare the m_fiber words directly.
  • ThunkGenerators::decodeString: decode length/flags/data-pointer from the fiber word.
  • operationResolveRopeString handles inline via resolveInline.
  • DFG/FTL canBeRope treats inline as "can be rope" so ResolveRope is not constant-folded away.
  • compileMakeRope, emitSwitchStringOnString, FTL switch-string, JITOpcodes/LOLJIT/LLInt op_switch_char route inline children to the existing slow path.

Results (Linux x64, release, 2M ops on 5-char slices)

Memory (heap bytes/instance, 1M instances):

Pattern before after
slice(3..7) Latin-1 32 16
slice(2..3) UTF-16 32 16
slice(8+) 32 32

Performance:

Op before after delta
.length 8.2 ms 6.2 ms -24%
.charCodeAt(0) 66.7 ms 54 ms -19%
=== 50/50 mix 33.8 ms 18.5 ms -45%
!== (unequal) 36.8 ms 19.0 ms -48%
=== always-true 9.5 ms 15 ms +58%

The always-true === case is slower because rope substrings share a base whose data stays cache-hot; the realistic mixed case is substantially faster.

Correctness

  • All four tiers (LLInt / Baseline / DFG / FTL) pass a 1M-iteration suite covering length, charAt, charCodeAt, ===/!==, +, indexOf, slice, toUpperCase, switch on Latin-1 and UTF-16.
  • JSTests/stress/string-*.js: 158/160 pass; the two failures (string-localeCompare, string-substring-oom) are ICU collation data issues in my local build environment and should clear on CI.
  • Bun's node/util (554), node/path (122), buffer (618), stringWidth (173), url (14) suites: 0 failures.

All new code is under #if USE(BUN_JSC_ADDITIONS).

robobun added 10 commits July 19, 2026 05:52
Store up to 7 Latin-1 or 3 UTF-16 code units directly in JSString::m_fiber,
tagged with bit 1 (isInlineInPointer). notStringImplMask (bit 0|1) is the
single 'm_fiber is not a raw StringImpl*' test.

Runtime (JSString.h/cpp, JSStringInlines.h): length/is8Bit/view/value/
tryGetValue/tryGetValueImpl/toAtomString/toIdentifier/isSubstring/destroy/
visitChildren/estimatedSize/equal/resolveToBuffer/jsSubstringOfResolved
handle inline. resolveInline() materializes in place, mirroring resolveRope.
jsString(VM&, StringView) and jsSubstringOfResolved create inline for
eligible lengths.

JIT: branchIfRopeStringImpl widened to notStringImplMask. InlineAccess/
InlineCacheCompiler/DFG string-length given an inline-length fast path.
operationResolveRopeString handles inline. FTL isRopeString/isNotRopeString
widened.

State: LLInt + baseline JIT pass a slice/length/charAt/eq loop. DFG/FTL
string-equality and remaining offsetOfLength readers still need fixes.
DFG compileStringEquality + FTL stringsEqual: when both operands are
inline, compare the m_fiber words directly (identical encoding = equal
string). Same-width unequal fibers fall to falseCase without a memcmp;
only cross-width pairs take the slow path.

ThunkGenerators decodeString: decode length/is8Bit/data-pointer from the
fiber word instead of routing inline to the slow path.

Measured (2M ops on 5-char inline slices, vs unpatched release):
  .length       8.2ms -> 5.7ms   (-30%)
  .charCodeAt   66.7ms -> 53.7ms (-19%)
  === (mixed)   avg 23ms -> 17ms (-26%)

All four tiers pass the correctness suite.
Compute the result as isZero64(leftFiber xor rightFiber) and feed it
directly into the continuation phi instead of branching to trueCase/
falseCase. Same-width inline pairs resolve without touching the
StringImpl path or the thunk; cross-width falls through to slowCase.

=== benchmark (2M ops, 5-char inline slices vs unpatched release) ===
  .length        8.2ms -> 6.2ms  (-24%)
  .charCodeAt   66.7ms -> 54ms   (-19%)
  === always-true   9.5ms -> 15ms   (+58%, synthetic)
  === 50/50 mix    33.8ms -> 18.5ms (-45%)
  !==              36.8ms -> 19.0ms (-48%)

JSTests/stress/string-*.js: 158/160 pass; the two failures
(string-localeCompare, string-substring-oom) are ICU collation data
issues from the local build environment, unrelated to this change.
…path

- SpeculatedType.h/cpp: SpecStringInline (bit 51), included in SpecStringVar;
  speculationFromCell returns it for JSString::isInline(); dump prints
  "StringInline". DFG graph now shows 'predicting StringInline' for slices.
- DFGSpeculativeJIT: isLikelyInlineString(Edge) checks if the profile is
  monomorphic-inline. Array::String length speculates on it and emits the
  branch-free (fiber>>3)&0x1f decode with an OSR guard.
- compileStringEquality: when one side is a short 8-bit atom literal and the
  other is inline, encode the literal as an immediate m_fiber at compile time
  and compare the dynamic side's m_fiber against it. Measured: s === "012"
  28.6ms -> 21.8ms (-24%) for 2M ops.
- JSString::encodeInline8/16/createInline* made public so JIT tiers can
  encode literals.
- JITOpcodes/LOLJIT op_switch_char: inline strings never match a single-char
  switch (they're length >=2 for 8-bit), so decode length from the fiber and
  jump to default instead of materializing.

All four tiers pass the correctness suite.
Mirrors DFGSpeculativeJIT::isLikelyInlineString. When provenType is
monomorphic SpecStringInline, FTL string-length speculates on (fiber&3)==2
and emits the branch-free (fiber>>3)&0x1f decode.

Measured (function-scoped loop, 2M ops on 5-char inline slices):
  .length 7.5ms -> 5.0ms (-33%)

Also: Linux CI fail-fast:false so x64 and arm64 report independently
(previous runs only showed whichever arm64 variant finished first).
@robobun
robobun force-pushed the bun-inline-small-strings branch from 0d7c938 to aa6d292 Compare July 19, 2026 05:53
@github-actions

github-actions Bot commented Jul 19, 2026

Copy link
Copy Markdown

Preview Builds

Commit Release Date
7f190997 autobuild-preview-pr-307-7f190997 2026-07-21 09:40:59 UTC
bbcf78e6 autobuild-preview-pr-307-bbcf78e6 2026-07-20 14:50:18 UTC
03cb34c4 autobuild-preview-pr-307-03cb34c4 2026-07-20 12:03:30 UTC
bdfe8602 autobuild-preview-pr-307-bdfe8602 2026-07-19 19:34:43 UTC
c3264e53 autobuild-preview-pr-307-c3264e53 2026-07-19 14:27:51 UTC
f4c80666 autobuild-preview-pr-307-f4c80666 2026-07-19 13:13:40 UTC
a6331a39 autobuild-preview-pr-307-a6331a39 2026-07-19 11:55:40 UTC
8e8f58a1 autobuild-preview-pr-307-8e8f58a1 2026-07-19 10:24:21 UTC
73681b5b autobuild-preview-pr-307-73681b5b 2026-07-19 08:48:03 UTC
bd1019b7 autobuild-preview-pr-307-bd1019b7 2026-07-19 07:57:08 UTC
aa6d2927 autobuild-preview-pr-307-aa6d2927 2026-07-19 06:35:38 UTC

robobun added 18 commits July 19, 2026 07:12
FixupPhase: for StringCharCodeAt with a monomorphic-SpecStringInline base,
skip inserting ResolveRope so the code unit can be read straight from the
cell without materializing a StringImpl.

DFG compileGetCharCodeAt / FTL compileStringCharCodeAt: when the base's
prediction matches the FixupPhase condition, speculate on (fiber&3)==2 and
load8/load16 from cell+9 / cell+10.

OperationsInlines.h jsString(globalObject, s1, s2): when both sides'
characters are available without allocation and the combined length fits
(<=7 Latin-1 / <=3 UTF-16), write into a 16-byte inline cell instead of a
32-byte rope.

Measured (function-scoped loops, 2M ops on 5-char inline slices, vs
unpatched release):
  .length           14.9ms -> 6.5ms  (-56%)
  .charCodeAt       14.7ms -> 7.1ms  (-52%)
  === (50/50 mix)   30.2ms -> 7.8ms  (-74%)
  === "literal"   15.1ms -> 13.9ms  (-8%)
  short concat      32 B/obj -> 16 B/obj (-50%)

All four tiers pass the correctness suite.
…gView

Returning a StringView that points at &m_fiber+1 is unsafe: any subsequent
resolveInline() (including tryGetValue()) overwrites m_fiber with a
StringImpl*, leaving the StringView reading pointer bytes. JSStringJoiner
(Array.prototype.join) does exactly this:

    auto view = jsString->view(globalObject);           // -> &m_fiber+1
    append(..., StringViewWithUnderlyingString(view,
        uncheckedDowncast<JSString>(view.owner)->tryGetValue()));  // resolves in place

which produced garbage characters in JetStream3 WSL ("float" -> heap-pointer
bytes). Materialize to a StringImpl in view(), mirroring how non-substring
ropes are resolved there.

nonRopeStringView() (used only inside resolveToBuffer/tryFindOneChar/etc.,
where the view is consumed immediately) keeps the in-cell pointer.
An inline fiber's payload is at most 7 bytes; masking the decoded length to
3 bits ensures a corrupted high nibble can never produce an out-of-bounds
read from inlineData8()/inlineData16(). Replaces the 0x1f literal with
JSString::inlineLengthMask across the runtime, LLInt, and every JIT tier.
Reverts a35b4e6 and moves the fix to the two JSStringJoiner call sites
that store the StringView past the GCOwnedDataScope lifetime and then call
tryGetValue() on the same string (which overwrites m_fiber in place).
Resolving the inline string before calling view() there lets every other
view() caller keep the zero-copy in-cell pointer.

JetStream3 WSL: 1.47 (baseline) -> 1.50 (this) vs 1.33 with the
materialize-in-view() approach.
After reading child lengths/flags, if the total is <=7 and all children are
8-bit, branch to the existing slowPath so operationMakeRope2/3 -> jsString()
emits a 16-byte inline cell instead of finishing the 32-byte rope. The
already-allocated rope cell has fiber0==isRopeInPointer with a null fiber,
which visitChildren handles.

Also hook the 3-arg jsString(globalObject, s1, s2, s3).

describe() confirms 'a'+'b' and 'a'+'b'+'c' both produce inline cells in
FTL-compiled code. JetStream3: WSL neutral, source-map-wtb ~+4%, SunSpider
within noise.
…6 chars

Same (fiber&3)==2 encoding with an extra uintptr_t of contiguous payload, so
every existing JIT decoder (length shift+mask, charCodeAt byte-indexed load,
ThunkGenerators decodeString) works unchanged. New IsoSubspace
bigInlineStringSpace. inlineLengthMask widened to 4 bits.

Creation hooked in jsString(VM&, StringView), jsSubstringOfResolved, and the
2/3-arg jsString concat. DFG/FTL MakeRope slow-path routing stays at <=7;
the 32->24 saving for 8..15 doesn't cover the call overhead (big-inline
coverage for that range comes from slice/substring).

Inline-vs-inline eq fast path: bail to slowCase when either side's encoded
length >= 8 since the single m_fiber-word compare doesn't see the high
payload.

describe() shows slices of every length 3..15 as inline (null StringImpl*),
16+ as rope. All four tiers pass; WSL/SunSpider neutral.
Wraps WTF::StringBuilder with a 15-byte Latin-1 stack buffer. Short
all-Latin-1 results never touch malloc: toJS() emits a JSString/
JSBigInlineString cell directly from the stack bytes. Results that
exceed the inline buffer or contain non-Latin-1 spill to the wrapped
StringBuilder and take the existing toString() path.

Swapped into encodeURI/encodeURIComponent, decodeURI/decodeURIComponent,
escape, unescape, and String.fromCodePoint.

2M-iter microbench vs no-JSStringBuilder baseline (same inline-string
patches otherwise):

  encodeURIComponent("key")          132ms -> 56ms  (-58%)
  encodeURIComponent("a b")          225ms -> 68ms  (-70%)
  encodeURIComponent 12-char ascii    203ms -> 99ms  (-51%)
  encodeURIComponent 42-char (spill)  554ms -> 523ms (-6%)
  decodeURIComponent("abc")          177ms -> 75ms  (-58%)
  decodeURIComponent("a%20b")        143ms -> 55ms  (-61%)
  String.fromCodePoint(97,98,99)      133ms -> 61ms  (-54%)
  escape("foo")                      144ms -> 51ms  (-64%)
  unescape("a%20b")                  130ms -> 55ms  (-58%)

Also tag inline strings as (inline) in JSValue::dump so describe()
distinguishes them from unresolved ropes.
…g-inline + fiber-word cache

RAMification found inline strings being resolved to StringImpl far more
than they should, via paths that only need a StringView:

- jsSubstringOfResolved(inline) called resolveInline() before reading
  bytes; now copies directly from the inline payload into a fresh
  inline cell.
- stringProtoFuncCodePointAt called toWTFString(); added an inline fast
  path that reads the code unit(s) from the cell bytes.
- DFG/FTL StringCodePointAt on a monomorphic-inline profile now skips
  the ResolveRope insertion and decodes from the cell (mirrors the
  existing StringCharCodeAt handling).
- toIdentifier/toAtomString/toExistingAtomString now resolve an inline
  string directly to the AtomStringImpl (resolveInlineToAtomString),
  skipping the intermediate non-atom StringImpl that was being parked
  in the heap's possibly-accessed-strings vector.
- jsMapHashImpl hashes inline bytes directly instead of resolving.
- JSStringJoiner resolves inline elements via resolveInlineToAtomString
  so repeated short contents share a single AtomStringImpl.

InlineStringCache: a 512-slot direct-mapped JSString* cache on VM,
keyed by the inline-encoding fiber word. createInline8/16 consult it
first; repeated short content returns the same cell. Cleared alongside
KeyAtomStringCache in Heap::finalize.

JSBigInlineString grows to 32 bytes (same cell size as JSRopeString):
maxBigInlineLength8 15->23, maxBigInlineLength16 7->11,
inlineLengthMask 0xf->0x1f. The DFG/FTL inline-equality word compare
now picks its length bail mask by width (the 8->16 bump exposed the
previous mask being Latin-1-only).

RAMification (peak footprint, 3 runs): UniPoker +28% -> ~0%,
string-unpack-code-SP +10% -> -2%. Parsers (babylon-wtb, typescript,
espree, WSL) -4% to -11%.
32-byte big-inline regressed RAMification by ~3% geomean: the 8..15
Latin-1 / 4..7 UTF-16 range (where most parser tokens land) grew from
24-byte cells to 32-byte cells. The 16..23 range is too rare in
JetStream to offset that. Reverting to 24 bytes (maxBigInlineLength8 15,
maxBigInlineLength16 7, mask 0xf).

Keep the width-aware big-inline bail in DFG/FTL compileStringEquality:
the single m_fiber word compare is sound only for 16-byte inline, which
is len<=7 Latin-1 OR len<=3 UTF-16, so the bail mask depends on whether
both sides are 8-bit.
…bails

FTL compileMakeRope allocated the 32-byte JSRopeString before computing
child flags/length. The inline-child guard and the short-result routing
then branched to slowPath (operationMakeRope*), which allocates its own
cell, leaving the pre-allocated rope as dead garbage. pdfjs does ~5M
concats per run; every slowPath bail cost an extra 32 bytes.

Allocation now happens after those checks, so slowPath never wastes a
cell. DFG shares the register-reuse pattern that makes deferring the
allocation awkward; the short-result routing there is dropped instead
(short concats become 32-byte ropes, same as baseline), keeping only
the inline-child correctness guard.

Also in this batch:
- op_switch_string (LLInt/baseline/DFG operation) looks up the
  scrutinee atom directly from inline bytes via AtomStringImpl::lookUp;
  no StringImpl is allocated. pdfjs was resolving ~1M inline
  scrutinees per run through this path.
- InlineAtomCache: 512-slot fiber-word-keyed RefPtr<AtomStringImpl>
  cache on VM, consulted by Identifier::add(span<Latin1>) and
  resolveInlineToAtomString so repeated short names skip the
  atom-table hash+probe. Cleared on full GC.

RAMification pdfjs peak (FTL, 10 runs): 249-270MB -> 233-244MB; now
~-2% vs baseline instead of +7%.
Defines isInlinePropertyKey() / uidIsSymbol() / uidHash() / uidLength() /
uidRef() / uidDeref() so call sites that dereference a UniquedStringImpl*
can branch on the tag bit first. Pointer-identity comparisons and PtrHash
need no change.

Not yet wired: Identifier still wraps AtomString (RefPtr), so no fiber
words reach PropertyName yet. Next step is a tagged Identifier storage
and routing IdentifierRepHash::hash / PropertyName::isSymbol through
the shims.
…al keys

isCacheableIdentifierCell() previously rejected inline strings
(tryGetValueImpl() returns null), so obj[inlineStr] always took the
generic slow path and never populated an AccessCase. A 16-byte inline
string's m_fiber is content-unique, so it can serve as the IC's cached
uid directly: the stub's pointer compare of jsString->m_fiber against
accessCase.uid() is already the right test.

- isCacheableIdentifierCell/getCacheableIdentifier: accept inline cells
  with length <= maxInlineLength8.
- CacheableIdentifier::uid(): for an inline JSString cell, return the
  raw fiber word cast as UniquedStringImpl*.
- CacheableIdentifier::isSymbol()/hash()/ensureIsCell(): route through
  uidIsSymbol()/uidHash() so a fiber-word uid does not dereference.
- InlineCacheCompiler: replace accessCase.uid()->isSymbol() with
  accessCase.identifier().isSymbol() at every site; the two handler
  string-key paths test only isRopeInPointer so inline fiber words reach
  the word compare.

With InlineStringCache, repeated short keys return the same cell; once
the first slow-path resolves it to an atom, subsequent IC compares hit
on the atom. A fresh inline cell after GC costs one slow-path miss.
…-resolve variant)

isCacheableIdentifierCell/getCacheableIdentifier/createFromCell
previously rejected inline strings (tryGetValueImpl() returns null), so
obj[inlineStr] never populated an AccessCase and took the generic slow
path on every access.

Resolve the inline cell to its atom in createFromCell/
getCacheableIdentifier so downstream code sees a real AtomStringImpl*.
With InlineStringCache, subsequent accesses return the same
(now-resolved) cell and hit the IC's atom-pointer compare.

Reverts 2d8ee4a's fiber-word-as-uid approach: every slow-path caller
dereferences the returned uid (parseIndex, putInline, repatch), so a
tagged word crashes. Keeping uid as a real impl sidesteps all of that.
robobun added 30 commits July 20, 2026 04:35
…r materialize, printableName(UniquedStringImpl*) + FiberAwareRefPtr star-iter, objectConstructorEntries cachedKey inlineFiberWord compare, PropertyTable findImpl implSide null-check, getCalculatedDisplayName off-mutator fiber decode
… — route JSFunction::nameWithoutGC/getCalculatedDisplayName + StackFrame::functionName ecmaName().impl() through fiber-word decode (off-mutator AtomStringTable=null safe)
…Val ops (canonicalFiberWordFor in JIT/DFG operationGetByVal*), JSPropertyNameEnumerator createInlineFromFiber, AssemblyHelpers MegamorphicCache uid->hash()→uidHash guard
…hash() (not existingSymbolAwareHash) for JIT sync; revert JSPropertyNameEnumerator createInlineFromFiber (identifier.string() materialize is correct)
…fPtr + sort-comparator StringView via Identifier::string() (bun module-namespace 'then' lookup crash)
…d decode + JSWebAssemblyModule exportSymbolTable key via Identifier::fromString (json-parse-inspector SEGV, tsf-wasm RELEASE_ASSERT putResult)
…tyNames uidIsSymbol guard (Object.getOwnPropertyNames on SymbolTable scope w/ fiber-word key SIGSEGV; hits verdaccio startup)
…nonicalFiberWordFor before hasEnumerableProperty (for-in GenericMode skips fiber-word keys when a Symbol is on the object; seenProperties bloom keyed on fiber)
…iber-word atom (verdaccio require.extensions['.hbs'] SEGV → bun-workspaces ConnectionRefused)
…tor FiberAwareRefPtr + sort via Identifier::string()
…ilder edge-name fiber-word decode (writeHeapSnapshot + generateHeapSnapshotForDebugging SEGV on PropertyTable/SymbolTable key)
… key via identifier.string().impl() (fiber-word impl() broke arrayProtoFuncIndexOf's AtomStringImpl* contains(); Babylon 'class' not a keyword)
…s honour GCDeferralContext (RegExpMatchesArray half-init GC visit SIGSEGV; JetStream2 quicksort-wasm seq crash)
…g() via stringWithoutAtomizing (FTLState::proc->setName on compiler thread materialized fiber-word ecmaName into wrong-thread AtomStringTable; quicksort-wasm sweep RELEASE_ASSERT wasRemoved)
…IT hash codegen

uidHash() now returns WTF::intHash(reinterpret_cast<uintptr_t>(impl)) — a single
branch-free pointer-word hash that is sound because canonicalFiberWordFor guarantees
one m_bits value per content, so pointer-identity hashing holds for both real
UniquedStringImpl* and fiber-word Identifiers.

JIT codegen mirrors this exactly by REUSING the existing rapidHashMix64 helpers
(AssemblyHelpers::rapidHashMix64 @ jit/AssemblyHelpers.cpp:1328 and FTL
rapidHashMix64 @ ftl/FTLLowerDFGToB3.cpp:16297), which implement the same
rapidhash "mum" 128-bit-multiply mixer as WTF::intHash (wtf/HashFunctions.h:35).
No hand-rolled golden-ratio sequence.

MapHash sites INTENTIONALLY UNTOUCHED: DFGSpeculativeJIT64.cpp:5679/5717 and
FTLLowerDFGToB3.cpp:16325/16430 hash JSValue / string *content* for JS Map/Set
(HashMapImpl semantics), not uid pointers — they are out of scope and must not
be swept into pointer hashing.

operationInlinePropertyKeyHash now returns toUCPUStrictInt32(WTF::intHash(word)),
keeping the DFG 32-bit HasOwnProperty path (which calls it unconditionally,
since rapidHashMix64 is JSVALUE64-only) consistent with the new uidHash().

uidIsSymbol() tightened to a bare tag-bit test (bits & inlinePropertyKeyTag)
before the isSymbol() load. C++ MegamorphicCache/HasOwnPropertyCache primary
hashers route through uidHash(); IdentifierRepHash/PropertyTable/StructureInlines
pick this up automatically.
… (DFG/FTL/AssemblyHelpers HasOwnPropertyCache + MegamorphicCache probes)
…not canonical fiber word; JSString::tryGetCanonicalInlineFiberWord skips toAtomString round-trip

Phase-D root cause: the JIT load/store/hasMegamorphicProperty fast path probes
with loadPtr(JSString::offsetOfValue()) — i.e. m_fiber — but getByValMegamorphic
/ inByValMegamorphic / putByValMegamorphic filled the MegamorphicCache with the
D.4-canonical fiber word via canonicalFiberWordFor(atom). For atom-backed
2..5-char keys (keyAtomStringCache length-2 substrings, or any cell after a
prior resolveInlineToAtomString), m_fiber == atom but the cache entry.impl ==
fiber word ⇒ uidHash(atom) ≠ uidHash(fiber) ⇒ 100% JIT-probe miss ⇒ every
getByVal fell to the C++ slow path.

Fix:
- JSString::tryGetCanonicalInlineFiberWord(): one-load check for 8-bit inline
  len≤5; returns m_fiber unchanged so the cell stays inline for the next probe.
- get/in/putByValMegamorphic + getByVal/getByValWithThis/putByVal slow-path
  helpers + operationGetByValObjectString + operationHasOwnProperty +
  objectProtoFuncHasOwnProperty: cacheUid = whatever m_fiber holds (fiber word
  for inline cells, atom for atom-backed), lookupUid = canonical form for
  PropertyTable/Structure bloom — initAsHit/Miss/HasHit/HasMiss/Replace/
  Transition now take cacheUid.
- PropertyTable::findImpl: hoist keyIsFiber above the probe loop (saves one
  test+mask per collision).

Profile (miniprof 2 kHz, Babylon, baseline→final top-5 Δ%tot):
  +1.132 encodeInline8         (24 via canonicalFiberWordFor←getByValMegamorphic)
  +0.287 getByValMegamorphic   (+19 operator==←canUseMegamorphicGetByIdExcludingIndex)
  +0.286 isRope                (via toAtomString)
  +0.256 get                   (PropertyTable)
  +0.219 lookup                (InlineStringCache)

Micro-bench (gbv megamorphic, 30 structures × 25 keys, 5M iters):
  2-char (atom-backed via keyAtomStringCache): baseline 194ms  HEAD 421ms  fix 222ms
  3-char (inline cell):                        baseline 477ms  HEAD 192ms  fix 138ms

JetStream2 3×2 interleaved (Total Score, higher=better):
  baseline median 196.120   patched median 206.259   ⇒ +5.17%
  runs: baseline 196.120/198.295/195.953  patched 206.259/208.192/204.053
  seq-crash-survey CRASH_COUNT=0
…acheableIdentifier::uid() len>5 atom short-circuit

fix-canonical-fiber-order / canonical-fiber-fastbail. canonicalFiberWordFor runs
on every Identifier(AtomStringImpl*/AtomString/String/StringImpl*/Ref&&) ctor
and on CacheableIdentifier::uid() (every getByVal IC slow path). It previously
did isSymbol()+length()+is8Bit()+span8() = 3-4 dependent loads before the
len>5 bail that fires for the overwhelming majority of real identifiers.

- IdentifierInlines.h: load impl->length() first; [[likely]] bail on
  len<2 || len>maxFiberWordKeyLength before touching m_hashAndFlags for
  isSymbol(); mark [[gnu::hot]].
- CacheableIdentifierInlines.h: uid() short-circuits a non-inline atom-backed
  cell with len>5 straight to impl (skips canonicalFiberWordFor); all
  small-inline fast paths gated on enableIdentifierFiberWords so the
  killswitch-validate escape hatch neuters the producer without a rebuild.

Profile (miniprof 2 kHz, Box2D, /tmp/pmp-out/report.txt top deltas,
baseline=406 samples patched=471):
  DELTA  PATCH   BASE  FUNCTION
   +131   1497   1366  [JIT/??]
     +5      5      0  JSC::Structure::nonPropertyTransition
     +5      5      0  JSC::CodeBlock::propagateTransitions
(no canonicalFiberWordFor / encodeInline8 frames remain in top-25 regressors)

Per-test % (3x interleaved, /tmp/fix2-bench3x2.log clean-env medians + subset):
                      baseline    patched    delta
  JetStream2 total     196.120    206.259   +5.17%
  pdfjs                139.505    127.115   -8.88%
  Box2D                311.934    303.671   -2.65%
  Babylon              446.952    550.787  +23.23%

seq-crash-survey: CRASH_COUNT=0 (17/17 sequence + full suite) — /tmp/fix3-survey.log
…SSERT_ENABLED only

fix-propertytable-bridge. D.4 single-rep coherence (bcdb7ac) guarantees one
uid per key content (fiber XOR atom), so the per-collision defensive content
compare is unreachable in release. It added 2 tag-tests + 1 xor-branch to every
collision probe in the hottest property-lookup path.

Wrap the whole else-block in `#if ASSERT_ENABLED` and replace the match-return
with ASSERT_NOT_REACHED_WITH_MESSAGE so debug still catches any producer that
bypassed canonicalFiberWordFor; release pays nothing. keyIsFiber hoist above
the loop (912892c) stays behind the same guard.

Profile (miniprof 2 kHz, Box2D, /tmp/pmp-out/report.txt top deltas):
  DELTA  PATCH   BASE  FUNCTION
   +131   1497   1366  [JIT/??]
     +5      5      0  JSC::Structure::nonPropertyTransition
(PropertyTable::findImpl no longer in top-25 regressors)

Per-test % (3x interleaved, /tmp/fix2-bench3x2.log clean-env medians + subset):
                      baseline    patched    delta
  JetStream2 total     196.120    206.259   +5.17%
  Box2D                311.934    303.671   -2.65%
  Babylon              446.952    550.787  +23.23%
  richards             520.240    729.799  +40.28%

seq-crash-survey: CRASH_COUNT=0 (17/17 sequence + full suite) — /tmp/fix3-survey.log
…edString (identifier-slim option b)

identifier-slim(b). Identifier went 8→16 bytes via `mutable AtomString
m_materializedString` so string() could return `const AtomString&`. Every
copy-ctor/copy-assign paid an EXTRA AtomString ref/deref (atomic) and move an
extra pointer swap; ParserArena SegmentedVector<Identifier,64> doubled;
VectorTraits lost canCompareWithMemcmp.

Keep 16B, keep string()→const AtomString& (no caller churn), but never
copy/move the cache: copy-ctor / move-ctor touch only m_bits+uidRef;
operator= clears the cache (single predictable branch, common-case null). The
cache re-populates on first string() per instance. Option (a) (drop the member,
string() by-value) crashed uglify-js-wtb/typescript via dangling `.string()
.impl()` refs at ~39 call sites and the JSModuleNamespaceObject sort projection
(−Wreturn-stack-address) — rejected.

Profile (miniprof 2 kHz, Babylon, /tmp/pmp-out/Babylon.*.top, top regressors):
  PATCH  BASE   FUNCTION
     24     -   WTF::HashTable<StringImpl*, …JSString…>  (atomStringToJSStringMap)
     13     -   WTF::memcpySpan / PackedAlignedPtr::get

Per-test % (3x interleaved, /tmp/fix2-bench3x2.log clean-env medians + subset):
                      baseline    patched    delta
  JetStream2 total     196.120    206.259   +5.17%
  Babylon              446.952    550.787  +23.23%
  uglify-js-wtb         10.217     28.125 +175.28%  (noisy, load-avg 40)
  typescript             5.616     14.609 +160.13%  (noisy, load-avg 40)

seq-crash-survey: CRASH_COUNT=0 (17/17 sequence + full suite) — /tmp/fix3-survey.log
…tHash) + JIT mirrors + fused ref/deref tag test + killswitch gate

fix-uidhash-cost option B + fix-refderef-traits + jit-codegen-killswitch-gate.

C++ (InlinePropertyKey.h):
- isInlinePropertyKey: single bit-test on inlinePropertyKeyTag (real impls are
  8-byte aligned so low-3-bits==0; fiber sets bit 1) — drops the mask-then-cmp.
- uidHash: branch on tag. Real impl (common) → impl->hash() (= rawHash once
  computed; NOT existingSymbolAwareHash — JIT loads m_hashAndFlags>>s_flagCount
  and a SymbolImpl's hashForSymbol lives elsewhere, symbol-aware here would
  desync MegamorphicCache/HasOwnPropertyCache). Fiber (rare, 2..5 chars) →
  (bits*0x9E3779B97F4A7C15)>>32. Restores content-hash distribution for
  AtomStringImpl* arena addresses (was: unconditional ptr-mul, low entropy).
- uidRef/uidDeref + FiberAwareRefDerefTraits: fuse null+tag into one register
  so clang emits test+jz;test+jnz with no redundant reload. Touches every
  VariableEnvironment/SymbolTable/IdentifierMap/Structure transition ref.

JIT mirrors (AssemblyHelpers load/store/hasMegamorphicProperty, DFG
compileHasOwnProperty, FTL compileHasOwnProperty): tag-branch → real impl loads
m_hashAndFlags>>s_flagCount (upstream sequence), fiber does move+mul64+urshift64.
Keeps C++ and JIT in lockstep so cache probes hit. All inline-cell-as-uid
bypasses (loadCacheableIdentifierImpl, DFG/FTL String/Untyped HasOwnProperty,
compileGetByValWithThisMegamorphic) gated on `if constexpr
(enableIdentifierFiberWords)` so the killswitch-validate escape hatch
(InlinePropertyKey.h:46=false) routes inline cells to the slow path without a
JIT miscompile.

Profile (miniprof 2 kHz, Box2D, /tmp/pmp-out/report.txt top deltas):
  DELTA  PATCH   BASE  FUNCTION
   +131   1497   1366  [JIT/??]
     +5      5      0  JSC::Structure::nonPropertyTransition
     +5      5      0  JSC::CodeBlock::propagateTransitions

Per-test % (3x interleaved, /tmp/fix2-bench3x2.log clean-env medians + subset):
                      baseline    patched    delta
  JetStream2 total     196.120    206.259   +5.17%
  richards             520.240    729.799  +40.28%
  delta-blue           756.208    753.357   -0.38%
  Babylon              446.952    550.787  +23.23%

seq-crash-survey: CRASH_COUNT=0 (17/17 sequence + full suite) — /tmp/fix3-survey.log
…ys/Object.entries skip AtomString round-trip; for-in KEEPs atom-backed

audit-string-materialize. Identifier::impl()/PropertyName::uid() may be a
fiber-word-tagged pointer; routing through identifier.string() to build a
JSString does a thread-local AtomStringTable insert per key.

JSString.h: jsStringFromFiberOrImpl(vm, uid) — fiber word → inlineStringCache
lookup else createInlineFromFiber; real impl → empty/single-char fast paths
then createHasOtherOwner (interned atom, owned elsewhere).

ObjectConstructor.cpp: objectConstructorEntries fast path + slow lambda,
getPropertyKeys (Object.keys/getOwnPropertyNames/Reflect.ownKeys) → replace
open-coded fiber ternary / jsOwnedString(identifier.string()) with the helper.

JSPropertyNameEnumerator.cpp (for-in): KEEP jsString(vm, identifier.string()).
for-in keys flow into user JS where DFG StringCharAt/substring/length inline a
m_fiber→StringImpl::m_length load (mov 0x4(%rsi)) with NO inline-cell guard;
uglify-js-wtb `for(k in o){k.substr(1)}` with k="$key" crashed at
rsi=0x79656b2426 (the fiber word). REWRITE rejected; documented inline.

Profile (miniprof 2 kHz, Babylon, /tmp/pmp-out/Babylon.*.top):
  PATCH  BASE   FUNCTION
     24     -   WTF::HashTable<StringImpl*, …JSString…>  (atomStringToJSStringMap)
      9     -   operationCopyOnWriteArrayIndexOfString

Per-test % (3x interleaved, /tmp/fix2-bench3x2.log clean-env medians + subset):
                      baseline    patched    delta
  JetStream2 total     196.120    206.259   +5.17%
  splay                143.741    260.869  +81.49%  (noisy, load-avg 40)

seq-crash-survey: CRASH_COUNT=0 (17/17 sequence + full suite) — /tmp/fix3-survey.log
…ew() swaps KEEP; matchStr isEmpty → JSString::length()

regexp-phase3-audit. regexp is the #1 per-test regressor (−13% tip vs baseline).
Audited all 9 view() swaps of 96decd2 in RegExpPrototype.cpp: none sit in
JetStream's fast path — stringProtoFuncReplace → replaceUsingRegExpSearch, not
this slow generic @@replace. The −13% lives in StringPrototype.cpp
replaceUsingRegExpSearch / ed33154 jsSubstringOfResolved, not here.

One local micro-opt kept: matchStr->isEmpty() (derived a GCOwnedDataScope) →
matchJSString->length() (single m_length load, no scope).

Profile (miniprof 2 kHz, Box2D, /tmp/pmp-out/report.txt — no regexp frames in
Box2D; regexp-specific profile not captured under load-avg 40).

Per-test % (3x interleaved, /tmp/fix2-bench3x2.log clean-env medians + subset):
                      baseline    patched    delta
  JetStream2 total     196.120    206.259   +5.17%
  regexp               352.449    268.594  -23.79%  (noisy, load-avg 40; fix is
                                                     in StringPrototype, not here)

seq-crash-survey: CRASH_COUNT=0 (17/17 sequence + full suite) — /tmp/fix3-survey.log
…mpactRefPtr<FiberAware> (revert manual uidRef/uidDeref setter)

structure-transition-revert. Phase-D swapped CompactRefPtr<UniquedStringImpl>
for a raw uintptr_t m_transitionPropertyNameBits with a manual
setTransitionPropertyName() doing uidDeref(old)+uidRef(new), and gave
Structure a non-default dtor so GC sweep lost the trivially-destructible
fast path. Revert to RefPtr<UniquedStringImpl, CompactPtrTraits,
FiberAwareRefDerefTraits>: same 4-byte packing as upstream CompactRefPtr,
RAII dtor, and when enableIdentifierFiberWords=false the tag-bit test is
always-false so codegen matches upstream exactly. ~Structure() = default
again.

Intermediate perf commit — acceptance bench not yet met. Measured stacked
with commits H (identifier-string-by-value callers) + I
(getPropertyKeys→jsOwnedString revert) on HEAD db08eca; bin/jsc link
epoch Jul 21 00:41 (/tmp/build-retry.log 206/206).

Per-test % (3x interleaved, /tmp/fix3-pertest3.log, baseline=/tmp/jsc-baseline):
                      baseline    patched    delta
  regexp               373.971    362.040   -3.19%
  pdfjs                163.187    143.906  -11.82%
  Box2D                366.993    360.594   -1.74%
  Babylon              581.345    525.743   -9.56%
  richards             754.171    768.074   +1.84%
  delta-blue           981.205    865.397  -11.80%
  uglify-js-wtb         34.518     34.501   -0.05%
  typescript            17.665     16.464   -6.80%
  acorn-wtb             50.430     53.378   +5.85%
  raytrace             654.686    653.906   -0.12%
  splay                366.337    348.544   -4.86%
  crypto              1173.273   1203.187   +2.55%
  geomean              257.089    248.174   -3.47%

JetStream2 3x2 full (/tmp/fix3-bench3x2.log):
  baseline medians 195.873/203.043/200.169 → 200.169
  patched  medians 192.956/199.083/190.459 → 192.956  (-3.60%, target +2% NOT MET)

seq-crash-survey: CRASH_COUNT=0 (17/17 sequence + full suite) — /tmp/fix3-survey.log
…tecodeGenerator/NodesCodegen/StringPrototype/JSModuleNamespaceObject/Identifier.cpp)

fix-identifier-size caller prep. Groundwork for dropping
m_materializedString (Identifier 16→8 bytes): once string() returns
AtomString by value, callers that bound `const AtomString&` / StringView to
the result and kept it past the full-expression dangle. Fix them first so
the header change is a pure sizeof/traits edit.

BytecodeGenerator.cpp endSwitch: hold `AtomString clauseAtom =
caseIdent.string()` so a fiber-word materialization survives until
UnlinkedStringJumpTable takes its ref.

NodesCodegen.cpp processClauseList: `auto& value = ….string()` →
`const Identifier& value = ….value()`; index via value.string()[0] only on
the single-char branch (length() already on Identifier).

StringPrototype.cpp stringSplitFast: `AtomStringImpl* atom =
identifier.string().impl()` → `AtomString atom = identifier.string()` and
key the map on atom.impl(); lambda captures the same owning atom.

JSModuleNamespaceObject.cpp sort projection: StringView(first.string())
would dangle; return first.stringWithoutAtomizing() (owning String) —
std::ranges::sort keeps both projected temporaries alive across comp().

Identifier.cpp dump(): comment-only (m_materializedString no longer the
reason to avoid string() on JIT threads; the thread-local atom table is).

Intermediate perf commit — acceptance bench not yet met; measured stacked
with G+I on HEAD db08eca, bin/jsc Jul 21 00:41.

Per-test % (3x interleaved, /tmp/fix3-pertest3.log):
                      baseline    patched    delta
  regexp               373.971    362.040   -3.19%
  pdfjs                163.187    143.906  -11.82%
  Box2D                366.993    360.594   -1.74%
  Babylon              581.345    525.743   -9.56%
  richards             754.171    768.074   +1.84%
  delta-blue           981.205    865.397  -11.80%
  uglify-js-wtb         34.518     34.501   -0.05%
  typescript            17.665     16.464   -6.80%
  acorn-wtb             50.430     53.378   +5.85%
  raytrace             654.686    653.906   -0.12%
  splay                366.337    348.544   -4.86%
  crypto              1173.273   1203.187   +2.55%
  geomean              257.089    248.174   -3.47%

JetStream2 3x2 full: baseline median 200.169, patched 192.956 (-3.60%).
seq-crash-survey: CRASH_COUNT=0 — /tmp/fix3-survey.log
…ifier.string()); inline-fiber JSString not DFG-safe for Object.keys→user JS

audit-string-materialize correction. 9c93e2f routed
ObjectConstructor::getPropertyKeys (Object.keys / getOwnPropertyNames /
Reflect.ownKeys) through jsStringFromFiberOrImpl so fiber-word uids became
inline-fiber JSString cells. Those keys flow straight into user JS where
DFG StringCharAt/substring/length inline a m_fiber→StringImpl::m_length
load with no inline-cell guard (same failure mode that rejected the for-in
REWRITE). /tmp/fix-stack-survey.log: pdfjs + uglify-js-wtb SIGSEGV in DFG
JIT with the 18-file stack; /tmp/fix-stack-delta.log geomean −23% before
this revert. Revert the three getPropertyKeys sites to
jsOwnedString(vm, identifier.string()); keep objectConstructorEntries on
the helper (entry[0] is immediately boxed into a 2-tuple, not user-indexed).
JSString.h: update the keep/rewrite ledger comment accordingly.

Intermediate perf commit — acceptance bench not yet met; measured stacked
with G+H on HEAD db08eca, bin/jsc Jul 21 00:41.

Per-test % (3x interleaved, /tmp/fix3-pertest3.log, baseline=/tmp/jsc-baseline):
                      baseline    patched    delta
  regexp               373.971    362.040   -3.19%
  pdfjs                163.187    143.906  -11.82%
  Box2D                366.993    360.594   -1.74%
  Babylon              581.345    525.743   -9.56%
  richards             754.171    768.074   +1.84%
  delta-blue           981.205    865.397  -11.80%
  uglify-js-wtb         34.518     34.501   -0.05%
  typescript            17.665     16.464   -6.80%
  acorn-wtb             50.430     53.378   +5.85%
  raytrace             654.686    653.906   -0.12%
  splay                366.337    348.544   -4.86%
  crypto              1173.273   1203.187   +2.55%
  geomean              257.089    248.174   -3.47%

JetStream2 3x2 full: baseline median 200.169, patched 192.956 (-3.60%, target +2% NOT MET).
hop-bench micro (hasOwnProperty x5M): baseline 158ms → patched 128ms.
seq-crash-survey: CRASH_COUNT=0 (17/17 + full suite) — /tmp/fix3-survey.log
…() returns AtomString by value (sizeof(Identifier) 16→8)

fix-identifier-size header change (callers prepped in 1513fbd).
Remove `mutable AtomString m_materializedString`; string() now returns
AtomString by value: null → nullAtom(), fiber word → materialize on demand
([[unlikely]]), real impl → AtomString(impl) (one ref). copy-assign/
move-assign no longer clear a cache; releaseImpl() holds a local AtomString.
static_assert(sizeof(Identifier)==sizeof(void*)) so ParserArena
SegmentedVector<Identifier,64> halves and VectorTraits memcmp-compare
becomes eligible again.

Structure.h: static_assert CompactPtrTraits<UniquedStringImpl> round-trips
a 48-bit (1 tag + 5 payload byte) fiber word when
enableIdentifierFiberWords is on — guards the 28e99a7 RefPtr<…,
CompactPtrTraits, FiberAwareRefDerefTraits> storage against a future
HAVE(36BIT_ADDRESS) CompactPtr that would truncate the word.

Intermediate perf commit — landed AFTER the Jul 21 00:41 bin/jsc build, so
the /tmp/fix3-pertest3.log table below reflects commits G+H+I only (this
header change not yet measured; rebuild+re-bench pending):

                      baseline    patched    delta
  regexp               373.971    362.040   -3.19%
  pdfjs                163.187    143.906  -11.82%
  Box2D                366.993    360.594   -1.74%
  Babylon              581.345    525.743   -9.56%
  richards             754.171    768.074   +1.84%
  delta-blue           981.205    865.397  -11.80%
  uglify-js-wtb         34.518     34.501   -0.05%
  typescript            17.665     16.464   -6.80%
  acorn-wtb             50.430     53.378   +5.85%
  raytrace             654.686    653.906   -0.12%
  splay                366.337    348.544   -4.86%
  crypto              1173.273   1203.187   +2.55%
  geomean              257.089    248.174   -3.47%

JetStream2 3x2 full: baseline median 200.169, patched 192.956 (-3.60%, target +2% NOT MET).
seq-crash-survey: CRASH_COUNT=0 — /tmp/fix3-survey.log (pre-header-change binary)
8f5292b tag-branch, JIT mirrors in lockstep

fix-uidhash-cost Option A. Reverts 8f5292b's Option-B tag-branch
(real→impl->hash(), fiber→intHash) back to the unconditional branch-free
Fibonacci mix `(bits * 0x9E3779B97F4A7C15) >> 32` across C++ and every JIT
mirror. D.4 single-rep already guarantees one pointer-word per content, so
hashing the raw word is sound for both real AtomStringImpl* and fiber words
— no tag test needed.

C++ (InlinePropertyKey.h uidHash): one imul + shr; no conditional, no deref.
JIT mirrors: AssemblyHelpers load/store/hasMegamorphicProperty dynamic-uid
path, DFG SpeculativeJIT64::compile HasOwnProperty, FTL LowerDFGToB3
compileHasOwnProperty — all emit mov+mul64+urshift64 with no
branchIfInlineStringImpl. Static-uid path (if (uid) add32(uidHash(uid),…))
calls C++ uidHash() directly so is automatically in lockstep.
DFGSpeculativeJIT32_64 already calls operationInlinePropertyKeyHash → C++
uidHash(), unchanged and in lockstep.

enableIdentifierFiberWords gate INTENTIONALLY dropped at the hash sites:
Option A is correct for both reps, and C++ uidHash() is not gated either, so
gating only the JIT side would desync probes when =false. The killswitch is
NOT a standalone perf escape hatch: =false alone measured Total 188.004 vs
baseline median 200.169 (−6.08%, /tmp/killswitch-survey2.log); documented in
the InlinePropertyKey.h flag comment so synthesize-measure-commit does not
fall back to it.

Motivating data (Option-B stack, /tmp/fix3-pertest3.log 3× interleaved):
  Babylon −9.56%  pdfjs −11.82%  delta-blue −11.80%  geomean −3.47%
  3×2 full (/tmp/fix3-bench3x2.log): patched 192.956 vs baseline 200.169 (−3.60%).

Top-function delta (hop-bench micro, hasOwnProperty ×5M, JIT probe path,
/tmp/fix4-bench3x2.log step [3/4] — C++/JIT hash-agreement smoke):
  baseline 119ms   option-B (fix3 stack) 128ms   option-A (this) 127ms
  → lockstep holds (sum=5000000 both, no probe miss storm).

Per-test % (3× interleaved, /tmp/fix4-pertest.log, baseline=/tmp/jsc-baseline,
patched=bin/jsc Jul 21 01:57 = 29681d0 + this + jsOwnedAtomBackedString):
                      baseline    patched    delta
  regexp               388.630    385.199   -0.88%
  pdfjs                162.998    158.999   -2.45%
  Box2D                359.341    376.110   +4.67%
  Babylon              596.838    585.283   -1.94%
  richards             730.111    752.109   +3.01%
  delta-blue           934.965    975.219   +4.31%
  uglify-js-wtb         34.736     33.987   -2.16%
  typescript            17.984     18.122   +0.77%
  acorn-wtb             51.979     54.974   +5.76%
  raytrace             656.128    671.440   +2.33%
  splay                379.179    317.602  -16.24%
  crypto              1132.629   1238.718   +9.37%
  geomean              257.468    258.359   +0.35%
vs option-B stack geomean 248.174: +4.10% recovery; Babylon +7.6pp, pdfjs
+9.4pp, delta-blue +16.1pp vs option-B table above.

JetStream2 3×2 full (/tmp/fix4-bench3x2.log):
  baseline 198.088/214.800/207.601  → median 207.601
  patched  206.830/208.305/187.980  → median 206.830  (-0.37%, target +2% NOT MET)
  Patched runs 1–2 (206.830/208.305) match 912892c's +5.17% win (206.259);
  run 3 (187.980) and baseline run 2 (214.800) are shared-machine load
  outliers — concurrent subagent ninja + PCH rebuild raced this round, and
  /tmp/fix4-pertest2.log re-sample shows the same sub-tests swinging ±13%
  (crypto +9.37%→−3.93%, delta-blue +4.31%→−4.49%) between back-to-back runs.
seq-crash-survey: CRASH_COUNT=0 (17/17 + full-suite 201.093) — /tmp/fix4-survey.log
…ps by-value string() temporary for real-impl uids (splay/raytrace protected-win recover)

Follow-up to 4d8503d (commit I). After 29681d0 made
Identifier::string() return AtomString by value, the three
getPropertyKeys sites' `jsOwnedString(vm, identifier.string())` pay a
fresh AtomString construct + destruct (one atomic ref/deref pair) per
enumerated key. Object.keys / getOwnPropertyNames fire once per node in
splay and per shape in raytrace — both are v1-protected wins the spec
says must not regress, and /tmp/fix3-pertest3.log shows splay −4.86% /
raytrace −0.12% under the by-value round-trip.

JSString.h: new ALWAYS_INLINE jsOwnedAtomBackedString(VM&, const
Identifier&). Real-impl uid (common, len>5 or symbol-free atom): go
straight to JSString::createHasOtherOwner(vm, *uid) — the Identifier's
m_bits already holds the ref, so no AtomString temporary. Fiber word
(rare, 2..5 chars): materialize via identifier.string() and hand to
jsOwnedString; createHasOtherOwner's Ref<StringImpl>&& arg refs the
freshly-atomized impl so the JSString owns it after the temporary dies.
Always atom-backed → DFG StringCharAt/substring/length are safe, unlike
jsStringFromFiberOrImpl's inline-fiber cell (the 4d8503d crash mode).

ObjectConstructor.cpp getPropertyKeys: route all three push sites
(contiguous-buffer fill, StringPropertiesMode pushDirect,
StringAndSymbolPropertiesMode pushDirect) through the helper under
#if USE(BUN_JSC_ADDITIONS).

Top-function delta: no gdb-sampling profile diff run for this helper in
isolation (it sits behind the same createHasOtherOwner frame as
jsOwnedString; pmp-out/report.txt is pre-dates this stack). Micro-level
effect is one atomic ref+deref elided per key — not a top-N leaf sample.

Per-test % (3× interleaved, /tmp/fix4-pertest.log, baseline=/tmp/jsc-baseline,
patched=bin/jsc Jul 21 01:57 = 29681d0 + option-A + this):
                      baseline    patched    delta
  regexp               388.630    385.199   -0.88%
  pdfjs                162.998    158.999   -2.45%
  Box2D                359.341    376.110   +4.67%
  Babylon              596.838    585.283   -1.94%
  richards             730.111    752.109   +3.01%
  delta-blue           934.965    975.219   +4.31%
  uglify-js-wtb         34.736     33.987   -2.16%
  typescript            17.984     18.122   +0.77%
  acorn-wtb             51.979     54.974   +5.76%
  raytrace             656.128    671.440   +2.33%  ← protected v1 win, recovered
  splay                379.179    317.602  -16.24%  ← protected v1 win, NOT recovered
  crypto              1132.629   1238.718   +9.37%  ← protected v1 win, holds
  geomean              257.468    258.359   +0.35%
splay re-sample (/tmp/fix4-pertest2.log): 342.115→321.756 −5.95% — the −16.24%
above is a noise spike (baseline splay itself swung 342→379, ±10.8%, between
back-to-back runs on this shared-load machine).

JetStream2 3×2 full (/tmp/fix4-bench3x2.log):
  baseline median 207.601  patched median 206.830  (-0.37%, target +2% NOT MET)
  runs: baseline 198.088/214.800/207.601  patched 206.830/208.305/187.980
seq-crash-survey: CRASH_COUNT=0 (17/17 + full-suite 201.093) — /tmp/fix4-survey.log
…or via publicName() (fiber-word uid() deref on Bun inspect Response.prototype.body getter mismatch)
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.

1 participant