# Factory and pooling verification (2026-09-23)

- Target: Unity 2022.3.62f3, Windows Editor/Mono, Onity checkout on this machine.
- Correctness baseline: four new EditMode cases failed (disposed use and get-hook failure in each pool); the active-prefab first-acquisition PlayMode case failed because `OnEnable` ran twice before the parameter was ready.
- Fix: both pools return an item when its get hook throws and reject operations after disposal. Prefab clones are created under an inactive parent, then configured before first activation. Prewarm reserves its known backing capacity. Managed pools omit the Unity release callback when no release hook exists.
- Verification: focused EditMode pool tests passed 15/15; focused PlayMode first-activation test passed 1/1. A `GC.Alloc` recorder with a positive allocation control saw zero allocation events during warmed managed pool and 0/1/2-parameter pooled-factory rent/return, and during warmed prefab-pool rent/return.
- Timing: explicit Editor/Mono test, five samples of 250,000 warmed rent/return pairs, median shown. One run measured raw Unity `ObjectPool<T>` at 107.4 ns/pair and Onity at 139.5 ns/pair; a repeat after skipping a no-op hook measured 100.4 and 152.5 ns/pair. The pooled factory variants ranged from 141.0 to 163.8 ns/pair across those runs. The variation does not establish a speed improvement from the hook change. These are local diagnostic snapshots, not an independent benchmark or evidence that Onity pooling beats Zenject or VContainer.
- Comparison status: later sections record Onity and Unity pool/factory diagnostics in IL2CPP. Unity 2022.3 omits its collection check in players, so the older IL2CPP runs used unchecked return behavior despite requesting `collectionCheck: true`. Equivalent Zenject factory/pool and VContainer factory scenarios were not measured in those runs.

## Follow-up speed experiment (2026-09-24)

- The explicit EditMode benchmark now interleaves eight warmed pool/factory paths over 11 samples of 250,000 rent/return pairs. It includes a raw Unity pool with the same two atomic diagnostic increments and direct parameterized Onity pool controls.
- Two unchanged-code baseline runs measured Onity/raw paired median ratios of 1.23x and 1.23x; Onity/raw-plus-atomics ratios were 1.12x and 1.25x. Absolute Onity pool medians were 122.3 and 133.0 ns/pair.
- Moving the optional get-hook exception handler into a cold helper produced candidate Onity/raw ratios of 1.23x and 1.20x, and Onity/raw-plus-atomics ratios of 1.21x and 1.13x. Absolute Onity pool medians were 127.0 and 118.5 ns/pair. This did not establish a repeatable improvement beyond baseline variation, so the runtime experiment was reverted.
- The fixed-pool hook-failure test now checks that rollback skips the release hook and leaves successful-operation diagnostics unchanged; all 15 focused EditMode pooling tests passed. The warmed allocation tests, including a positive control, remain in that suite.
- Raw Unity pooling is a lower-cost reference without Onity's disposal, fixed-capacity, hook, and diagnostics contract. These Editor/Mono measurements cannot establish a Zenject or IL2CPP pooling speed claim.

## Zenject pool comparison (2026-09-24)

- Added an explicit EditMode comparison against the repository's Zenject `MemoryPool<T>` reference. Reflection configures Zenject only before timing; all three timed paths use typed `Func<T>` and `Action<T>` delegates. Each pool starts with one inactive item, a maximum of 16, and no user hooks. Onity with `collectionCheck: true` is the primary contract comparison because Zenject checks for duplicate returns; Onity's default mode is also reported.
- Three runs of the same timed loop used 11 interleaved samples of 250,000 warmed rent/return pairs; the third added diagnostics outside timing. Median ns/pair, in run order: Onity checked **166.0, 138.8, 132.2**; Zenject **171.0, 139.1, 123.5**. The paired checked/Zenject median ratios were **0.97x, 1.00x, 1.067x**. The third run's paired-ratio Q1-Q3 was 1.056-1.081. The direction changed between runs, so this Editor case does not show a reproducible speed advantage for either pool.
- Onity default median ns/pair was **170.4, 134.2, 127.6**. Its paired ratios to Zenject were **0.95x, 0.97x, 1.019x**; this is a separate, less equivalent contract setting. In the third run `Profiler.enabled` was false and Zenject's `ProfileBlock.UnityMainThread` did not match the current thread, so its Editor profiling wrapper did not start samples. The first two runs did not record this state. All three pools reported zero warmed `GC.Alloc` events over 64 pairs, with a positive allocation control. The final run also verified that each pool had zero active and one inactive item after timing.
- The test only exercises one repeatedly reused managed item in a hookless pool. It does not establish relative speed for multiple inactive items, prefab pools, parameterized factories, or IL2CPP. Raw NUnit outputs: `onity-pooling-zenject-1.xml`, `onity-pooling-zenject-2.xml`, and `onity-pooling-zenject-3.xml` in this directory.

## Checked Unity pool control (2026-09-24)

- A separate explicit EditMode benchmark compares `OnityObjectPool<T>` with `collectionCheck: true` against Unity `ObjectPool<T>` with the same check, one prewarmed item, capacity 16, no hooks, and the same typed delegate call layer. The primary Unity control adds the two `Interlocked.Increment` operations used by Onity diagnostics. A plain checked Unity pool is also timed as a lower-cost reference.
- Two fresh processes each measured 11 interleaved samples of 250,000 rent/return pairs. Onity checked / Unity checked plus atomics paired medians were **1.317x** (Q1-Q3 **1.296-1.332**) and **1.183x** (Q1-Q3 **1.159-1.203**). Absolute medians were Onity **133.5/134.7 ns/pair** and the matched Unity control **102.2/114.1 ns/pair**. The difference exceeded the preselected 10% wrapper-cost investigation threshold in both runs, while the size varied with the reference timing.
- All three warmed paths recorded zero `GC.Alloc` events over 64 pairs, with a positive allocation control. `Profiler.enabled` was false, and final counts confirmed one inactive item and no active item. This isolates an Editor/Mono wrapper cost; it is not a Zenject or IL2CPP speed result. Raw NUnit outputs: `onity-pooling-unity-checked-1.xml` and `onity-pooling-unity-checked-2.xml` in this directory.

### `CheckCapacity()` inlining experiment

- A single candidate added `AggressiveInlining` to `OnityObjectPool<T>.CheckCapacity()`; no condition, callback, counter, or exception behavior changed. In a nearby baseline-candidate-candidate-baseline sequence, the Onity/matched-Unity paired median ratios were **1.183x → 1.196x → 1.233x → 1.286x**. Each run used the same 11-sample harness. The candidate results lie within baseline variation and do not meet the preselected repeatable 5% gain gate.
- The annotation was removed. All four runs passed their allocation and final-state checks. This experiment leaves the production pool implementation unchanged. Raw NUnit outputs for the candidate and last baseline are `onity-pooling-unity-inline-1.xml`, `onity-pooling-unity-inline-2.xml`, and `onity-pooling-unity-checked-post.xml` in this directory.

## IL2CPP test-player transport attempt (2026-09-24)

- A focused Windows Standalone IL2CPP Development test player built successfully from the existing PlayMode assembly. The Unity Test Framework player launched, but no NUnit result XML or pool measurement was returned. Its log repeated a player-alive message; the Editor log reported a player-connection multicast error (`10013`). The exact result-transport failure is unresolved, so this attempt provides **no IL2CPP pool timing or allocation evidence**.
- The stalled test player and its Editor were stopped. The generated `InitTestScene*.unity` and `.meta` were removed. `ProjectSettings.asset` was restored from the pre-run backup to SHA-256 `93CC8177FCACAC2BF530346EF05658BF568483C0214EA9446AFA99C2E07E99F8` with no Git diff. A standalone report path in the same test-player assembly is the next measurement route; it will not rely on NUnit result transport.

## Standalone IL2CPP pool diagnostic, previously labeled checked (2026-09-24)

- A separate `BuildPipeline.BuildPlayer` entry point built a Windows Standalone Development IL2CPP player with test assemblies and the existing sample scene. It did not change the active target, scripting backend, scenes, or project settings. `RuntimeInitializeOnLoads.json` contains `Onity.Tests.PlayMode.OnityPoolBenchmarkPlayModeTests.RunFromCommandLine`; the player wrote JSON directly, without NUnit result transport. Both processes exited successfully and reported `status: passed`.
- The same binary was run twice, each with 11 interleaved samples of 250,000 warmed rent/return pairs. Both paths requested `collectionCheck: true`, but neither enforced it in the player. The paired Onity/Unity median ratios were **1.246x** (Q1-Q3 **1.203-1.262**) and **1.210x** (Q1-Q3 **1.189-1.214**). Absolute median ns/pair was Unity **13.469/14.232** and Onity **16.762/17.083**. This is an unchecked-behavior wrapper diagnostic and cannot be compared to the new checked implementation.
- The `GC.Alloc` positive control recorded 64 events in each process; both warmed paths recorded **0 events** over 64 pairs. The recorder was valid and did not wrap. Each process ended with zero active and one inactive item in each pool; Onity and the Unity control each completed 2,760,128 matched successful gets and returns. `Profiler.enabled` was false.
- Executable SHA-256: `8D4C804FF605213A298140BE3C172EB407E7F5DC78C5082CDFAABAEA7B43BA0E`; `GameAssembly.dll` SHA-256: `530E6352BF9E3DE36ED6C01D2C107ABC16A6A3D21132428AFA969FD2D47DE6C7`. Raw reports: [run 1](pool-checked-il2cpp-development-1-2026-09-24.json) and [run 2](pool-checked-il2cpp-development-2-2026-09-24.json). `ProjectSettings.asset` retained its original SHA-256 and no generated `Assets/Resources/PerformanceTestRun*.json` remained.
- This is Development IL2CPP standalone benchmark evidence, not a successful NUnit player run, a Zenject comparison, or Release-player speed evidence. The repeated gap exceeds the predeclared 10% investigation threshold; split `Get` and `Release` costs before selecting a runtime change.

## Four-arm IL2CPP pool diagnostic (2026-09-24)

- A second Development IL2CPP player compared four paths over each `OnityObjectPool<T>` instance's own Unity backend: U/U (inner Unity get/release plus external counters), O/U (Onity get, inner Unity release plus external counter), U/O (inner Unity get plus external counter, Onity release), and O/O (Onity get/release). Reflection fetched the private backend only during setup; the timed paths used typed delegates. Each path had two atomic counter increments and one prewarmed item. Unity's requested collection check was absent in this player, so all four paths had unchecked return behavior.
- Two processes ran the same `GameAssembly.dll` (SHA-256 `9D758D1592C9F844CC85C643B14D295F4499DA5A16AD516AF1553EC153526D6C`) with 16 rotated rounds of 250,000 pairs per arm. O/O over U/U paired median was **1.210x** (Q1-Q3 **1.193-1.218**) and **1.204x** (Q1-Q3 **1.174-1.255**), reproducing the earlier aggregate gap. Arm median ns/pair in run order: U/U **14.238/14.005**, O/U **16.404/15.911**, U/O **15.109/15.700**, O/O **17.134/16.947**.
- Conditional replacement-delta medians in ns/pair were: Get with Unity release **2.259/2.130**, Get with Onity release **1.841/1.343**, Release with Unity get **1.009/1.682**, and Release with Onity get **0.617/0.815**. The interaction median was **-0.804/-0.830 ns**; its Q1-Q3 ranges were **-1.409 to 0.267** and **-1.198 to 0.019**. These are whole-pair replacement effects that also include counter placement and delegate code, not direct method durations. The interaction and spread do not justify attributing the total cost to one method, so no production micro optimization was retained.
- The positive allocation control recorded 64 events in each process; every warmed arm recorded **0 events**. All four arms ended with one inactive item, zero active items, and 4,010,128 matched successful gets and returns after combining internal and external counters. Raw reports: [run 1](pool-checked-il2cpp-split-1-2026-09-24.json) and [run 2](pool-checked-il2cpp-split-2-2026-09-24.json). This remains Development IL2CPP diagnostic evidence, not Release or Zenject speed evidence.

## Next declared factory workload

- After the single-item IL2CPP pool diagnostic, measure a burst of 32 simultaneously rented managed projectiles through the existing two-parameter pooled factory. Prewarm 32 distinct items, use a fixed maximum of 32, and preallocate the holding array and callbacks. The plan requested checked returns, but the older player runs below did not enforce them. Every rent writes changing `Damage` and `OwnerId` values; every return runs the same reset callback. Time the complete rent, configure, checksum, and return burst with a fixed return order.
- First compare `PooledFactory<int, int, T>.Create` with direct `OnityObjectPool<T>.Get(param1, param2, initialize)` in the same burst to isolate the factory layer. A checked Unity `ObjectPool` with equivalent callbacks and diagnostic increments is a secondary control. After that decision, add Zenject's two-parameter `MemoryPool` to the same harness for a product comparison; record its effective assertion and duplicate-return-check settings. A custom VContainer pool adapter is outside this comparison because this repository has no native VContainer pool.
- Before timing, verify 32 distinct objects, correct parameters and one reset per return; a 33rd outstanding rent must fail and a new rent must succeed after one return. After timing, verify no new objects, zero active items, 32 inactive items, matched successful rent/return counts, and matching checksums. Require a valid nonwrapped allocation recorder, a positive allocation control, and zero warmed `GC.Alloc` events.
- Use two fresh IL2CPP processes from the same build, each with 2,000 warmup bursts and 11 interleaved samples of 20,000 full bursts per arm. Retain raw paired samples, build/profiler metadata, and executable plus `GameAssembly.dll` hashes. If factory/direct is at most **1.05x** in both runs, stop factory-dispatch optimization; if it is above **1.10x** in both with consistent paired samples, inspect generated calls and test at most one evidence-backed candidate; intermediate or inconsistent results remain inconclusive. Retain a runtime optimization only when this workload improves by at least 5% in both processes without correctness or allocation regression, then confirm the before/after result in Release IL2CPP.

## 32-item factory burst IL2CPP diagnostic (2026-09-24)

- The declared first-stage workload ran in a Windows Standalone Development IL2CPP player. Each arm prewarmed 32 managed projectiles, acquired all 32 with changing `Damage` and `OwnerId`, accumulated a payload checksum, and returned them in reverse order. Factory and direct arms used identically configured `OnityObjectPool<T>` instances with fixed capacity; their requested collection check was absent in this player. The Unity arm likewise ran unchecked, with the same callbacks, a fixed-outstanding guard, and two external atomic diagnostic counters; its ratio is a secondary wrapper-cost reference.
- Two independent processes used the same player and 11 rotated samples of 20,000 bursts per arm. Median ns per 32-item burst in run order: Onity two-parameter factory **1,284.880/1,283.225**; direct Onity two-parameter pool **810.055/805.920**; Unity control **699.015/705.335**. The paired factory/direct median ratio was **1.592x** (Q1-Q3 **1.564-1.821**) and **1.574x** (Q1-Q3 **1.559-1.602**). This exceeds the predeclared 1.10x factory-call-path investigation threshold in both runs. Factory/Unity paired medians were **1.885x/1.803x**, but the Unity arm does not provide all Onity safety behavior.
- Every arm created exactly 32 objects, rejected the 33rd outstanding request before reaching its creator, reused a returned object with both parameters correct, and finished with zero active and 32 inactive objects. Per arm and process, 7,108,129 successful gets matched returns; 7,108,096 reset callbacks ran, one per item per measured/warmup burst; payload checksums matched the expected `26,845,067,974,144`. The `GC.Alloc` positive control recorded 64 events, while all three warmed arms recorded **0 events**. `Profiler.enabled` was false.
- Executable SHA-256: `8D4C804FF605213A298140BE3C172EB407E7F5DC78C5082CDFAABAEA7B43BA0E`; `GameAssembly.dll` SHA-256: `ADA791DD1DCD2C8E3928521FF15E5EC264569D75EDE3B8B5A4CF7EA1B6ED1C0E`. Raw reports: [run 1](pool-factory-burst-il2cpp-development-1-2026-09-24.json) and [run 2](pool-factory-burst-il2cpp-development-2-2026-09-24.json). `ProjectSettings.asset` retained its original SHA-256 with no Git diff and no generated `Assets/Resources/PerformanceTestRun*.json` remained.
- This diagnoses the current factory dispatch path in a Development player. It is not a Release before/after result or a Zenject comparison. Inspect generated IL2CPP calls before selecting at most one narrow runtime candidate; retain it only after the declared correctness, allocation, repeated-speed, and Release confirmation gates.

## Matched factory dispatch baselines (2026-09-24)

- Before changing the factory implementation, the shared 32-item harness was run twice in a new Development IL2CPP player and twice in a non-Development IL2CPP player. Both builds reported `Il2CppCompilerConfiguration.Release` and `Il2CppCodeGeneration.OptimizeSpeed`; Bee input recorded `ConfigurationName: Release`, with `Development: true` and `false` respectively. `Profiler.enabled` was false in every player run. Keep the two build modes separate when comparing a candidate.
- Development factory/direct paired medians were **1.580x** (Q1-Q3 **1.572-1.598**) and **1.568x** (**1.552-1.608**). Factory medians were **1,262.600/1,278.120 ns** per burst; direct medians were **803.060/813.350 ns**. The positive allocation control recorded 64 events, and all three warmed arms recorded **0 events** in both runs. Raw reports: [run 1](pool-factory-burst-baseline-il2cpp-development-1-2026-09-24.json), [run 2](pool-factory-burst-baseline-il2cpp-development-2-2026-09-24.json). `GameAssembly.dll` SHA-256: `650B85C8D6DA83FFEFA4F87AF896701231B53A5206F746E490A90FACA8574E6C`.
- Non-Development factory/direct paired medians were **1.572x** (Q1-Q3 **1.461-1.656**) and **1.605x** (**1.580-1.620**). Factory medians were **1,299.495/1,295.525 ns**; direct medians were **828.715/811.025 ns**. Allocation recording is unavailable in this mode and is marked `-1`, not zero. Raw reports: [run 1](pool-factory-burst-baseline-il2cpp-release-1-2026-09-24.json), [run 2](pool-factory-burst-baseline-il2cpp-release-2-2026-09-24.json). `GameAssembly.dll` SHA-256: `B2786E7BC04CD904DAE4C9F17BE400ECBEB7C3EB0AE0A8F2AEF7D3B6D9ED9841`.
- Every run passed the same 32-item uniqueness, fixed-capacity, parameter, reset, checksum, and final-state checks: 32 created per arm, zero active, 32 inactive, 7,108,129 successful gets matched with returns, and 7,108,096 reset callbacks. The non-Development player reports `developmentBuild: false`. `ProjectSettings.asset` retained its original SHA-256 `93CC8177FCACAC2BF530346EF05658BF568483C0214EA9446AFA99C2E07E99F8` without a Git diff.

## Cached two-parameter factory dispatch (2026-09-24)

- `PooledFactory<TParam1,TParam2,TValue>` now binds its typed pool `Get` method once in the constructor and calls the cached delegate in `Create`. The public API and custom `IParameterizedPool<T>` support remain unchanged. This adds one delegate allocation during factory construction while removing the repeated generic-interface lookup from the hot path. Generated IL2CPP in `Generics__180.cpp` resolves the interface method in the constructor and invokes the cached `Func` in `Create`.
- Candidate Development IL2CPP factory/direct paired medians were **1.013x** (Q1-Q3 **0.992-1.041**) and **0.983x** (**0.971-1.018**), a **35.9%/37.3%** reduction in the normalized ratio from the matched baselines. Factory median time fell to **823.095/795.660 ns** per 32-item burst; direct control remained **813.550/807.050 ns**. All warmed arms recorded **0 GC.Alloc events**, with 64 events in the positive control. Raw reports: [run 1](pool-factory-burst-candidate-il2cpp-development-1-2026-09-24.json), [run 2](pool-factory-burst-candidate-il2cpp-development-2-2026-09-24.json). `GameAssembly.dll` SHA-256: `743BF8CDEB8E2416DBF82CF9ED0A3FBC4A246627BA0E2685D9FAFC97CC806985`.
- Candidate non-Development IL2CPP factory/direct paired medians were **1.017x** (Q1-Q3 **0.998-1.033**) and **1.023x** (**1.001-1.055**), a **35.3%/36.2%** normalized reduction. Factory median time fell to **835.720/834.720 ns**; direct control remained **821.430/817.235 ns**. Allocation measurement is unavailable and marked `-1` in these reports. Raw reports: [run 1](pool-factory-burst-candidate-il2cpp-release-1-2026-09-24.json), [run 2](pool-factory-burst-candidate-il2cpp-release-2-2026-09-24.json). `GameAssembly.dll` SHA-256: `412900DB63B466AC83172B06C1AE7207E24ED1D83BCDFE9228DF4D6E4F0F0809`.
- All four candidate player runs passed the same 32-item uniqueness, parameter, capacity, recovery, checksum, and final-state checks as the baselines. The focused Unity EditMode pooling suite passed **20/20**, including explicit custom-interface dispatch, null arguments, initialization rollback, exhaustion, recovery, and disposed use. The candidate's Q3 paired ratio was below both corresponding baseline Q1 values in Development and non-Development players, so the narrow optimization was retained. The secondary Unity control drifted in one Development candidate run; these data support no Unity, Zenject, or VContainer speed claim for pooling/factory.
- The matched baseline/candidate speedup isolates factory dispatch under the older unchecked player behavior. It is valid for that workload, but its absolute times and allocation results are not evidence for the new checked pool implementation.

## Declared Zenject two-parameter pool comparison

- A separate player scenario will compare Onity two-parameter factory, direct Onity pool, unchecked Unity pool with external atomics, and Zenject `MemoryPool<int,int,int[]>`. All four arms use 32 prewarmed `int[3]` items, equal parameter/reset callbacks, fixed capacity, reverse-order returns, and the same checksum. The Unity arm is a secondary unchecked control, not a like-for-like checked speed comparison. Zenject is configured through a preserved typed bridge; a build-only `ONITY_BENCHMARKS` define must make that bridge available without changing `ProjectSettings.asset`.
- Before timing, every arm must prove distinct prewarmed items, parameter application, 33rd-acquisition rejection before its creator, return/reset/reuse, and zero active/32 inactive items. Onity and Zenject must also reject a duplicate return without state mutation; Unity's player pool has no such check, so the test does not make an invalid duplicate return on that arm. The player must verify the Zenject bridge assembly and protocol, not infer inclusion from build success. Development requires a working, nonwrapped `GC.Alloc` recorder with a positive control and zero warmed events in all four arms. Non-Development records allocation as unavailable.
- Use two fresh processes per Development and non-Development binary, with 2,000 warmup bursts and 12 rotated samples of 20,000 bursts per arm. Quartiles use the medians of the lower/upper six sorted samples. A **Release speed advantage in this workload** requires both non-Development factory/Zenject paired medians at most **0.95x** and both Q3 values below **1.0x**; the same gates in all four runs support a claim for both build modes. If both Release medians are at least **1.05x** with Q1 above **1.0x**, call this workload slower; otherwise report no clear advantage. Preserve raw samples, allocation/correctness state, and binary hashes. These quartiles are not confidence intervals and do not establish general performance.

### First player smoke result

- The Development IL2CPP player built with a build-only `ONITY_BENCHMARKS` define; Bee input included `Onity.Benchmarks.dll`, and `ProjectSettings.asset` stayed unchanged. Its first run stopped before timing because arm 0, `OnityObjectPool<int[]>` with `collectionCheck: true`, accepted a second return of the same item. The [failure report](pool-zenject-burst-il2cpp-development-smoke-failed-2026-09-24.json) contains no valid speed or allocation result. Unity 2022.3's pool only checks collections in the Editor; its player `Release` omits the check. Onity now tracks inactive references itself when checking is requested. The old IL2CPP "checked pool" filenames remain for provenance, but their timings had unchecked return behavior.

## Player-safe checked pool and Zenject comparison (2026-09-24)

- `OnityObjectPool<T>` now tracks inactive references when `collectionCheck: true`. It rejects a second return before Unity's release callback or Onity's counters change. Reference identity keeps distinct value-equal objects separate. The unchecked path retains its previous behavior. Unity's native pool check is disabled inside Onity to avoid duplicate Editor work. Focused Unity EditMode pooling tests passed **24/24**, including duplicate returns in partially/full pools, failed release retry, failed initialization recovery, overflow, clear, reference identity, and warmed zero-allocation checks. [NUnit result](pool-checked-fix-editmode-2026-09-24.xml).
- The separate four-arm `int[3]` player scenario used 32 prewarmed items and fixed capacity per arm, two runtime parameters, the same create/reset callbacks, reverse-order returns, 2,000 warmup bursts, and 12 rotated samples of 20,000 bursts. All four arms created 32 items, rejected a 33rd outstanding acquisition, applied parameters, reused an item, and finished at zero active/32 inactive. Onity factory, Onity direct, and Zenject rejected duplicate returns without changing callbacks or counts. The Unity arm is explicitly **unchecked** in players and is only a secondary control. `Profiler.enabled` was false. Zenject used a preserved typed bridge from `Onity.Benchmarks`, with assertions retained and internal profiling disabled; reflection ran only during setup.
- Two fresh Development IL2CPP processes measured Onity factory/Zenject paired medians **0.797x** (Q1-Q3 **0.790-0.810**) and **0.786x** (**0.777-0.800**). Factory medians were **1,953.615/1,968.178 ns** per 32-item burst versus Zenject **2,438.943/2,483.775 ns**. Direct Onity/Zenject paired medians were **0.809x/0.815x**. Each process recorded **64** positive-control `GC.Alloc` events and **0** warmed events in all four arms. [Run 1](pool-zenject-checked-il2cpp-development-1-2026-09-24.json), [run 2](pool-zenject-checked-il2cpp-development-2-2026-09-24.json). Player executable SHA-256: `8D4C804FF605213A298140BE3C172EB407E7F5DC78C5082CDFAABAEA7B43BA0E`; `GameAssembly.dll`: `B1CC1C2A575C26C1BB498A8FE85354F57F10753A28A3CD048186DE16B3D53A6D`.
- Two fresh non-Development IL2CPP processes measured factory/Zenject paired medians **0.766x** (Q1-Q3 **0.759-0.778**) and **0.779x** (**0.773-0.785**). Factory medians were **1,947.680/1,934.218 ns** versus Zenject **2,532.933/2,495.183 ns**. Direct Onity/Zenject paired medians were **0.776x/0.792x**. Allocation recording is unavailable in this mode and is marked `-1`, not zero. [Run 1](pool-zenject-checked-il2cpp-release-1-2026-09-24.json), [run 2](pool-zenject-checked-il2cpp-release-2-2026-09-24.json). Player executable SHA-256: `143DD3C6A93A45819A98E3CCA8FD1535B461CDF860C927D955CA54815FAF5D7B`; `GameAssembly.dll`: `9497B5F1CB50D0EDC5F8F7EFD04ABCEE742D6F78303F26099C2314909BA79884`.
- Every player run passed identical creation, reset, payload checksum, duplicate, and final-state checks. Onity factory and direct completed **7,748,129** successful gets and returns per run; each arm ran **7,748,096** reset callbacks and produced checksum **31,896,501,574,144**. Both build modes satisfy the predeclared factory/Zenject ratio gate in this workload. This supports a speed advantage for the tested two-parameter managed pooling scenario, not a general pooling, prefab, VContainer, or Unity speed ranking. The Unity control has weaker checking behavior and is not a like-for-like competitor. `ProjectSettings.asset` kept SHA-256 `93CC8177FCACAC2BF530346EF05658BF568483C0214EA9446AFA99C2E07E99F8` with no Git diff.

## Pre-fast-slot checked single-item Editor baseline (2026-09-24)

- The explicit Editor/Mono one-item rent/return test was repeated against Zenject after Onity's player-safe reference check was added. Onity checked/Zenject paired medians were **2.229x** (Q1-Q3 **2.140-2.285**) and **2.123x** (**1.999-2.181**). Onity checked medians were **317.4/311.1 ns** per pair versus Zenject **142.2/145.6 ns**. Onity default/Zenject ratios were **1.062x/0.989x**. All paths recorded **0 warmed GC.Alloc events** over 64 pairs. [Run 1](pool-zenject-checked-editor-1-2026-09-24.xml), [run 2](pool-zenject-checked-editor-2-2026-09-24.xml).
- This is a different workload from the 32-item IL2CPP burst: a single repeatedly reused item, no parameter or reset callback, and an Editor Mono JIT. It exposes the cost of hashing on every checked rent/return. The earlier Editor checked results above used Unity's native Editor-only checker before this Onity implementation; they do not measure the current source.

### Declared single-item fast-slot experiment

- A checked pool may keep its most recently retained reference in one field and track all other inactive references in the existing identity set. This removes set lookup/update from repeated one-item rent/return while retaining player duplicate checks. The unchecked path remains separate.
- Retain the candidate only if both fresh Editor checked/Zenject paired medians are at most **1.70x**, each Q3 is below the lower baseline Q1 (**1.999x**), and warmed allocation remains zero. The unchecked path must not show a reproducible normalized regression above **5%**.
- Run two fresh processes per Development and non-Development IL2CPP build. All duplicate, creation, reset, checksum, and final-state gates must pass; Development warmed allocation must remain zero. The 32-item checked Onity/Zenject ratio must not reproducibly regress by more than **5%** from the current checked baseline, and the original faster-than-Zenject gate must still pass. Revert the candidate if these conditions fail.
- The first two guarded candidate Editor runs met the checked speed and allocation gates, but Zenject's one-item control moved enough to make the unchecked normalized comparison ambiguous. Before an IL2CPP candidate build, run the exact pre-slot checked implementation and guarded candidate in fresh-process **baseline → candidate → candidate → baseline** order with unchanged settings. Compare candidate unchecked/Zenject ratios with adjacent baselines using the same **5%** gate.
- The A/B/A Editor sequence recorded checked Onity/Zenject paired medians **2.386x → 1.429x → 1.566x → 2.432x**. The two candidate Q3 values were **1.482x/1.579x**, below the preselected **1.999x** baseline bound; both candidate medians met **≤1.70x**. Unchecked Onity/Zenject medians were **1.122x → 1.029x → 1.147x → 1.219x**: each candidate was below its adjacent baseline, so the guarded unchecked path showed no normalized regression. The control's absolute median varied from **122.8 to 139.1 ns**, explaining why isolated runs were ambiguous. All four processes reported zero warmed allocation events. Raw NUnit results: [baseline first](pool-zenject-ab-baseline-first-editor-2026-09-24.xml), [candidate first](pool-zenject-ab-candidate-first-editor-2026-09-24.xml), [candidate second](pool-zenject-ab-candidate-second-editor-2026-09-24.xml), [baseline last](pool-zenject-ab-baseline-last-editor-2026-09-24.xml). The guarded candidate was restored to source SHA-256 `E7CC14434CE785B11F740B36BC17996AD444FC4429B7BD20494349B5A1E30BBB` after the final baseline run.

### Fast-slot IL2CPP verification

- The guarded candidate built as Windows IL2CPP Development and non-Development players from that source SHA. All player runs passed the same 32-item distinctness, parameter, fixed-capacity, duplicate-return, reset, checksum, and final-state checks. Onity factory/direct and Zenject rejected duplicate returns; the Unity control remained explicitly unchecked. Every arm ended with zero active and 32 inactive items, 7,748,096 resets, and checksum 31,896,501,574,144.
- Development candidate factory/Zenject paired medians were **0.818x** (Q1-Q3 **0.789-0.837**) and **0.827x** (**0.816-0.840**), against checked baselines **0.797x/0.786x**. The positive allocation control recorded **64** events and every warmed arm **0** in both processes. [Run 1](pool-zenject-fast-slot-il2cpp-development-1-2026-09-24.json), [run 2](pool-zenject-fast-slot-il2cpp-development-2-2026-09-24.json). Player executable SHA-256: `8D4C804FF605213A298140BE3C172EB407E7F5DC78C5082CDFAABAEA7B43BA0E`; `GameAssembly.dll`: `F997B98C13062777B8539B442A5BAE1340CE6DD40C700A4003C6A9C68FBC32E6`.
- Non-Development candidate factory/Zenject paired medians were **0.847x** (Q1-Q3 **0.832-0.871**) and **0.794x** (**0.786-0.825**) in the two declared fresh processes, against checked baselines **0.766x/0.779x**. The first run was higher than the other paired ratios, so two further processes from the *same unchanged binary* were collected to resolve this concrete regression risk: **0.797x** (**0.786-0.811**) and **0.801x** (**0.780-0.817**). Allocation is unavailable and marked `-1`, not zero. Raw reports: [run 1](pool-zenject-fast-slot-il2cpp-release-1-2026-09-24.json), [run 2](pool-zenject-fast-slot-il2cpp-release-2-2026-09-24.json), [extra run 3](pool-zenject-fast-slot-il2cpp-release-3-2026-09-24.json), [extra run 4](pool-zenject-fast-slot-il2cpp-release-4-2026-09-24.json). Player executable SHA-256: `143DD3C6A93A45819A98E3CCA8FD1535B461CDF860C927D955CA54815FAF5D7B`; `GameAssembly.dll`: `C1919EB505A849A35AF595038FC5066BEAD1AC80CA637531D452D7FE5CE2B68D`.
- The candidate still meets the predeclared faster-than-Zenject factory threshold in every IL2CPP run. The following interleaved comparison tests the separate no-repeatable-regression gate. `ProjectSettings.asset` kept its original SHA-256 and has no Git diff.
- The original checked baseline and candidate builds were measured about 40 minutes apart, while Zenject and Unity control timings changed. The final confirmation used the **existing unchanged binaries** in baseline → candidate → candidate → baseline order separately in Development and non-Development modes. Each process wrote a new report. Adjacent normalized candidate/baseline comparisons within **5%** met the no-regression gate; no result was excluded.
- The Development confirmation measured factory/Zenject paired medians **0.7972 → 0.8280 → 0.8178 → 0.7920**. Each candidate was **3.87%/3.26%** above its adjacent checked baseline, within the declared 5% limit. All four runs passed and recorded zero warmed `GC.Alloc` events with 64 events in the positive control. Raw reports: [baseline first](pool-zenject-ab-il2cpp-development-baseline-first-2026-09-24.json), [candidate first](pool-zenject-ab-il2cpp-development-candidate-first-2026-09-24.json), [candidate second](pool-zenject-ab-il2cpp-development-candidate-second-2026-09-24.json), [baseline last](pool-zenject-ab-il2cpp-development-baseline-last-2026-09-24.json).
- The non-Development confirmation measured **0.7669 → 0.8031 → 0.7761 → 0.7607**. Each candidate was **4.72%/2.03%** above its adjacent baseline, within the 5% limit. All four runs passed; allocation remains unavailable. Raw reports: [baseline first](pool-zenject-ab-il2cpp-release-baseline-first-2026-09-24.json), [candidate first](pool-zenject-ab-il2cpp-release-candidate-first-2026-09-24.json), [candidate second](pool-zenject-ab-il2cpp-release-candidate-second-2026-09-24.json), [baseline last](pool-zenject-ab-il2cpp-release-baseline-last-2026-09-24.json). No results were excluded, and neither binary was rebuilt for the interleaving.
- **Decision: retain the guarded fast slot.** It reduced the checked single-item Editor paired ratio by **40.1%/35.6%** in the interleaved sequence, while the 32-item IL2CPP normalized cost increase was **2.0–4.7%** in adjacent comparisons. The confirmed Release candidate factory/Zenject ratios **0.8031x/0.7761x** correspond to **19.7%/22.4% less elapsed time** in this workload. Checked single-item Editor/Mono Onity remains slower than Zenject, so this is not a general pooling-speed ranking. All source, test, benchmark, and report changes stayed outside serialized assets and package/project settings.
