12 — Feeding Frenzy Benchmark¶
A performance benchmark — the same gameplay implemented across three ECS architectures and nine iteration styles.
Source: com.trecs.core/Samples~/Tutorials/12_FeedingFrenzyBenchmark/
What it does¶
The fish-eating-meals simulation from 07 — Feeding Frenzy, implemented three ways. Switch approaches and iteration styles at runtime to compare performance. Default presets span 5,000 to 1,000,000 fish plus a configurable ratio of meals.
Three approaches¶
Branching¶
Eating/NotEating tracked via component values. Systems iterate all entities and branch on value:
// All fish iterated, check if eating
if (fish.TargetMeal.IsNull)
{
// Idle behavior
}
else
{
// Eating behavior
}
Trade-off: Simplest code, but iterates entities that don't need processing.
Sets¶
Uses IEntitySet for membership tracking. Systems filter by set:
[ForEachEntity(typeof(FrenzyTags.Fish), Set = typeof(FrenzySets.NotEating))]
void IdleBob(in Fish fish) { ... }
Trade-off: Visits only relevant entities. No data movement on membership flips. Sparse iteration.
Partitions¶
Uses IPartitionedBy for partition separation:
[ForEachEntity(typeof(FrenzyTags.Fish), typeof(FrenzyTags.NotEating))]
void IdleBob(in Fish fish) { ... }
Trade-off: Dense, cache-friendly iteration. Partition changes copy component data to the new partition's buffers.
Nine iteration styles¶
Each approach is tested with several iteration patterns:
| Style | Description |
|---|---|
| ForEachMethodAspect | [ForEachEntity] with aspect parameter |
| ForEachMethodComponents | [ForEachEntity] with component ref parameters |
| AspectQuery | Manual Aspect.Query(World).WithTags<T>() loop |
| QueryGroupSlices | World.Query().GroupSlices() with direct buffer access |
| RawComponentBuffersJob | Manual Burst job with NativeComponentBufferRead/Write |
| ForEachMethodAspectJob | [ForEachEntity] aspect with [WrapAsJob] |
| ForEachMethodComponentsJob | [ForEachEntity] components with [WrapAsJob] |
| WrapAsJobAspect | Static [WrapAsJob] method with aspect |
| WrapAsJobComponents | Static [WrapAsJob] method with components |
Runtime controls¶
| Key | Action |
|---|---|
| F1 / F2 / F3 | Switch to Branching / Sets / Partitions |
| Tab / Shift+Tab | Cycle iteration style |
| Up / Down | Adjust entity count preset |
The display shows simulation Hz, FPS, entity count, and memory usage in real time.
Fish count presets¶
Logarithmically spaced from 5,000 to 1,000,000 fish (with proportional meals) for testing at different scales. The min/max are configurable in the scene's SampleSettings.Common.
Key implementation details¶
Population management¶
Fish and meal counts lerp toward the desired preset. The spawner removes idle entities first to minimize disruption.
GPU rendering¶
At high entity counts, RendererSystem uses GPU-instanced indirect rendering. A Burst job marshals Position, Rotation, Scale, and Color from ECS components into GPU instance buffers each frame.
Bidirectional entity references¶
Fish ↔ Meal pairing uses EntityHandle cross-references with cleanup handlers, preventing dangling references when either entity is removed.
Concepts introduced¶
- Partitions are fastest for dense iteration — entities are contiguous in memory
- Sets avoid storage fragmentation — no combinatorial blowup across dimensions
- Branching is simplest but slowest — iterates everything, branches per entity
- Jobs provide the biggest speedup — Burst + parallel iteration scales with core count
- Source generation produces efficient code —
[ForEachEntity]and[WrapAsJob]generate tight loops comparable to hand-written jobs