Skip to content

chore: bulk-write outgoing CRDT payload across the JS boundary#8969

Open
pravusjif wants to merge 1 commit into
devfrom
opti/crdt-js-boundary-bulk-write
Open

chore: bulk-write outgoing CRDT payload across the JS boundary#8969
pravusjif wants to merge 1 commit into
devfrom
opti/crdt-js-boundary-bulk-write

Conversation

@pravusjif

Copy link
Copy Markdown
Member

What

Fixes the two highest-impact findings of the CRDT pipeline audit (series: #8967, #8968), on the renderer→scene direction of the JS boundary:

  • Per-byte marshaling. EngineApi.js did new Uint8Array(UnityEngineApi.CrdtSendToRenderer(...)). The returned PoolableByteArray is an IV8FastHostObject whose only operation is an enumerator factory, so the Uint8Array constructor fell back to the iterator protocol — one MoveNext() + GetCurrent() host transition per byte. The inbound direction was already a single bulk memcpy; outbound now is too: the wrapper copies the payload into a script-owned Uint8Array via IJsOperations.NewUint8Array + WriteBytes (the exact pattern already used by ClientWebSocketApiImplementation and the comms controller).
  • Never-returned pool buffers. The PoolableByteArray handed to JS was never disposed (ClearScript doesn't dispose collected host objects), so SharedPoolsProvider.SERIALIZED_STATE_BYTES_POOL never got an array back and every tick of every scene allocated a fresh byte[] of outgoing-payload size. The wrapper now disposes the pooled array deterministically right after the bulk copy.
  • Empty results (the common case for idle scenes) now return null and the JS module maps it to data: [] — no JS-side allocation at all, and the SDK's no-op onmessage(emptyArray) round-trip disappears.

Touches: EngineApiWrapper (+ SDK-observables variant ctor), ISceneRuntime/SceneRuntimeImpl (exposes JsOperations for wrapper construction), EngineApi.js. The IEngineApi implementation layer is unchanged.

Why

Per-byte fast-proxy transitions cost microseconds per KB on every scene tick, and tens of milliseconds for CrdtGetState at scene load (full state, potentially hundreds of KB). The pool leak added steady-state GC pressure proportional to outgoing traffic across all loaded scenes.

QA / verification

  • All assemblies + DCL.EditMode.Tests compile clean locally (dotnet build, 0 errors).
  • SceneRuntimeShould.EngineApi_CrdtSendToRenderer / ..._CrdtGetState execute the real V8 path end-to-end in the EditMode suite (CI) — they cover the new null/empty contract.
  • ⚠️ Manual verification requested before merge (boundary change, unit tests can't fully prove V8 interop behavior):
    1. Load Genesis Plaza — verify scene loads and is interactive (exercises CrdtGetState with a large payload).
    2. Any scene with raycasts/pointer events (e.g. test parcel 73,-2) — verify pointer interactions work (exercises per-tick outgoing data).
    3. SDK observables scene (onEnterScene/onSceneReady) — verify events still fire (exercises the SDK-observables wrapper variant).
    4. Profiler: OutgoingMessages/CrtdGetState samplers should show reduced time; SERIALIZED_STATE_BYTES_POOL no longer churns allocations (GC Alloc column in the scene tick).

🤖 Generated with Claude Code

The outgoing CRDT payload was marshaled to V8 one byte at a time (PoolableByteArray
fast-proxy enumerator consumed by new Uint8Array(...)), and the pooled array handed
to JS was never disposed so the shared serialization pool never got its arrays back
(a fresh byte[] allocation per scene tick).

The wrapper now copies the payload into a script-owned Uint8Array with a single
WriteBytes and returns the pooled array deterministically; empty results return null
so idle ticks allocate nothing on the JS side.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@pravusjif
pravusjif requested review from a team as code owners June 12, 2026 01:26
@github-actions
github-actions Bot requested a review from DafGreco June 12, 2026 01:26
@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

badge

New build in progress, come back later!

@github-actions
github-actions Bot requested review from biotech77 and dalkia June 12, 2026 01:26
@pravusjif pravusjif changed the title opti: bulk-write outgoing CRDT payload across the JS boundary chore: bulk-write outgoing CRDT payload across the JS boundary Jun 12, 2026
@pravusjif pravusjif added enhancement Enhancement of an existing feature optimization Something isn't optimal. Lets optimize it! force-build Used to trigger a build on draft PR labels Jun 12, 2026
@pravusjif pravusjif moved this from Todo to In Progress in Explorer Alpha Jun 12, 2026
@m3taphysics

Copy link
Copy Markdown
Contributor

PR #8969, run #27391756867

Builds: Windows change, Windows baseline, macOS change, macOS baseline

How to read this table
  • Each build is measured 3 times. The values are the median, and (min–max) is the lowest and highest of those runs — a wide range means the metric is noisy and small differences are not trustworthy.
  • Δ is Change minus Baseline (a negative Δ means Change is faster).
  • 🟢 faster / 🔴 slower — a real difference: larger than both 3% and the run-to-run range.
  • ⚪ within noise — the difference is smaller than how much the build varies between its own runs, so it cannot be told apart from random variation. Treat it as no change.
  • Exceptions logged — exceptions found in the run logs; more than the baseline is flagged 🔴 even when frame times look fine. The Exception breakdown under each table groups them by the explorer's report category and exception type.
  • A run that logged unusually many exceptions (at least 10 and 5× the median of its build's runs — e.g. a service was down during it) is excluded from all numbers and called out under the table.

Framework 13 i7

Metric Baseline Change Δ Result
Samples 2701 (×3) 2701 (×3)
CPU average 33.3 ms 33.3 ms -0.0 ms ⚪ within noise
CPU 1% worst 33.8 ms (33.7–34.0) 33.6 ms (33.5–33.7) -0.1 ms ⚪ within noise
CPU 0.1% worst 37.1 ms (36.0–37.2) 35.7 ms (34.5–35.9) -1.5 ms 🟢 4% faster
GPU average 7.5 ms (7.5–7.6) 7.9 ms (7.5–8.2) 0.4 ms ⚪ within noise
GPU 1% worst 19.7 ms (19.2–20.0) 19.1 ms (18.4–20.2) -0.5 ms ⚪ within noise
GPU 0.1% worst 24.8 ms (24.1–26.8) 24.1 ms (23.8–26.4) -0.7 ms ⚪ within noise
Exceptions logged 6 6 0 ⚪ none new
Exception breakdown
Exception Baseline Change
[ENGINE] NullReferenceException 6 6

@pravusjif pravusjif added the sdk label Jun 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

do not merge enhancement Enhancement of an existing feature force-build Used to trigger a build on draft PR optimization Something isn't optimal. Lets optimize it! sdk

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

3 participants