Repository navigation
VM startup performance fixes, -O3 Android release, and two upstream backports - #13
Merged
Merged
Conversation
CMake: HERMES_EXTRA_COMPILE_FLAGS / HERMES_EXTRA_LINK_FLAGS cache variables applied to every target (used for PGO). android/build.gradle.kts: release builds honor -PhermesReleaseCmakeBuildType (default MinSizeRel), -PhermesHeapHvMode and -PhermesExtraCmakeArgs='-Da=b|-Dc=d'.
Deleting or redefining a property of a dictionary object switched it to no-cache mode from the second change on, so every property-cache lookup on it missed. React Native's lazy globals redefine the global object 13 times at startup, so every read of Object, Array, Math, BigInt, HermesInternal... missed (~325k slow lookups per cold start). Give it a new cacheable class up to 15 times (4 spare ClassFlags bits) before falling back.
obj[key] / key in obj / hasOwnProperty with a non-interned key string (e.g. an id taken from JSON) re-hashed the string and probed the identifier table every time. A 256-entry direct-mapped cache keyed by the string's address, cleared at every GC start and before symbols are freed. 68% hit rate on device.
For {...src} into a fresh object, when src is a plain object whose properties all have default flags and string keys (no accessors, not a dictionary, same prototype), share src's hidden class and copy the slots in bulk. Observably identical to CopyDataProperties. 2.6-4x faster on the host.
Follow an existing hidden-class add transition for object keys (an existing transition proves the key is absent), append array elements directly and set length once, and lex ASCII input in place instead of widening it to UTF-16 (integers of up to 15 digits skip strtod). Encodes the value before taking raw class/object pointers: encoding can allocate, and holding them across it was a GC hazard that crashed 32-bit ARM.
…leted entries" (meta#2114) Summary: `IdentifierHashTable` capacity grows without bound when its' nonEmpty entries count is nearing the capacity. However, this also happens when most of the entries are already invalid. This PR makes the table rehash in place when the table is dominated by invalid entries (more than 75% entries are invalid). ~30-line standalone script, no dependencies — run with the `hermes` CLI and watch process RSS: ```js const sink = {}; let interned = 0; for (let round = 0; round < 240; round++) { for (let j = 0; j < 200000; j++) { const s = "unique_key_" + interned++ + "_suffix_padding"; if (s in sink) throw new Error("unreachable"); const garbage = { a: j, b: [j, j + 1], c: "g" + (j & 1023) }; // drive GC if (garbage.a === -1) print(garbage.b, garbage.c); } const st = HermesInternal.getInstrumentedStats(); print(round, st.js_heapSize ?? st.heapSize, st.js_numGCs ?? st.numGCs); } ``` 48M unique keys, macOS host CLI built from the same source, RSS sampled at 0.5 s: | binary | RSS peak | RSS final | JS heap (self-reported) | |---|---|---|---| | before | 223 MB | 159 MB | 12.6 MB flat, 3908 GCs | | after | 68 MB | **36 MB** | 12.6 MB flat, 3916 GCs | Real-world instance: a React Native app animating interpolated color strings (a library probes a color-keyword dictionary with each animated `hsl(...)` string ≈ 4k unique interned strings/s) grew from 81 MB to 333 MB over 6 hours through seven table doublings (8→16→…→256 MB tables).S Pull Request resolved: meta#2114 Test Plan: See description Reviewed By: lavenzg Differential Revision: D113932368 Pulled By: tmikov fbshipit-source-id: 05f51c60fe1c4bd17637f264e84bfadc10784c49 (cherry picked from commit 859499d)
… elements (meta#2239) Summary: Fixes meta#2238. `HBCISel::generateAllocArrayInst` caps the `NewArray` size hint at `UINT16_MAX`, so a large array literal gets storage for 65,535 elements (indices 0–65534). `generateDefineOwnInDenseArrayInst` still emitted the unchecked `DefineOwnInDenseArrayL` for `index == UINT16_MAX`, which stores one slot past the `ArrayStorage` cell into the header of the next heap cell. In release builds that is either silent data loss (`a[65535]` ends up `undefined`) or a later crash when the clobbered cell is used; debug builds assert in `unsafeSetExistingElementAt`. This changes the condition to `index < UINT16_MAX`, so element 65535 takes the growing `DefineOwnByIndexL` path like elements 65536+ already do. The emitted bytecode for a 24 MB React Native bundle is otherwise identical. Pull Request resolved: meta#2239 Test Plan: - New lit test `test/hermes/array-literal-over-65535-elements.js` builds a 65,537-element literal through `eval` and checks `a.length, a[65534], a[65535], a[65536]`. Before: `65537 1 undefined 1`. After: `65537 1 1 1`. - Same literal precompiled with `hermesc -O -emit-binary` and run on an unmodified VM: `DefineOwnByIndexL r3, r2, 65535` is emitted and the program prints `65537 1 1 1`. - A debug `hermes` build no longer hits `Assertion failed: (index >= self->beginIndex_ && index < self->getEndIndex() && "array index out of range")` on the repro. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Reviewed By: avp Differential Revision: D124887906 Pulled By: lavenzg fbshipit-source-id: b3ea4634de0dda8b4975453dce8411c7d1832246 (cherry picked from commit eefe5da) (cherry picked from commit 2e1397b)
Release builds use CMAKE_BUILD_TYPE=Release unless -PhermesReleaseCmakeBuildType overrides it, instead of upstream's MinSizeRel (-Os). Trades library size for faster interpreter and VM code on Android.
Apply the weak-reference read barrier on string-to-symbol cache hits, as every other read of a symbol from a weak table does, so a hit during concurrent marking keeps the symbol alive instead of relying on the cache being cleared at the start of every marking cycle. Tie the dictionary re-class limit to the width of its 4-bit counter with a static_assert, so raising the limit past 15 fails to compile instead of wrapping the counter. Remove an unused pinned local from JSON.parse's array path.
Flewp
marked this pull request as ready for review
November 5, 2026 19:09
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Startup and runtime performance fixes for the 250829098.0.15 line, plus two upstream correctness backports. No bytecode-format or JSI changes, so existing
hermescoutput and RN integration are unaffected.VM performance (AJ Palkovic):
Object,Array,Mathand similar missed the property cache. Allows up to 15 cacheable re-classings first.obj[key]with a non-interned key string re-hashed it on every access. Adds a 256-entry direct-mapped cache, cleared at GC start and before symbols are freed.{...src}into a fresh object shares the source's hidden class and copies slots in bulk, when the source is a plain object of default-flag, string-keyed data properties with the same prototype.Defaults:
CMAKE_BUILD_TYPE=Release(-O3) instead ofMinSizeRel(-Os), overridable with-PhermesReleaseCmakeBuildType. iOS is unchanged (the Apple script still usesMinSizeRel).Hardening (follow-up to review):
static_asserttying the re-class limit to its 4-bit counter.parseArray.Upstream backports (
cherry-pick -x):859499d4f22e19cb4bbb0973ae76309d7a9865fc: IdentifierHashTable growing indefinitely with deleted entries (facebook/hermes#2114).2e1397b4f886711ebdc92accfdfc4fa22f5cef06: out-of-bounds dense store for array literals with more than 65535 elements (facebook/hermes#2238).Base:
3e1261f2a7bc50ffe52f8354868e78e4e74bb0c8(250829098.0.15-discord-4).Testing
Local, macOS, Release builds of base and candidate:
check-hermes(lit): the candidate fails nothing that base passes. The one failure on both,hermes/truncate-long-stack.js, is environmental: its finalCHECK-NEXT: endmatches the substring "end" inside the checkout path on theat global (...)line.Object.assigned (accessors, symbols, index-like keys, non-enumerable, frozen/sealed/non-extensible, dictionary-mode and null/foreign prototypes), 40 rounds of global-object redefinition and deletion, and JSON-derived computed keys with deletes across forced GCs.-gc-sanitize-handles=1(heap moved on every allocation): dictionary re-class and JSON fast-path cases.0.05: spread clone and the object-model fuzzer.0.002with four random seeds each: the 20k-input JSON fuzz and the string-cache GC stress. The JSON fuzz output is byte-identical to the base Release build for every seed.CI on this branch covers test262 and lit on macOS, Linux x64, Linux arm64 and ARMv7. The forced-JIT macOS job fails on the base branch as well.
Deferred review notes
forEachPropertyWhile, which may rebuild a property map that a later add-transition moved away. Repeated{...state, x}on the same class could allocate more than the slow path. Worth measuring, and worth caching an "all default flags" bit on the class.JSON.parseencodes each value before knowing the fast path applies. On boxed-double builds this can allocate an unusedBoxedDoubleon the slow path. Reordering needs care, because encoding before taking raw pointers is what fixed the 32-bit GC hazard.HiddenClass.h,JSObject.h).