Skip to content

Default Interop.CacheRecentObjectWrappers to true for 4.14#2734

Merged
lahma merged 1 commit into
sebastienros:mainfrom
lahma:perf/recent-wrapper-cache-default
Jul 22, 2026
Merged

Default Interop.CacheRecentObjectWrappers to true for 4.14#2734
lahma merged 1 commit into
sebastienros:mainfrom
lahma:perf/recent-wrapper-cache-default

Conversation

@lahma

@lahma lahma commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Closes #2729.

With LiveView as the array default (#2728) and the wrapper-construction costs removed (#2730, #2731), the dominant remaining cost of a collection member being re-read from script was wrapper re-creation per crossing. This flips Options.Interop.CacheRecentObjectWrappers to true: a small bounded ring (8 entries, per engine, strong references — cannot grow unbounded) makes repeated crossings of the same object reuse its wrapper by default. TrackObjectWrapperIdentity remains the opt-in for full identity persistence; setting CacheRecentObjectWrappers = false restores fresh-wrapper-per-crossing.

Latent cache bug fixed along the way (it also affects the existing opt-in flags): both identity-cache lookups ignored the exposed CLR type, so an object crossing once as its concrete type and later as an explicit interface/superclass view (holder.i1 vs holder.ci1) got the first wrapper back — resolving the wrong member set. Seven existing InteropExplicitTypeTests caught this the moment the cache was on by default. Cache hits now require the wrapper's ClrType to match the exposed type; the ring can hold both views of the same object, and the identity map replaces its entry when the view changes. A regression test exercises alternating views against the populated cache.

Observable changes (and their opt-outs):

  • Wrapper reference identity is now stable for recently-crossed objects, so script-attached state (Object.freeze, defineProperty, expandos) survives crossings. (=== was already true — wrapper equality compares targets.)
  • Under explicit ArrayConversionMode.Copy, repeated reads of the same CLR array now reuse the first JsArray snapshot while cached — CLR-side mutations are not re-copied. Full pre-4.14 semantics = Copy + CacheRecentObjectWrappers = false; the option XML doc spells this out and the snapshot-contract tests migrated to the explicit opt-out, mirroring the Default Interop.ArrayConversion to LiveView for 4.14 #2728 test migration.

Benchmarks (same machine, adjacent same-base A/B vs main@fed894d43, default job)

InteropWrapperChurnBenchmark:

Row main PR delta
ArrayMemberTraversal_DefaultLiveView 19.35 µs / 13,944 B 15.27 µs / 344 B −21% / −97.5%
ArrayMemberTraversal_LiveViewNoCache (new opt-out lane) 19.51 µs / 13,944 B reproduces the old default
SameObjectReturnedInLoop_DefaultFlag 2.58 µs / 2,336 B 2.45 µs / 2,016 B wrapper reused
ArrayHoistedTraversal / Copy lanes flat / allocation-identical

EngineComparisonInteropBenchmark (Jint lane):

Row main PR delta
interop-collection-traversal 2,288 µs / 1,659 KB 1,501 µs / 332 KB −34% / −80%
interop-method-calls / property-access / string-passing allocation byte-identical, time within run-to-run wobble

Cumulative on this row across the 4.14 interop work (#2728#2730/#2731 → this PR): 15.6 ms / 32.9 MB → 1.50 ms / 0.33 MB.

Gates: full Jint.Tests (net10.0 + net472), Jint.Tests.PublicInterface, Test262 99,431 passed / 0 failed.

🤖 Generated with Claude Code

Closes sebastienros#2729. With LiveView as the array default, wrapper re-creation per
crossing was the dominant remaining interop cost; the bounded recent-wrapper
ring (8 entries, per engine) makes repeated crossings of the same object
reuse its wrapper by default while staying memory-safe.

Flipping the default surfaced a latent cache bug that also affected the
opt-in flags: lookups ignored the exposed CLR type, so an object crossing
both as its concrete type and as an explicit interface/superclass view got
the wrong wrapper back. Cache hits now require the wrapper's ClrType to
match the exposed type; the ring can hold both views of one object, and the
identity map replaces its entry when the view changes.

Copy-mode tests that assert fresh-snapshot-per-crossing semantics opt out of
the cache explicitly, mirroring the sebastienros#2728 migration; the option XML doc
spells out how to restore the full pre-4.14 behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lahma
lahma enabled auto-merge (squash) July 22, 2026 08:34
@lahma
lahma merged commit 73da060 into sebastienros:main Jul 22, 2026
4 checks passed
@lahma
lahma deleted the perf/recent-wrapper-cache-default branch July 22, 2026 08:42
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.

Proposal: default Interop.CacheRecentObjectWrappers to true for 4.14

1 participant