mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-26 07:57:36 +00:00
docs: add ECS pattern survey appendix
Captures the bitECS / miniplex / koota / ECSY / Bevy survey from the world-consolidation analysis: structural patterns adopted (substrate-deep with domain-colocated components, small public API, reactive bridging via reactive(Map), brand-typed entity IDs), patterns explicitly rejected (replace-on-write, SoA/archetype storage, opaque entity IDs, substrate-side parent/child relations), and revisit thresholds. Cross-links from ADR 0008 Supporting Documents table. Amp-Thread-ID: https://ampcode.com/threads/T-019dd18c-2255-702a-9a0c-851d10fcd420 Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
@@ -239,18 +239,19 @@ The design goal is to preserve ECS modularity while keeping render throughput wi
|
||||
|
||||
Companion architecture documents that expand on the design in this ADR:
|
||||
|
||||
| Document | Description |
|
||||
| ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------- |
|
||||
| [Entity Interactions](../architecture/entity-interactions.md) | Maps all current entity relationships and interaction patterns — the ECS migration baseline |
|
||||
| [Entity System Structural Problems](../architecture/entity-problems.md) | Detailed problem catalog with line-level code references motivating the ECS migration |
|
||||
| [Proto-ECS Stores](../architecture/proto-ecs-stores.md) | Inventory of existing Pinia stores that already partially implement ECS patterns |
|
||||
| [ECS Target Architecture](../architecture/ecs-target-architecture.md) | Full target architecture showing how entities and interactions transform under ECS |
|
||||
| [ECS Migration Plan](../architecture/ecs-migration-plan.md) | Phased migration roadmap with shipping milestones and go/no-go criteria |
|
||||
| [ECS Lifecycle Scenarios](../architecture/ecs-lifecycle-scenarios.md) | Before/after walkthroughs of lifecycle operations (node removal, link creation, etc.) |
|
||||
| [World API and Command Layer](../architecture/ecs-world-command-api.md) | How each lifecycle scenario maps to a command in the World API |
|
||||
| [Subgraph Boundaries and Widget Promotion](../architecture/subgraph-boundaries-and-promotion.md) | Design rationale for modeling subgraphs as node components, not separate entities |
|
||||
| [Appendix: Critical Analysis](../architecture/appendix-critical-analysis.md) | Independent verification of the accuracy of the architecture documents |
|
||||
| [Change Tracker](../architecture/change-tracker.md) | Documents the current undo/redo system that ECS cross-cutting concerns will replace |
|
||||
| Document | Description |
|
||||
| ------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------- |
|
||||
| [Entity Interactions](../architecture/entity-interactions.md) | Maps all current entity relationships and interaction patterns — the ECS migration baseline |
|
||||
| [Entity System Structural Problems](../architecture/entity-problems.md) | Detailed problem catalog with line-level code references motivating the ECS migration |
|
||||
| [Proto-ECS Stores](../architecture/proto-ecs-stores.md) | Inventory of existing Pinia stores that already partially implement ECS patterns |
|
||||
| [ECS Target Architecture](../architecture/ecs-target-architecture.md) | Full target architecture showing how entities and interactions transform under ECS |
|
||||
| [ECS Migration Plan](../architecture/ecs-migration-plan.md) | Phased migration roadmap with shipping milestones and go/no-go criteria |
|
||||
| [ECS Lifecycle Scenarios](../architecture/ecs-lifecycle-scenarios.md) | Before/after walkthroughs of lifecycle operations (node removal, link creation, etc.) |
|
||||
| [World API and Command Layer](../architecture/ecs-world-command-api.md) | How each lifecycle scenario maps to a command in the World API |
|
||||
| [Subgraph Boundaries and Widget Promotion](../architecture/subgraph-boundaries-and-promotion.md) | Design rationale for modeling subgraphs as node components, not separate entities |
|
||||
| [Appendix: Critical Analysis](../architecture/appendix-critical-analysis.md) | Independent verification of the accuracy of the architecture documents |
|
||||
| [Appendix: ECS Pattern Survey](../architecture/appendix-ecs-pattern-survey.md) | Survey of bitECS, miniplex, koota, ECSY, and Bevy — patterns adopted, departed, when to revisit |
|
||||
| [Change Tracker](../architecture/change-tracker.md) | Documents the current undo/redo system that ECS cross-cutting concerns will replace |
|
||||
|
||||
## Notes
|
||||
|
||||
|
||||
282
docs/architecture/appendix-ecs-pattern-survey.md
Normal file
282
docs/architecture/appendix-ecs-pattern-survey.md
Normal file
@@ -0,0 +1,282 @@
|
||||
# Appendix: ECS Pattern Survey
|
||||
|
||||
_A survey of mainstream Entity Component System libraries — bitECS, miniplex,
|
||||
koota, ECSY, and Bevy — captured during the world-consolidation analysis that
|
||||
shipped slice 1 of [ADR 0008](../adr/0008-entity-component-system.md). This
|
||||
appendix records which structural patterns our `src/world/` substrate adopts,
|
||||
which it deliberately departs from, and where the trade-offs are load-bearing
|
||||
rather than incidental._
|
||||
|
||||
The source material is [temp/plans/world-consolidation.md §C "ECS pattern
|
||||
alignment"](../../temp/plans/world-consolidation.md). The in-code anchors for
|
||||
the load-bearing constraints discussed below are the doc-comments in
|
||||
[src/world/world.ts](../../src/world/world.ts) (storage strategy) and
|
||||
[src/world/entityIds.ts](../../src/world/entityIds.ts) (identity contract) —
|
||||
see §3 below.
|
||||
|
||||
---
|
||||
|
||||
## 1. Survey Comparison
|
||||
|
||||
Five libraries were sampled for structural patterns: where component
|
||||
definitions live relative to the substrate, how components are declared,
|
||||
how entities are identified, and roughly how large the substrate's public
|
||||
surface is. Sources: the linked READMEs and docs.
|
||||
|
||||
| Library | Component placement | Component definition style | Entity ID type | Approx. # core exports |
|
||||
| ------------------------------------------------- | ------------------------------------ | ----------------------------- | -------------------- | ----------------------: |
|
||||
| [bitECS](https://github.com/NateTheGreatt/bitECS) | Outside the substrate; user's choice | plain arrays / objects | `number` (unbranded) | ~12 |
|
||||
| [miniplex](https://github.com/hmans/miniplex) | Colocated with the `Entity` type | properties on a TS type | plain object ref | ~5 |
|
||||
| [koota](https://github.com/pmndrs/koota) | Colocated with the consumer | `trait({...})` factory | numeric `.id()` | ~15 (core) + ~8 (react) |
|
||||
| [ECSY](https://github.com/ecsyjs/ecsy) | User's choice | `class extends Component` | `Entity` object | ~10 |
|
||||
| [Bevy](https://bevyengine.org/) (Rust, for shape) | Plugin-owned (industry std) | `#[derive(Component)] struct` | `Entity(u64)` | n/a |
|
||||
|
||||
Two structural patterns are unanimous across the surveyed libraries:
|
||||
|
||||
1. **Component definitions live with the code that owns the data**, not
|
||||
inside the substrate package. Whether by explicit recommendation
|
||||
(Bevy plugins, koota's colocation guidance) or by default (bitECS,
|
||||
miniplex), no surveyed substrate ships pre-defined component types.
|
||||
2. **Substrate surface area is small** — bitECS at ~12 exports, koota at
|
||||
~15, miniplex at ~5. ECSY is the outlier with a wider class hierarchy.
|
||||
|
||||
Our slice-1 end state — five source files under
|
||||
[src/world/](../../src/world/), ~14 exported names total — sits squarely in
|
||||
this band. See [temp/plans/world-consolidation.md
|
||||
§1](../../temp/plans/world-consolidation.md) for the file-by-file breakdown.
|
||||
|
||||
---
|
||||
|
||||
## 2. Patterns We Adopt
|
||||
|
||||
### 2.1 Substrate is deep; components live in domain code
|
||||
|
||||
The mainstream convention is that the ECS substrate exposes only the
|
||||
machinery — entities, component keys, a World — and component definitions
|
||||
live next to the system, store, or feature module that owns the data.
|
||||
This is the Bevy / miniplex / koota convention by design and the bitECS /
|
||||
ECSY convention by default.
|
||||
|
||||
Our substrate follows the same shape: `src/world/` contains entity-ID
|
||||
brands, the `ComponentKey` definition primitive, and the `World`
|
||||
interface, but no domain-specific component types. Slice 1 places
|
||||
`WidgetValueComponent`, `WidgetContainerComponent`, and the `widgetParent`
|
||||
reverse-lookup in
|
||||
[src/stores/widgetComponents.ts](../../src/stores/widgetComponents.ts),
|
||||
next to [widgetValueStore.ts](../../src/stores/widgetValueStore.ts) — the
|
||||
module that already owns widget value state.
|
||||
|
||||
This keeps the substrate / domain seam crisp: the World knows how to store
|
||||
and look up arbitrary components keyed by entity ID; the domain layer
|
||||
knows what a "widget value" is. It also aligns with the AGENTS.md DDD
|
||||
guidance to group code by bounded context. Future components follow the
|
||||
same rule — `PositionComponent`, when it lands, will live with the layout
|
||||
domain rather than inside the substrate
|
||||
([temp/plans/world-consolidation.md
|
||||
§4.1](../../temp/plans/world-consolidation.md)).
|
||||
|
||||
### 2.2 Small public API
|
||||
|
||||
The substrate exports ~14 names — comparable to bitECS (~12) and koota
|
||||
(~15), much smaller than ECSY's class hierarchy. This is a deliberate
|
||||
target: every exported name is a contract a contributor must understand
|
||||
before extending the World, and every export is a potential migration
|
||||
cost when the substrate evolves.
|
||||
|
||||
The `Brand` / `EntityId` / `ComponentKey` / `World` / `worldInstance`
|
||||
split keeps each module single-purpose
|
||||
([temp/plans/world-consolidation.md
|
||||
§A.F5–F9](../../temp/plans/world-consolidation.md)). `Brand<T,Tag>` is 5
|
||||
LOC and shared across all branded ID kinds. `ComponentKey<TData,TEntity>`
|
||||
carries a two-parameter phantom that enables cross-kind compile-time
|
||||
checking. `asGraphId` is a single named boundary cast. The two explicit
|
||||
factories `nodeEntityId` / `widgetEntityId` are kept rather than collapsed
|
||||
into a parameterized helper because slice 2/3/4 will add factories with
|
||||
different parameter tuples (`rerouteEntityId`, `linkEntityId`,
|
||||
`slotEntityId`); the explicit-factory pattern scales linearly with new
|
||||
entity kinds without growing the helper's signature.
|
||||
|
||||
### 2.3 Reactive bridging via existing storage proxy
|
||||
|
||||
bitECS, koota, and miniplex bolt on a separate `onChange` event bus when
|
||||
a consumer wants reactive notifications. koota's React layer
|
||||
(`useTrait(entity, ComponentKey)`) is the closest analog to what
|
||||
`useUpstreamValue` and future composables want.
|
||||
|
||||
Because our World stores values inside Vue's `reactive(Map<EntityId, ...>)`,
|
||||
a plain `computed(() => world.getComponent(id, key))` already provides
|
||||
fine-grained per-`(entity, component)` tracking — no separate event bus
|
||||
is needed. **This is a real Vue-specific advantage.** The Vue tracker and
|
||||
the ECS storage are the same mechanism, so reactivity falls out of the
|
||||
storage choice rather than being layered on top.
|
||||
|
||||
### 2.4 Brand-typed entity IDs
|
||||
|
||||
No surveyed TypeScript ECS uses branded IDs. bitECS uses unbranded
|
||||
`number`, miniplex uses plain object references, koota uses a numeric
|
||||
`.id()`. Our `Brand<T, Tag>` over each entity kind enables the
|
||||
`@ts-expect-error` cross-kind test in
|
||||
[world.test.ts](../../src/world/world.test.ts) and documents slice-2/3/4
|
||||
entity kinds at compile time.
|
||||
|
||||
This is a deliberate departure rather than an accident. It earns its keep
|
||||
once `Position` lands on `NodeEntityId | RerouteEntityId` (slice 2) and
|
||||
`Connectivity` lands on `SlotEntityId` (slice 4); without brands, those
|
||||
component-key declarations would accept any numeric ID and silently allow
|
||||
cross-kind misuse.
|
||||
|
||||
---
|
||||
|
||||
## 3. Patterns We Explicitly Do NOT Adopt
|
||||
|
||||
Each of the following is a real industry idiom we considered and rejected
|
||||
on load-bearing grounds. None of these are pure performance trade-offs.
|
||||
|
||||
### 3.1 Replace-on-write usage idioms
|
||||
|
||||
koota's `entity.set(Position, {...})` and miniplex's `world.add(entity)`
|
||||
**replace** component values with new objects on each write. Adopting
|
||||
either would break
|
||||
[BaseWidget.\_state](../../src/lib/litegraph/src/widgets/BaseWidget.ts)
|
||||
shared reactive identity — the contract that lets DOM widget overrides,
|
||||
`useProcessedWidgets` memoization, and the 40+ extension ecosystem all
|
||||
read the same proxy. Our `setComponent(id, key, sharedRef)` is
|
||||
identity-preserving by design, and `widgetValueStore.registerWidget`
|
||||
returns the same proxy that `getComponent` returns
|
||||
([temp/plans/world-consolidation.md §3.1 step
|
||||
6](../../temp/plans/world-consolidation.md), reactive-identity test).
|
||||
|
||||
### 3.2 SoA / archetype storage
|
||||
|
||||
bitECS, koota, and miniplex use sparse-set / archetype storage internally
|
||||
for cache locality. Our `reactive(Map<EntityId, unknown>)` is closer to
|
||||
ECSY's AoS — slower iteration but **integrates natively with Vue's
|
||||
tracking**.
|
||||
|
||||
The surface trade-off is performance; the deeper trade-off is identity.
|
||||
SoA storage spreads each component's fields across parallel typed arrays,
|
||||
so the per-entity "row object" is reconstructed on read. **A future
|
||||
migration to SoA would lose the proxy on the row object** — and with it
|
||||
the shared-reactive-identity contract that `BaseWidget._state` and the
|
||||
`widgetValueStore` facade rely on. This is a load-bearing constraint, not
|
||||
just a perf optimization decision.
|
||||
|
||||
The contract is pinned in the doc-comment at the top of
|
||||
[src/world/world.ts](../../src/world/world.ts) (Appendix D.1 of the
|
||||
world-consolidation plan — copied here for proximity):
|
||||
|
||||
```ts
|
||||
/**
|
||||
* Storage strategy: AoS (per-entity reactive object reference) backed by
|
||||
* `reactive(Map)`. Component values are stored by reference; mutating a
|
||||
* value's fields propagates to all readers through Vue's reactive proxy.
|
||||
* `setComponent(id, key, ref)` is intentionally identity-preserving.
|
||||
*
|
||||
* NOT a sparse-set / archetype store. A future SoA migration would break
|
||||
* the shared-reactive-identity contract that BaseWidget._state and the
|
||||
* widgetValueStore facade rely on; do not refactor without revisiting
|
||||
* those consumers.
|
||||
*/
|
||||
```
|
||||
|
||||
### 3.3 Auto-generated opaque entity IDs
|
||||
|
||||
bitECS and koota assume IDs are opaque numbers — `lastId++`, with no
|
||||
external structure. miniplex uses plain object references with the same
|
||||
property.
|
||||
|
||||
Our `widgetEntityId(rootGraphId, nodeId, name)` is **deterministic and
|
||||
content-addressed**. Consumers consistently pass `rootGraph.id`, so a
|
||||
widget viewed at different subgraph depths shares identity with itself.
|
||||
Migrating to opaque numeric IDs would break cross-subgraph value sharing —
|
||||
the same widget at depth 0 and depth 2 would receive different IDs and
|
||||
diverge.
|
||||
|
||||
The contract is pinned in the doc-comment at the top of
|
||||
[src/world/entityIds.ts](../../src/world/entityIds.ts) (Appendix D.2 of
|
||||
the plan):
|
||||
|
||||
```ts
|
||||
/**
|
||||
* Entity IDs are deterministic, content-addressed, and string-prefix
|
||||
* encoded — NOT opaque numeric IDs (cf. bitECS, koota, miniplex).
|
||||
*
|
||||
* `widgetEntityId(rootGraphId, nodeId, name)` is load-bearing:
|
||||
* consumers consistently pass `rootGraph.id` so widgets viewed at
|
||||
* different subgraph depths share identity. Migrating to numeric IDs
|
||||
* would break cross-subgraph value sharing. See ADR 0008 and
|
||||
* widgetValueStore for the canonical keying contract.
|
||||
*/
|
||||
```
|
||||
|
||||
### 3.4 Substrate-side parent/child relations
|
||||
|
||||
Bevy ships `Parent` / `Children` components at the substrate layer; Flecs
|
||||
ships first-class relations. These are useful when many subsystems need
|
||||
hierarchical traversal at storage-near speeds.
|
||||
|
||||
We treat parent lookup as a query over a domain component instead.
|
||||
`widgetParent(world, widgetId)` is a **query over `WidgetContainer`**,
|
||||
defined alongside the component in
|
||||
[src/stores/widgetComponents.ts](../../src/stores/widgetComponents.ts). It
|
||||
runs in the domain layer because today only one subsystem (the widget
|
||||
domain) needs parent lookup. We may revisit this if multiple slices need
|
||||
a shared traversal API; until then, keeping parent lookup domain-local
|
||||
preserves the substrate's "no domain knowledge" property.
|
||||
|
||||
---
|
||||
|
||||
## 4. When to Revisit
|
||||
|
||||
The choices in §3 are deliberate but not eternal. Each has a revisit
|
||||
threshold.
|
||||
|
||||
**SoA / archetype storage.** The break-even point against `reactive(Map)`
|
||||
iteration is roughly **>10k entities per component** in steady-state hot
|
||||
paths. ComfyUI's projected widget count through slice 4 stays well under
|
||||
that. The watch signal is whether a render-loop or solver-loop pass
|
||||
demonstrably dominates frame time on `entitiesWith(WidgetValueComponent)`
|
||||
or any successor query — not just micro-benchmarks of `Map.get`.
|
||||
|
||||
If we cross that threshold, the migration is non-trivial: SoA loses the
|
||||
proxy on the row object (see §3.2), so a SoA World must either
|
||||
reconstruct proxies on read (defeating the perf gain) or move
|
||||
shared-identity reads back to a domain-side cache. ADR 0008's
|
||||
"Render-Loop Performance Implications and Mitigations" section already
|
||||
enumerates the planned mitigations (frame-stable query caches, archetype
|
||||
buckets, profiling-gated storage upgrades behind the World API).
|
||||
|
||||
**Replace-on-write idioms.** Revisitable only if the 40+ extension
|
||||
ecosystem moves off `BaseWidget._state` shared identity entirely — a
|
||||
separate, larger slice with explicit cost analysis (re-entry, DOM widget
|
||||
options.getValue overrides, `linkedWidgets` fan-out,
|
||||
`useProcessedWidgets` memoization invalidation), out of scope for the
|
||||
current ADR 0008 implementation
|
||||
([temp/plans/world-consolidation.md
|
||||
§F](../../temp/plans/world-consolidation.md)).
|
||||
|
||||
**Opaque entity IDs.** Revisitable only if the cross-subgraph identity
|
||||
contract is dropped. Today widget value sharing across subgraph depths
|
||||
depends on it; slice 2 may extend the same contract to `nodeEntityId`
|
||||
for spatial reads. Until the product requirement changes, opaque IDs
|
||||
would be a regression.
|
||||
|
||||
**Substrate-side parent/child relations.** Revisitable when ≥2 subsystems
|
||||
need parent traversal. At one consumer it stays domain-local.
|
||||
|
||||
---
|
||||
|
||||
## 5. Cross-References
|
||||
|
||||
- [ADR 0008 — Entity Component System](../adr/0008-entity-component-system.md)
|
||||
for the full target taxonomy and migration strategy.
|
||||
- [ECS Target Architecture](./ecs-target-architecture.md) for the full
|
||||
end-state shape.
|
||||
- [ECS Migration Plan](./ecs-migration-plan.md) for shipping milestones.
|
||||
- [Appendix: Critical Analysis](./appendix-critical-analysis.md) for the
|
||||
independent verification of the architecture documents.
|
||||
- [temp/plans/world-consolidation.md
|
||||
§C](../../temp/plans/world-consolidation.md) — the source material for
|
||||
this appendix; §A.F5–F9 explains the kept-as-is substrate choices,
|
||||
§D.1/§D.2 hold the canonical doc-comment wording for the in-code
|
||||
contracts.
|
||||
Reference in New Issue
Block a user