@lite-fsm/entities 0.1.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/LICENSE +21 -0
  2. package/PERFORMANCE.md +537 -0
  3. package/README.md +395 -0
  4. package/dist/index.cjs +1 -0
  5. package/dist/index.d.cts +8 -0
  6. package/dist/index.d.ts +8 -0
  7. package/dist/index.js +1 -0
  8. package/dist/internal.d.cts +5 -0
  9. package/dist/internal.d.ts +5 -0
  10. package/dist/machine-extension.d.cts +153 -0
  11. package/dist/machine-extension.d.ts +153 -0
  12. package/dist/plugin.d.cts +32 -0
  13. package/dist/plugin.d.ts +32 -0
  14. package/dist/react/index.d.cts +11 -0
  15. package/dist/react/index.d.ts +11 -0
  16. package/dist/react.cjs +1 -0
  17. package/dist/react.js +1 -0
  18. package/dist/runtime/access.d.cts +72 -0
  19. package/dist/runtime/access.d.ts +72 -0
  20. package/dist/runtime/columns.d.cts +11 -0
  21. package/dist/runtime/columns.d.ts +11 -0
  22. package/dist/runtime/compile.d.cts +68 -0
  23. package/dist/runtime/compile.d.ts +68 -0
  24. package/dist/runtime/effects.d.cts +26 -0
  25. package/dist/runtime/effects.d.ts +26 -0
  26. package/dist/runtime/lifecycle.d.cts +10 -0
  27. package/dist/runtime/lifecycle.d.ts +10 -0
  28. package/dist/runtime/mutation-snapshot.d.cts +41 -0
  29. package/dist/runtime/mutation-snapshot.d.ts +41 -0
  30. package/dist/runtime/react.d.cts +29 -0
  31. package/dist/runtime/react.d.ts +29 -0
  32. package/dist/runtime/reactions.d.cts +15 -0
  33. package/dist/runtime/reactions.d.ts +15 -0
  34. package/dist/runtime/reduce-batch.d.cts +6 -0
  35. package/dist/runtime/reduce-batch.d.ts +6 -0
  36. package/dist/runtime/reduce-despawn.d.cts +7 -0
  37. package/dist/runtime/reduce-despawn.d.ts +7 -0
  38. package/dist/runtime/reduce-post-process.d.cts +35 -0
  39. package/dist/runtime/reduce-post-process.d.ts +35 -0
  40. package/dist/runtime/reduce-shared.d.cts +34 -0
  41. package/dist/runtime/reduce-shared.d.ts +34 -0
  42. package/dist/runtime/reduce-spawn.d.cts +5 -0
  43. package/dist/runtime/reduce-spawn.d.ts +5 -0
  44. package/dist/runtime/reduce-transitions.d.cts +11 -0
  45. package/dist/runtime/reduce-transitions.d.ts +11 -0
  46. package/dist/runtime/reduce.d.cts +5 -0
  47. package/dist/runtime/reduce.d.ts +5 -0
  48. package/dist/runtime/routing.d.cts +11 -0
  49. package/dist/runtime/routing.d.ts +11 -0
  50. package/dist/runtime/runtime-index.d.cts +23 -0
  51. package/dist/runtime/runtime-index.d.ts +23 -0
  52. package/dist/runtime/snapshot-export.d.cts +12 -0
  53. package/dist/runtime/snapshot-export.d.ts +12 -0
  54. package/dist/runtime/snapshot-import.d.cts +5 -0
  55. package/dist/runtime/snapshot-import.d.ts +5 -0
  56. package/dist/runtime/snapshot-types.d.cts +56 -0
  57. package/dist/runtime/snapshot-types.d.ts +56 -0
  58. package/dist/runtime/snapshot.d.cts +15 -0
  59. package/dist/runtime/snapshot.d.ts +15 -0
  60. package/dist/runtime/spawn-commit.d.cts +10 -0
  61. package/dist/runtime/spawn-commit.d.ts +10 -0
  62. package/dist/runtime/state.d.cts +15 -0
  63. package/dist/runtime/state.d.ts +15 -0
  64. package/dist/runtime/storage.d.cts +19 -0
  65. package/dist/runtime/storage.d.ts +19 -0
  66. package/dist/runtime/store-types.d.cts +81 -0
  67. package/dist/runtime/store-types.d.ts +81 -0
  68. package/dist/runtime/transaction.d.cts +104 -0
  69. package/dist/runtime/transaction.d.ts +104 -0
  70. package/dist/runtime/transitionTrace.d.cts +19 -0
  71. package/dist/runtime/transitionTrace.d.ts +19 -0
  72. package/dist/schema.d.cts +90 -0
  73. package/dist/schema.d.ts +90 -0
  74. package/dist/spawn.d.cts +56 -0
  75. package/dist/spawn.d.ts +56 -0
  76. package/package.json +90 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 AlexanderGureev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/PERFORMANCE.md ADDED
@@ -0,0 +1,537 @@
1
+ # Производительность `@lite-fsm/entities`
2
+
3
+ Документ фиксирует базовый профиль производительности и очередь работ для приближения публичного `@lite-fsm/entities` к ручному SoA baseline.
4
+
5
+ ## Базовый замер
6
+
7
+ Команда:
8
+
9
+ ```bash
10
+ pnpm run bench:entities:record -- --runs 5 --label codex-baseline-2026-06-14 --include gate,diagnostics
11
+ ```
12
+
13
+ Артефакты:
14
+
15
+ - `.bench/entities/codex-baseline-2026-06-14.json`
16
+ - `.bench/entities/codex-baseline-2026-06-14.md`
17
+
18
+ Окружение:
19
+
20
+ - Дата: `2026-06-14T16:46:13.766Z`
21
+ - Git: `4c7cd65fffbf`, branch `entities`, status `clean`
22
+ - Node: `v24.16.0`
23
+ - OS: `darwin/arm64`
24
+ - CPU: `Apple M1 Max`, 10 cores
25
+ - Package manager: `pnpm/10.33.0`
26
+
27
+ ## Gate summary
28
+
29
+ | Сценарий | Строки | SoA median | entities median | Ratio | Бюджет | Статус |
30
+ | --- | ---: | ---: | ---: | ---: | ---: | --- |
31
+ | `movement update` | 10 000 | 0.046ms | 0.226ms | 4.87x | 1.50x | fail |
32
+ | `movement update` | 50 000 | 0.232ms | 1.075ms | 4.62x | 1.50x | fail |
33
+ | `projectile lifetime update` | 10 000 | 0.036ms | 0.202ms | 5.65x | 1.50x | fail |
34
+ | `projectile lifetime update` | 50 000 | 0.179ms | 0.978ms | 5.44x | 1.50x | fail |
35
+ | `despawnOn cleanup` | 10 000 | 0.055ms | 2.406ms | 43.91x | 2.00x | fail |
36
+ | `despawnOn cleanup` | 50 000 | 0.277ms | 20.521ms | 74.65x | 2.00x | fail |
37
+ | `sprite sync reaction` | 10 000 | 0.075ms | 1.506ms | 20.04x | 2.00x | fail |
38
+ | `sprite sync reaction` | 50 000 | 0.491ms | 8.310ms | 16.58x | 2.00x | fail |
39
+
40
+ Важные предупреждения стабильности:
41
+
42
+ - `despawnOn cleanup / 50 000` имеет высокий разброс entity median: `27.8%`.
43
+ - Diagnostics `despawnOn cleanup / 50 000 / public manager.transition` имеет высокий разброс: `24.4%`.
44
+ - Несколько baseline на `10 000` строк ниже `0.050ms`; для них ratio чувствителен к шуму таймера.
45
+
46
+ ## Historical diagnostics summary
47
+
48
+ Этот раздел фиксирует legacy synthetic diagnostics из базового record 2026-06-14. После `transition-trace-baseline` эти данные не являются источником production attribution и не используются как strict gate для следующих оптимизаций. Текущий workflow использует `gate` для budget check и `trace` для attribution реального `manager.transition(...)`.
49
+
50
+ | Сценарий | Строки | raw SoA | semantic SoA | raw entity kernel | public transition |
51
+ | --- | ---: | ---: | ---: | ---: | ---: |
52
+ | `movement update` | 10 000 | 0.015ms | 0.048ms | 0.189ms | 0.220ms |
53
+ | `movement update` | 50 000 | 0.074ms | 0.244ms | 0.954ms | 1.077ms |
54
+ | `projectile lifetime update` | 10 000 | 0.007ms | 0.039ms | 0.178ms | 0.206ms |
55
+ | `projectile lifetime update` | 50 000 | 0.032ms | 0.193ms | 0.886ms | 0.996ms |
56
+ | `despawnOn cleanup` | 10 000 | 0.028ms | 0.055ms | 0.212ms | 2.667ms |
57
+ | `despawnOn cleanup` | 50 000 | 0.141ms | 0.280ms | 1.421ms | 22.694ms |
58
+ | `sprite sync reaction` | 10 000 | 0.025ms | 0.064ms | 1.047ms | 1.482ms |
59
+ | `sprite sync reaction` | 50 000 | 0.123ms | 0.334ms | 8.052ms | 8.300ms |
60
+
61
+ Вывод по слоям:
62
+
63
+ - Для reducer-only сценариев основная цена уже внутри `raw entity kernel`: `movement 50k` имеет `0.954ms` против `0.244ms` semantic SoA, `projectile 50k` имеет `0.886ms` против `0.193ms`.
64
+ - Для `sprite sync reaction` почти весь разрыв находится в entity kernel и reaction scope: `8.052ms` raw entity kernel против `0.334ms` semantic SoA на `50k`.
65
+ - Для `despawnOn cleanup` публичный путь резко дороже диагностического kernel: `22.694ms` против `1.421ms` на `50k`. Это главный источник выигрыша и главный источник нестабильности.
66
+ - Общий `manager.transition` добавляет небольшой постоянный overhead в reducer-only сценариях: примерно `0.03ms` на `10k` и `0.11-0.12ms` на `50k`. Это не первый источник оптимизации.
67
+
68
+ ## Устройство текущего hot path
69
+
70
+ `entitiesPlugin` регистрирует storage runtime `entityStorageRuntime`, route meta `entityId`, hook `beforeReduce` для spawn staging и manager extension `manager.entities()`.
71
+
72
+ На каждый `manager.transition` core создает dispatch context, `runtime: Map`, route, `touched: Set`, проходит storage `prepareAction`, `beforeReduce`, `reduce`, `commit`, `reactions`, `effects` и subscribers. Для `entities` это приводит к следующей цепочке:
73
+
74
+ 1. `prepareEntityTransaction` создает transaction на каждый dispatch.
75
+ 2. `stageSpawnAction` при spawn-событии запускает recipe, валидирует specs и payload.
76
+ 3. `reduceEntityBucket` выполняет spawn lifecycle, cleanup, public event routing, reducers, повторный cleanup.
77
+ 4. `collectEntityPublicReducerBatches` для unscoped событий выбирает state buckets по `acceptStateBucketsByEventCode`.
78
+ 5. `reduceAcceptedBatch` применяет transition table, создает `self`, вызывает reducer, затем отдельными проходами валидирует state, обновляет row versions, планирует effects/reactions/despawn/terminal cleanup и обновляет state buckets.
79
+ 6. `commit` восстанавливает public slices.
80
+ 7. `runEntityReactions` собирает scoped entries, создает scoped deps и выполняет reactions.
81
+
82
+ Сильные стороны текущей модели:
83
+
84
+ - Данные контекста уже лежат в SoA колонках.
85
+ - Unscoped routing не сканирует все `presence`; он использует state buckets.
86
+ - Public state slice отделен от тяжелых колонок и обновляется через lightweight object.
87
+
88
+ Основная проблема: generic lifecycle pipeline выполняет много проходов, проверок, allocations и cleanup-операций даже для простых событий вида `active -> active`.
89
+
90
+ ## Приоритеты оптимизации
91
+
92
+ Разделы 1-3 фиксируют исторические итерации до появления trace baseline. Команды с `--include gate,diagnostics` в этих разделах оставлены как provenance старых artifacts; для следующих оптимизаций использовать workflow `gate,trace` из раздела `Итог trace baseline` и финального порядка работ.
93
+
94
+ ### 1. Ускорить `despawnOn cleanup`
95
+
96
+ Ожидаемый максимальный выигрыш: самый высокий. Текущий gate на `50k`: `20.521ms`, `74.65x` к SoA. Diagnostics public: `22.694ms`, `165.86x` к raw SoA. Сценарий также нестабилен.
97
+
98
+ Затронутые места:
99
+
100
+ - `packages/entities/src/runtime/reduce.ts`: `scheduleDespawnOnRows`, cleanup plan, `flushEntityLifecycleCleanup`.
101
+ - `packages/entities/src/runtime/state.ts`: `removeActorRowOwnership`, `removeActorRowsForStore`, `removeEntityRecords`.
102
+ - `packages/entities/src/runtime/transaction.ts`: `scheduleEntityDespawn`, `consumeScheduledDespawns`.
103
+
104
+ Контракт решения:
105
+
106
+ 1. Fast path удаляет строки без observable `ENTITY_DESPAWNED` work. Observable work — accepted `ENTITY_DESPAWNED` transition и reducer или `reactions.ENTITY_DESPAWNED`; state `effects` целевого состояния не являются cleanup contract.
107
+ 2. Cleanup строит план удаления и удаляет строки батчами по store через `removeActorRowsForStore`.
108
+ 3. `publicSlice`, `store.version` и `entityStore.version` обновляются один раз на затронутый batch.
109
+ 4. Контракт колонок удалённых строк: публичное чтение корректно только через `has(entity) === true`; runtime не обязан очищать колонки удаленной строки в hot path, а `dehydrate()` должен сериализовать удалённые слоты через defaults из `initialContext`.
110
+ 5. `despawnScheduled` использует переиспользуемый scratch runtime с очисткой только затронутых индексов.
111
+
112
+ Критерий приемки:
113
+
114
+ - `despawnOn cleanup / 50 000` должен сначала приблизиться к `raw entity kernel` уровня `1-2ms`, затем к `semantic SoA` уровня `0.3-0.6ms`.
115
+ - RSD для `despawnOn cleanup / 50 000` должен опуститься ниже `15%`.
116
+ - После изменения запускать `pnpm run bench:entities:record -- --runs 5 --label after-despawn-cleanup --include gate,diagnostics` и сравнивать с baseline.
117
+
118
+ ### Итог problem 1: despawnOn cleanup
119
+
120
+ Команда:
121
+
122
+ ```bash
123
+ pnpm run bench:entities:record -- --runs 5 --label after-despawn-cleanup --include gate,diagnostics
124
+ pnpm run bench:entities:compare -- .bench/entities/codex-baseline-2026-06-14.json .bench/entities/after-despawn-cleanup.json
125
+ ```
126
+
127
+ Артефакты:
128
+
129
+ - [`after-despawn-cleanup.json`](../../.bench/entities/after-despawn-cleanup.json)
130
+ - [`after-despawn-cleanup.md`](../../.bench/entities/after-despawn-cleanup.md)
131
+ - [`after-despawn-cleanup-vs-codex-baseline-2026-06-14.md`](../../.bench/entities/after-despawn-cleanup-vs-codex-baseline-2026-06-14.md)
132
+
133
+ Окружение итогового record: `2026-06-14T19:02:30.226Z`, Git `9f092b7109fd`, branch `entities`, status `dirty`, Node `v24.16.0`, CPU `Apple M1 Max`, package manager `pnpm/10.33.0`.
134
+
135
+ | Сценарий | Строки | Baseline median | After median | Ratio к SoA до | Ratio к SoA после | Ускорение | RSD after |
136
+ | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
137
+ | `despawnOn cleanup` | 10 000 | 2.406ms | 0.241ms | 43.91x | 4.43x | 2.166ms (90.0%) | 11.7% |
138
+ | `despawnOn cleanup` | 50 000 | 20.521ms | 1.098ms | 74.65x | 3.99x | 19.423ms (94.6%) | 0.9% |
139
+
140
+ Итог: первый целевой диапазон `1-2ms` для `despawnOn cleanup / 50 000` достигнут: gate median `1.098ms`. RSD gate-сценария `50 000` ниже порога `15%` (`0.9%`). Соседние gate-сценарии не получили регрессий больше `10%`: compare не показывает ни одного public entity median regression выше порога.
141
+
142
+ Diagnostics после обновления fixture содержит cleanup breakdown:
143
+
144
+ | Фаза / слой | 10 000 median | 50 000 median | RSD 50 000 |
145
+ | --- | ---: | ---: | ---: |
146
+ | `schedule despawn` | 0.015ms | 0.073ms | 1.4% |
147
+ | `despawn lifecycle plan` | 0.006ms | 0.010ms | 15.5% |
148
+ | `batch remove actor rows` | 0.005ms | 0.007ms | 9.9% |
149
+ | `remove entity records` | 0.005ms | 0.013ms | 14.1% |
150
+ | `public commit` | 0.000ms | 0.001ms | 55.6% |
151
+ | `raw entity kernel` | 0.196ms | 1.647ms | 1.4% |
152
+ | `public manager.transition` | 0.247ms | 1.086ms | 1.1% |
153
+
154
+ Вывод по problem 1: public cleanup path теперь близок к primary reducer path и проходит первый целевой диапазон. Batch cleanup phases сами по себе занимают малую часть времени; основная стоимость остается в полном reducer/kernel проходе по активному bucket. Diagnostics `raw entity kernel / 50 000` стал медленнее baseline из-за более точной batch-removal модели fixture, но gate public scenarios не регрессировали больше `10%`.
155
+
156
+ Следующие приоритеты:
157
+
158
+ 1. Двигать `despawnOn cleanup / 50 000` от первого диапазона `1-2ms` к следующему диапазону `0.3-0.6ms`: сейчас лимитирующий слой — общий reducer/kernel проход.
159
+ 2. Профилировать `raw entity kernel` cleanup после перехода fixture на batch-removal модель, чтобы отделить цену полного bucket scan от цены удаления `128` строк.
160
+ 3. Сохранять контракт колонок удалённых строк: чтение только через `has(entity)`, snapshot sanitation через defaults, без возврата очистки колонок в hot path.
161
+
162
+ ### 2. Ускорить reaction scope и `sprite sync reaction`
163
+
164
+ Ожидаемый выигрыш: очень высокий. Текущий gate на `50k`: `8.310ms`, `16.58x` к SoA. Diagnostics показывают, что почти вся цена уже в `raw entity kernel`: `8.052ms` против `0.334ms` semantic SoA.
165
+
166
+ Затронутые места:
167
+
168
+ - `packages/entities/src/runtime/transaction.ts`: `scheduleEntityReactionBatch` копирует `indices`.
169
+ - `packages/entities/src/runtime/reactions.ts`: `collectReactionScopeEntries`, `createReactionDeps`, `runReactionBatch`.
170
+ - `packages/entities/src/runtime/access.ts`: `createScopedEntitySelf`, `createScopedEntityAccess`.
171
+
172
+ Что исправлять:
173
+
174
+ 1. Убрать обязательное `indices.slice()` для reaction batch. Нужен стабильный batch view или scratch ownership, который не копирует `50k` индексов для каждого события.
175
+ 2. Не строить `entries: CapturedEntityScopeEntry[]` и `Map` для всего batch, если reaction использует только `self.indices` и колонки. Сейчас `createScopedEntitySelf` создает `Map` по всем entries, что дорого на `50k`.
176
+ 3. Разделить быстрый scoped self для reactions и строгий diagnostic self. В production hot path `has(entity)` и `entityId(entity)` могут опираться на generation arrays и scope marker без построения `Map`.
177
+ 4. Не копировать user deps через `{ ...ctx.manager.getDependencies() }` и серию `delete` на каждый reaction. Нужен заранее нормализованный deps object или wrapper с предсказуемой формой.
178
+ 5. Кешировать `entities().get("movementActor")` view на уровне reaction deps, не создавать лишний scoped access wrapper при каждом batch, если scope не требует строгой dev-проверки.
179
+
180
+ Критерий приемки:
181
+
182
+ - `sprite sync reaction / 50 000 / raw entity kernel` должен опуститься с `8.052ms` хотя бы ниже `2ms` первым шагом.
183
+ - Public `sprite sync reaction / 50 000` должен следовать за kernel и не добавлять больше `0.3ms`.
184
+ - Поведение scoped `self.has`, `self.entityId`, `entities().get` должно остаться покрытым runtime tests.
185
+
186
+ ### Итог problem 2: reaction scope и sprite sync reaction
187
+
188
+ Команды:
189
+
190
+ ```bash
191
+ pnpm run bench:entities:record -- --runs 5 --label after-reaction-scope --include gate,diagnostics
192
+ pnpm run bench:entities:compare -- .bench/entities/after-despawn-cleanup.json .bench/entities/after-reaction-scope.json
193
+ pnpm run bench:entities:compare -- .bench/entities/codex-baseline-2026-06-14.json .bench/entities/after-reaction-scope.json
194
+ ```
195
+
196
+ Артефакты:
197
+
198
+ - [`after-reaction-scope.json`](../../.bench/entities/after-reaction-scope.json)
199
+ - [`after-reaction-scope.md`](../../.bench/entities/after-reaction-scope.md)
200
+ - [`after-reaction-scope-vs-after-despawn-cleanup.md`](../../.bench/entities/after-reaction-scope-vs-after-despawn-cleanup.md)
201
+ - [`after-reaction-scope-vs-codex-baseline-2026-06-14.md`](../../.bench/entities/after-reaction-scope-vs-codex-baseline-2026-06-14.md)
202
+
203
+ Окружение итогового record: `2026-06-14T20:53:31.896Z`, Git `ff6c39838767`, branch `entities`, status `dirty`, Node `v24.16.0`, CPU `Apple M1 Max`, package manager `pnpm/10.33.0`.
204
+
205
+ Gate `sprite sync reaction` относительно `after-despawn-cleanup`:
206
+
207
+ | Сценарий | Строки | Baseline median | After median | Ratio до | Ratio после | Ускорение | RSD after |
208
+ | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
209
+ | `sprite sync reaction` | 10 000 | 1.507ms | 0.893ms | 19.26x | 12.33x | 0.614ms (40.7%) | 2.2% |
210
+ | `sprite sync reaction` | 50 000 | 8.259ms | 4.792ms | 16.85x | 9.77x | 3.467ms (42.0%) | 5.0% |
211
+
212
+ Diagnostics `sprite sync reaction` относительно `after-despawn-cleanup`:
213
+
214
+ | Слой | Строки | Baseline median | After median | Ratio к raw SoA до | Ratio к raw SoA после | Ускорение | RSD after |
215
+ | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
216
+ | `semantic SoA baseline` | 10 000 | 0.064ms | 0.065ms | 2.62x | 2.58x | -0.001ms (-1.5%) | 13.7% |
217
+ | `raw entity kernel` | 10 000 | 1.001ms | 0.430ms | 41.06x | 17.61x | 0.571ms (57.1%) | 7.0% |
218
+ | `public manager.transition` | 10 000 | 1.447ms | 0.904ms | 59.45x | 36.66x | 0.543ms (37.5%) | 1.1% |
219
+ | `semantic SoA baseline` | 50 000 | 0.328ms | 0.336ms | 2.64x | 2.70x | -0.009ms (-2.6%) | 1.9% |
220
+ | `raw entity kernel` | 50 000 | 7.916ms | 2.346ms | 63.17x | 18.87x | 5.570ms (70.4%) | 1.1% |
221
+ | `public manager.transition` | 50 000 | 7.941ms | 4.753ms | 65.18x | 38.65x | 3.188ms (40.1%) | 2.4% |
222
+
223
+ Breakdown `sprite sync reaction / 50 000` после reaction scope:
224
+
225
+ | Фаза / слой | Median | RSD after |
226
+ | --- | ---: | ---: |
227
+ | `reduce entity batches` | 1.681ms | 0.8% |
228
+ | `schedule reaction batch` | 0.000ms | 10.0% |
229
+ | `collect reaction scope` | 0.433ms | 2.5% |
230
+ | `create reaction deps` | 0.000ms | 16.9% |
231
+ | `run user reaction` | 0.094ms | 0.5% |
232
+ | `raw entity kernel` | 2.346ms | 1.1% |
233
+ | `public manager.transition` | 4.753ms | 2.4% |
234
+
235
+ Итог: problem 2 дал сильное ускорение reaction scope, но first gate не достигнут. `sprite sync reaction / 50 000 / raw entity kernel` остается выше порога `2ms` (`2.346ms`), а `public manager.transition` добавляет к kernel `2.407ms`, что выше лимита `0.3ms`. Stretch gate также не достигнут: kernel выше `1ms`, public ratio `9.77x` далек от бюджета `2.00x`.
236
+
237
+ Breakdown показывает, что основная оставшаяся цена находится в общем reducer pipeline и public path: `reduce entity batches` занимает `1.681ms`, public overhead поверх kernel — `2.407ms`. Собственный reaction scope слой больше не доминирует: `collect reaction scope` занимает `0.433ms`, `run user reaction` — `0.094ms`, scheduling и deps creation находятся на уровне таймера.
238
+
239
+ Соседние gate-сценарии не прошли порог регрессии относительно `after-despawn-cleanup`: `movement update` замедлился на `13.0%` / `13.2%`, `projectile lifetime update` — на `12.9%` / `14.6%` для `10 000` / `50 000` строк. `despawnOn cleanup` остался ниже порога (`6.4%` / `5.9%`). Diagnostics raw entity kernel для `movement` и `projectile` при этом изменился в пределах `0.2%`, поэтому основная регрессия видна в public path.
240
+
241
+ ### 3. Сократить generic overhead `reduceAcceptedBatch` для простого `active -> active`
242
+
243
+ Ожидаемый выигрыш: высокий для всех reducer-only сценариев. Текущий raw entity kernel на `50k` примерно в `4-5x` медленнее semantic SoA: `movement` `0.954ms` против `0.244ms`, `projectile` `0.886ms` против `0.193ms`.
244
+
245
+ Затронутые места reducer layer:
246
+
247
+ - `packages/entities/src/runtime/reduce.ts`: `resolveTransitionTarget`, `applyDefaultTransitions`, `postProcessAcceptedRows`, `markActorRowsTouched`, `scheduleEnteredStateEffects`, `updateActorStateBuckets`.
248
+ - `packages/entities/src/runtime/compile.ts`: compile-time metadata для event/store fast path.
249
+ - `packages/entities/src/runtime/state.ts`: stable reducer self cache, `getActorReducerSelf`, `refreshActorPublicSlice`.
250
+
251
+ Что исправлять:
252
+
253
+ 1. Компилировать per-event reduce plan. Для события с одним принимающим state и identity transition (`active -> active`) не нужно записывать `prevStateCode` и `stateCode` для каждой строки до reducer.
254
+ 2. Если template не имеет effects, terminal states, `despawnOn` и reactions для event, не запускать соответствующие проходы по всем accepted rows.
255
+ 3. Не выполнять `updateActorStateBuckets` для identity transition, пока reducer явно не записал другой `stateCode`. Нужен дешевый способ определить state writes: отдельный dirty list, explicit helper или compile-time режим для reducer-only templates.
256
+ 4. Создавать `self` со стабильной формой один раз на store и менять только `indices`.
257
+ 5. Объединить проходы post-processing там, где они остаются обязательными: validation, rowVersion, effects/despawn/terminal scan могут быть одним циклом.
258
+
259
+ Критерий приемки:
260
+
261
+ - `movement update / 50 000 / raw entity kernel` должен опуститься ниже `0.5ms` первым шагом.
262
+ - `projectile lifetime update / 50 000 / raw entity kernel` должен опуститься ниже `0.5ms` первым шагом.
263
+ - Gate reducer-only должен двигаться от `4.6-5.4x` к бюджету `1.5x`.
264
+
265
+ ### Итог problem 3: reducer layer
266
+
267
+ Команды:
268
+
269
+ ```bash
270
+ pnpm run bench:entities:record -- --runs 1 --label reducer-layer-smoke --include gate,diagnostics --row-counts 1000
271
+ pnpm run bench:entities:record -- --runs 3 --label after-reducer-layer --include gate,diagnostics
272
+ pnpm run bench:entities:compare -- .bench/entities/after-reaction-scope.json .bench/entities/after-reducer-layer.json
273
+ pnpm run bench:entities:compare -- .bench/entities/codex-baseline-2026-06-14.json .bench/entities/after-reducer-layer.json
274
+ ```
275
+
276
+ Артефакты:
277
+
278
+ - [`after-reducer-layer.json`](../../.bench/entities/after-reducer-layer.json)
279
+ - [`after-reducer-layer.md`](../../.bench/entities/after-reducer-layer.md)
280
+ - [`after-reducer-layer-vs-after-reaction-scope.md`](../../.bench/entities/after-reducer-layer-vs-after-reaction-scope.md)
281
+ - [`after-reducer-layer-vs-codex-baseline-2026-06-14.md`](../../.bench/entities/after-reducer-layer-vs-codex-baseline-2026-06-14.md)
282
+
283
+ Окружение итогового record: `2026-06-14T22:36:11.015Z`, Git `6924c1ba6c38`, branch `entities`, status `dirty`, Node `v24.16.0`, CPU `Apple M1 Max`, package manager `pnpm/10.33.0`.
284
+
285
+ Gate относительно `after-reaction-scope`:
286
+
287
+ | Сценарий | Строки | Before median | After median | Ratio после | Изменение | RSD after |
288
+ | --- | ---: | ---: | ---: | ---: | ---: | ---: |
289
+ | `movement update` | 10 000 | 0.254ms | 0.101ms | 2.19x | -60.0% | 5.5% |
290
+ | `movement update` | 50 000 | 1.286ms | 0.471ms | 2.04x | -63.4% | 1.9% |
291
+ | `projectile lifetime update` | 10 000 | 0.236ms | 0.086ms | 2.42x | -63.3% | 3.9% |
292
+ | `projectile lifetime update` | 50 000 | 1.171ms | 0.411ms | 2.28x | -64.9% | 5.6% |
293
+ | `despawnOn cleanup` | 10 000 | 0.256ms | 0.130ms | 2.35x | -49.3% | 7.5% |
294
+ | `despawnOn cleanup` | 50 000 | 1.163ms | 0.508ms | 1.81x | -56.3% | 1.9% |
295
+ | `sprite sync reaction` | 10 000 | 0.893ms | 0.589ms | 7.55x | -34.0% | 0.5% |
296
+ | `sprite sync reaction` | 50 000 | 4.792ms | 3.093ms | 6.32x | -35.5% | 5.2% |
297
+
298
+ Соседних gate-регрессий выше `10%` относительно `after-reaction-scope` нет: все public gate median ускорились. От исходного `codex-baseline-2026-06-14` итог также быстрее во всех gate-сценариях: `movement 50 000` на `56.2%`, `projectile 50 000` на `57.9%`, `despawnOn cleanup 50 000` на `97.5%`, `sprite sync reaction 50 000` на `62.8%`.
299
+
300
+ Diagnostics reducer layers для `50 000` строк:
301
+
302
+ | Слой | `movement update` | RSD | `projectile lifetime update` | RSD | `sprite sync reaction` | RSD |
303
+ | --- | ---: | ---: | ---: | ---: | ---: | ---: |
304
+ | `collect/default transitions` | 0.116ms | 1.1% | 0.115ms | 0.5% | 0.232ms | 0.6% |
305
+ | `create reducer self` | 0.000ms | 7.2% | 0.000ms | 6.3% | 0.000ms | 0.8% |
306
+ | `run user reducer` | 0.211ms | 45.0% | 0.122ms | 1.5% | 0.191ms | 0.1% |
307
+ | `validate final states` | 0.110ms | 0.8% | 0.106ms | 0.8% | 0.211ms | 1.2% |
308
+ | `mark rows/public slice` | 0.048ms | 8.2% | 0.048ms | 8.3% | 0.096ms | 0.2% |
309
+ | `schedule lifecycle work` | 0.174ms | 5.5% | 0.174ms | 4.1% | 0.346ms | 0.5% |
310
+ | `update state buckets` | 0.065ms | 15.5% | 0.067ms | 15.0% | 0.131ms | 0.2% |
311
+ | `reduce entity batches` | n/a | n/a | n/a | n/a | 1.374ms | 10.4% |
312
+ | `raw entity kernel` | 0.888ms | 1.2% | 0.810ms | 0.4% | 2.173ms | 7.0% |
313
+ | `public manager.transition` | 0.480ms | 2.7% | 0.407ms | 0.7% | 3.073ms | 6.3% |
314
+
315
+ Public overhead над diagnostics `raw entity kernel` для `50 000` строк:
316
+
317
+ | Сценарий | `raw entity kernel` | `public manager.transition` | Public minus kernel | Ratio public/kernel |
318
+ | --- | ---: | ---: | ---: | ---: |
319
+ | `movement update` | 0.888ms | 0.480ms | -0.408ms | 0.54x |
320
+ | `projectile lifetime update` | 0.810ms | 0.407ms | -0.403ms | 0.50x |
321
+ | `despawnOn cleanup` | 0.850ms | 0.517ms | -0.332ms | 0.61x |
322
+ | `sprite sync reaction` | 2.173ms | 3.073ms | 0.900ms | 1.41x |
323
+
324
+ Отрицательный overhead в reducer-only и cleanup строках означает, что diagnostics `raw entity kernel` продолжает моделировать более общий internal kernel и завышает стоимость относительно production `manager.transition` после reducer layer. Поэтому strict критерий `raw entity kernel < 0.5ms` не считается закрытым по diagnostics fixture: `movement` показывает `0.888ms`, `projectile` показывает `0.810ms`. При этом production gate для тех же `50 000` строк уже ниже `0.5ms`: `movement` `0.471ms`, `projectile` `0.411ms`.
325
+
326
+ Итог first gate: достигнут частично. Reducer-only production path прошел первый временной порог `0.5ms` для `50 000` строк, и соседних public gate-регрессий нет. Полный first gate не закрыт: `sprite sync reaction / 50 000 / reduce entity batches` остается выше `1.0ms` (`1.374ms`), `sprite sync reaction / 50 000 / raw entity kernel` остается выше `2.0ms` (`2.173ms`), а diagnostics `raw entity kernel` для reducer-only сценариев требует обновления модели перед использованием как strict gate.
327
+
328
+ Остаточные риски для problem 5:
329
+
330
+ 1. Public ratio reducer-only еще выше бюджета `1.50x`: `movement 50 000` имеет `2.04x`, `projectile 50 000` имеет `2.28x`.
331
+ 2. `sprite sync reaction / 50 000` ускорился до `3.093ms`, но остается далек от бюджета `2.00x`: `6.32x` к SoA.
332
+ 3. Для `sprite sync reaction / 50 000` public overhead над diagnostics kernel составляет `0.900ms`, поэтому core/bucket overhead и reaction/public path остаются отдельной областью problem 5.
333
+ 4. Diagnostics fixture нужно синхронизировать с новым reducer fast path: сейчас production public path быстрее modeled `raw entity kernel` для reducer-only сценариев.
334
+
335
+ ### Итог trace baseline
336
+
337
+ Команды:
338
+
339
+ ```bash
340
+ pnpm run bench:entities:record -- --runs 5 --label transition-trace-baseline --include gate,trace --row-counts 50000
341
+ pnpm run bench:entities:compare -- .bench/entities/transition-trace-baseline.json .bench/entities/transition-trace-baseline.json
342
+ ```
343
+
344
+ Артефакты:
345
+
346
+ - [`transition-trace-baseline.json`](../../.bench/entities/transition-trace-baseline.json)
347
+ - [`transition-trace-baseline.md`](../../.bench/entities/transition-trace-baseline.md)
348
+
349
+ Trace измеряет реальный public path `manager.transition(...)` на production dist и использует те же composition runners, что gate. Включенный collector добавляет собственный overhead, поэтому абсолютные `Trace total` отделены от значений gate. `traceTotal / gateEntityMedian` — guard качества instrumentation: значение выше `2.00x` требует проверки разметки, но не является performance budget. Основной budget check остается за gate; trace является attribution-отчетом для выбора следующего шага оптимизации.
350
+
351
+ Начиная с этого baseline, `raw entity kernel` из legacy diagnostics не используется для выбора следующего шага и не считается production truth. Старые diagnostics artifacts сохраняются только для исторического сравнения.
352
+
353
+ Gate `50 000`:
354
+
355
+ | Сценарий | SoA median | entities median | Ratio | Бюджет | Статус | RSD |
356
+ | --- | ---: | ---: | ---: | ---: | --- | ---: |
357
+ | `movement update` | 0.250ms | 0.832ms | 3.39x | 1.50x | fail | 32.5% |
358
+ | `projectile lifetime update` | 0.181ms | 0.404ms | 2.27x | 1.50x | fail | 32.9% |
359
+ | `despawnOn cleanup` | 0.276ms | 0.512ms | 1.86x | 2.00x | fail | 1.9% |
360
+ | `sprite sync reaction` | 0.320ms | 3.723ms | 11.70x | 2.00x | fail | 5.2% |
361
+
362
+ Trace totals:
363
+
364
+ | Сценарий | Trace total | Gate entities median | `traceTotal / gateEntityMedian` | Transitions | RSD |
365
+ | --- | ---: | ---: | ---: | ---: | ---: |
366
+ | `movement update` | 0.819ms | 0.832ms | 0.98x | 750 | 1.0% |
367
+ | `projectile lifetime update` | 0.413ms | 0.404ms | 1.02x | 750 | 0.7% |
368
+ | `despawnOn cleanup` | 0.510ms | 0.512ms | 0.99x | 750 | 1.3% |
369
+ | `sprite sync reaction` | 3.529ms | 3.723ms | 0.95x | 750 | 4.8% |
370
+
371
+ Предупреждений `traceTotal / gateEntityMedian > 2.00x` нет. Высокий RSD у reducer-only gate-сценариев относится к budget report data и не меняет trace attribution semantics.
372
+
373
+ Top core phases:
374
+
375
+ | Сценарий | Фазы |
376
+ | --- | --- |
377
+ | `movement update` | `core.rootReducer` 0.805ms (98.5%); `core.commit.total` 0.003ms (0.3%); `core.prepareAction.total` 0.002ms (0.2%) |
378
+ | `projectile lifetime update` | `core.rootReducer` 0.402ms (97.5%); `core.commit.total` 0.002ms (0.5%); `core.prepareAction.total` 0.002ms (0.4%) |
379
+ | `despawnOn cleanup` | `core.rootReducer` 0.477ms (93.6%); `core.prepareAction.total` 0.009ms (1.7%); `core.createDispatch` 0.005ms (0.9%) |
380
+ | `sprite sync reaction` | `core.reactions.total` 2.369ms (67.3%); `core.rootReducer` 1.095ms (31.7%); `core.commit.total` 0.007ms (0.2%) |
381
+
382
+ Top entities phases:
383
+
384
+ | Сценарий | Фазы |
385
+ | --- | --- |
386
+ | `movement update` | `entities.reduce.total` 0.803ms (100.0%); `entities.reduce.publicBatch.total` 0.801ms (99.8%); `entities.reduce.publicBatch.userReducer` 0.523ms (65.4%); `entities.reduce.publicBatch.postProcess` 0.239ms (29.9%) |
387
+ | `projectile lifetime update` | `entities.reduce.total` 0.400ms (99.9%); `entities.reduce.publicBatch.total` 0.398ms (99.7%); `entities.reduce.publicBatch.postProcess` 0.239ms (60.0%); `entities.reduce.publicBatch.userReducer` 0.122ms (30.7%) |
388
+ | `despawnOn cleanup` | `entities.reduce.total` 0.470ms (99.9%); `entities.reduce.publicBatch.total` 0.448ms (95.7%); `entities.reduce.publicBatch.postProcess` 0.278ms (62.1%); `entities.reduce.publicBatch.userReducer` 0.131ms (29.2%) |
389
+ | `sprite sync reaction` | `entities.reactions.total` 2.367ms (99.9%); `entities.reactions.user` 1.701ms (74.0%); `entities.reduce.total` 1.090ms (100.0%); `entities.reduce.publicBatch.total` 1.087ms (99.7%); `entities.reactions.captureScope` 0.586ms (23.9%) |
390
+
391
+ Residual unattributed time:
392
+
393
+ | Сценарий | Core residual | Entities reduce residual | Entities reactions residual |
394
+ | --- | ---: | ---: | ---: |
395
+ | `movement update` | 0.002ms (0.2%) | 0.001ms (0.1%) | 0.000ms |
396
+ | `projectile lifetime update` | 0.002ms (0.4%) | 0.001ms (0.1%) | 0.000ms |
397
+ | `despawnOn cleanup` | 0.004ms (0.8%) | 0.001ms (0.3%) | 0.000ms |
398
+ | `sprite sync reaction` | 0.003ms (0.1%) | 0.001ms (0.1%) | 0.045ms (1.9%) |
399
+
400
+ Основной кандидат для следующего optimization ТЗ — `sprite sync reaction / 50 000`: gate ratio `11.70x`, а trace показывает `core.reactions.total` 2.369ms и внутри него `entities.reactions.user` 1.701ms. Следующий шаг должен разбирать public reaction path и scoped access/deps work в `entities.reactions.user`; `entities.reactions.captureScope` 0.586ms и `entities.reduce.publicBatch.total` 1.087ms остаются соседними вторичными целями.
401
+
402
+ ### Итог unit composition benchmark
403
+
404
+ Команда:
405
+
406
+ ```bash
407
+ pnpm run bench:entities:record -- --runs 3 --label unit-composition-baseline --include gate,trace --row-counts 50000
408
+ ```
409
+
410
+ Артефакты:
411
+
412
+ - [`unit-composition-baseline.json`](../../.bench/entities/unit-composition-baseline.json)
413
+ - [`unit-composition-baseline.md`](../../.bench/entities/unit-composition-baseline.md)
414
+
415
+ Окружение record: `2026-06-15T13:29:56.184Z`, Git `381ee5e0f982`, branch `entities`, status `dirty`, Node `v24.16.0`, CPU `Apple M1 Max`, package manager `pnpm/10.33.0`.
416
+
417
+ Активный composition benchmark заменяет узкий `sprite sync reaction` на `unit frame composition`: entity `unit` содержит `movementActor`, `spriteSyncActor`, `healthActor` и `targetingActor`. `healthActor` имеет state effect, который читает `getState()`, scoped `entities().get("movementActor")` и `entities().get("targetingActor")`, затем пишет агрегат во внешний `world` adapter через deps. Поэтому этот record не является прямым сравнением со старым `sprite sync reaction`; старые значения выше остаются историческим baseline для reaction-only сценария.
418
+
419
+ Gate `50 000`:
420
+
421
+ | Сценарий | SoA median | entities median | Ratio | Бюджет | Статус | RSD |
422
+ | --- | ---: | ---: | ---: | ---: | --- | ---: |
423
+ | `movement update` | 0.236ms | 0.285ms | 1.21x | 1.50x | pass | 3.8% |
424
+ | `projectile lifetime update` | 0.180ms | 0.218ms | 1.22x | 1.50x | fail | 49.5% |
425
+ | `despawnOn cleanup` | 0.277ms | 0.421ms | 1.51x | 2.00x | fail | 2.6% |
426
+ | `unit frame composition` | 1.121ms | 10.269ms | 9.26x | 2.00x | fail | 6.7% |
427
+
428
+ Trace totals:
429
+
430
+ | Сценарий | Trace total | Gate entities median | `traceTotal / gateEntityMedian` | Transitions | RSD |
431
+ | --- | ---: | ---: | ---: | ---: | ---: |
432
+ | `movement update` | 0.293ms | 0.285ms | 1.03x | 450 | 0.7% |
433
+ | `projectile lifetime update` | 0.227ms | 0.218ms | 1.04x | 450 | 1.2% |
434
+ | `despawnOn cleanup` | 0.420ms | 0.421ms | 1.00x | 450 | 1.4% |
435
+ | `unit frame composition` | 10.465ms | 10.269ms | 1.02x | 450 | 1.3% |
436
+
437
+ Trace для `unit frame composition / 50 000`:
438
+
439
+ | Фаза | Median | Share |
440
+ | --- | ---: | ---: |
441
+ | `core.effects.total` | 6.406ms | 61.7% от transition |
442
+ | `core.rootReducer` | 3.210ms | 31.1% от transition |
443
+ | `core.reactions.total` | 0.682ms | 6.5% от transition |
444
+ | `entities.effects.resolve` | 2.046ms | 99.9% от parent |
445
+ | `entities.effects.invoke` | 4.300ms | 99.8% от parent |
446
+ | `entities.reduce.publicBatch.total` | 2.957ms | 93.1% от `entities.reduce.total` |
447
+ | `entities.reactions.user` | 0.620ms | 91.6% от `entities.reactions.total` |
448
+
449
+ Trace coverage для нового сценария закрывает attribution: `core top-level` 100.0%, `entities reduce` 99.9%, `entities reactions` 99.7%, `entities effects` 99.8%. Предупреждений `traceTotal / gateEntityMedian > 2.00x` нет.
450
+
451
+ Вывод: новый пример лучше отражает реальный игровой unit, но смещает главный hot spot с reaction-only пути на effects path. Основной кандидат для следующего optimization ТЗ — `unit frame composition / 50 000`: `core.effects.total` занимает `6.406ms`, из них `entities.effects.invoke` `4.300ms`, а `entities.effects.resolve` `2.046ms`. Это включает capture scope для effect, создание scoped access/self и пользовательский effect, который читает root state, две entity views и пишет во внешний adapter. Вторичные цели — `entities.reduce.publicBatch.total` `2.957ms` для четырех actor batches и `entities.reactions.user` `0.620ms`.
452
+
453
+ Остаточные риски record:
454
+
455
+ 1. `projectile lifetime update` имеет высокий разброс gate median `49.5%`: median ratio ниже бюджета, но один из трех runs провалил per-run gate.
456
+ 2. `despawnOn cleanup` имеет median ratio ниже бюджета (`1.51x` против `2.00x`), но record status остался `fail` из-за per-run gate.
457
+ 3. Бюджет `unit frame composition` пока не является приемочным production budget: он фиксирует новый более реалистичный baseline для будущих оптимизаций effects path.
458
+
459
+ ### 4. Уменьшить validation overhead в spawn path
460
+
461
+ Ожидаемый выигрыш: средний для текущего измеряемого `TICK`, высокий для реальных workloads со spawn/despawn. Bulk commit, пакетный reserve и payload scope уже вынесены в `spawn-commit.ts`; оставшийся кандидат — validation/staging до physical commit.
462
+
463
+ Затронутые места:
464
+
465
+ - `packages/entities/src/runtime/transaction.ts`: `validateSpawnSpec`, `validateActorPayload`, `stageSpawnAction`.
466
+ - `packages/entities/src/runtime/spawn-commit.ts`: planning, reserve, physical commit и payload scope.
467
+
468
+ Что исправлять:
469
+
470
+ 1. Предкомпилировать spawn schema validators по template, чтобы не делать `Object.entries(schema)` и повторные проверки формы на каждую строку.
471
+ 2. Сохранить текущие duplicate id и payload diagnostics без переноса validation в commit phase.
472
+ 3. Не менять public spawn recipe API и schema descriptors.
473
+
474
+ Критерий приемки:
475
+
476
+ - Добавить отдельный gate/trace scenario для timed spawn batch.
477
+ - Не ухудшить `movement update`, `projectile lifetime update` и bulk spawn benchmark на `50k`.
478
+
479
+ ### 5. Снизить постоянный overhead `manager.transition` для bucket storage
480
+
481
+ Ожидаемый выигрыш: средний-низкий до исправления entity kernel, но важный для достижения бюджета `1.5x`.
482
+
483
+ Затронутые места:
484
+
485
+ - `packages/core/src/runtime/kernel/createMachineManagerFactory.ts`: `createDispatch`, `coreTransition`, hook phases.
486
+ - `packages/core/src/runtime/kernel/bucketRuntime.ts`: `prepareAction`, `beforeReduce`, `reduce`, `commit`, `runReactions`, `runEffects`.
487
+ - `packages/entities/src/runtime/storage.ts`: `prepareAction`, `commit`, `effects`, `reactions`.
488
+
489
+ Что исправлять:
490
+
491
+ 1. Сократить создание readonly action views и spread-объектов в bucket runtime на hot path.
492
+ 2. Не проходить пустые hook/interceptor phases, если registry заранее знает, что callbacks отсутствуют.
493
+ 3. Для bucket storage с одним активным runtime рассмотреть прямой reduce path без лишних loops по пустым buckets.
494
+ 4. Не создавать `Map` в dispatch runtime, если ни один plugin/storage не использует runtime slots. Для `entities` slot нужен, но можно заменить на специализированный carrier или переиспользуемый объект внутри одного transition.
495
+
496
+ Критерий приемки:
497
+
498
+ - Gate reducer-only `50k` должен приближаться к бюджету `1.50x` без регрессий соседних сценариев больше `10%`.
499
+ - Trace должен показать снижение постоянных core/bucket фаз (`core.createDispatch`, `core.prepareAction.total`, `core.commit.total`, hooks и bucket phases) для reducer-only сценариев.
500
+ - Legacy diagnostics `raw entity kernel` не используется как strict gate или источник production attribution.
501
+ - Изменения core должны проходить общий `pnpm run test`, `pnpm run test:types`, `pnpm run check-types`.
502
+
503
+ ### 6. Уточнить trace breakdown перед крупными изменениями
504
+
505
+ Ожидаемый выигрыш: непрямой. Нужен, чтобы не оптимизировать по неверной гипотезе.
506
+
507
+ Что добавить:
508
+
509
+ 1. Дополнительные trace phases для cleanup и reactions, если текущая детализация не объясняет изменение gate.
510
+ 2. Timed spawn scenario в gate/trace workflow, где spawn входит в measured участок.
511
+ 3. Allocation counters для hot scenarios, хотя бы retained heap plus operation count.
512
+ 4. Если нужен synthetic calibration benchmark, оформить его отдельной legacy-командой и не включать в optimization gates.
513
+
514
+ Критерий приемки:
515
+
516
+ - Trace должен объяснять не менее `80%` времени `despawnOn cleanup / public transition` и `sprite sync reaction / public transition`.
517
+ - Существующие gate таблицы должны остаться сопоставимыми с текущим baseline.
518
+
519
+ ## Рекомендуемый порядок работ
520
+
521
+ Актуальный порядок идет от `unit-composition-baseline`, потому что активный benchmark теперь измеряет более реалистичный игровой unit composition, а не только reaction-only путь.
522
+
523
+ 1. Entity effects path для `unit frame composition`: уменьшить `entities.effects.invoke` и `entities.effects.resolve`, особенно capture scope, scoped access/self creation и user effect с `getState()`/`entities()` reads.
524
+ 2. `reduceAcceptedBatch` для multi-actor unit frame: снизить `entities.reduce.publicBatch.total` при четырех actor batches без регрессий reducer-only сценариев.
525
+ 3. Reaction user path: сохранить прошлые улучшения `sprite` reaction и дальше снижать `entities.reactions.user` в составе `unit frame composition`.
526
+ 4. Spawn/lifecycle allocation: validators, payload arrays, capacity reservation, отдельный timed spawn scenario.
527
+ 5. Core bucket overhead: убрать пустые phases и лишние action views после стабилизации entity-specific hot spots.
528
+ 6. Benchmark breakdown: при необходимости добавить более детальные effect phases для capture scope, deps/self creation и user callback.
529
+
530
+ После каждого пункта сохранять отдельный record:
531
+
532
+ ```bash
533
+ pnpm run bench:entities:record -- --runs 5 --label <short-name> --include gate,trace --row-counts 50000
534
+ pnpm run bench:entities:compare -- .bench/entities/unit-composition-baseline.json .bench/entities/<short-name>.json
535
+ ```
536
+
537
+ Отчет считать успешным для этапа только если целевой сценарий ускорился, соседние сценарии не получили регрессию больше `10%`, `traceTotal / gateEntityMedian` остался ниже `2.00x`, а новые runtime контракты покрыты тестами.