Skip to content

Commit d61a1c6

Browse files
authored
Spike/allocations (#21)
* Updating pools to expose Clear and rename RemoveAll to Clear * Moved batched systems over to use refs * Added missing computed component benchmark * Got first iteration of Multiplexer in there * Added more benchmarks and added ref based multiplexer * Good example for allocations * Updated tests * Added lazy computers * Moved more over to lazy computers * Fixed up resolution on ref computed field * Updated tests to batch correctly * Fixed up tests and made IsDirty true by default to mimic old behaviour * Bumped version * Updated docs and readme
1 parent 9befd7f commit d61a1c6

65 files changed

Lines changed: 2633 additions & 353 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/build-and-test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: Build And Test
22
on: [push]
33

44
env:
5-
BuildVersion: '12.0.${{github.run_number}}'
5+
BuildVersion: '13.0.${{github.run_number}}'
66
SolutionFile: 'src/EcsR3.sln'
77

88
jobs:

docs/ecs-r3/framework/computeds.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,18 @@ Now we can all laugh at the name here, but this is basically the same as the pre
2828

2929
This provides you with an `IComputedEntityGroup` as the `DataSource` then you translate them into whatever you want `T` to be.
3030

31+
> This inherits from the lazy computed line, so it will only refresh its value when you read its `Value`(and it has awaiting changes) or you explicitly call `ForceRefresh`.
32+
3133
## `IComputedComponentGroup`
3234

3335
While this is listed as a high level `Computed` and not a convention, in reality it is a `ComputedFromEntityGroup<ReadOnlyMemory<ComponentBatch<...>>>` which is a bit of a mouthful, but it provides a really performant way to access components for `Entities`.
3436

3537
> For example if you have a group that requires `ComponentA`, `ComponentB` then this computed will provide `ComponentBatch<ComponentA, ComponentB>` for each entity, allowing quick lookup and processing, this is what `BatchedSystems` use under the hood to resolve components.
3638
39+
> This inherits from the lazy computed line, so it will only refresh its value when you read its `Value`(and it has awaiting changes) or you explicitly call `ForceRefresh`.
40+
3741
### `ComputedFromComponentGroup<T>`
3842

39-
This provides you a `IComputedComponentGroup` as the `DataSource` and lets you process it however you want into `T`.
43+
This provides you a `IComputedComponentGroup` as the `DataSource` and lets you process it however you want into `T`.
44+
45+
> This inherits from the lazy computed line, so it will only refresh its value when you read its `Value`(and it has awaiting changes) or you explicitly call `ForceRefresh`.

docs/ecs-r3/framework/systems.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,63 @@ public class BatchedExampleSystem : BatchedMixedSystem<SomeStructComponentA, Som
7777

7878
> Due to the MANY permutations of this that you can have its recommended that if you have very specific scenarios you just copy the code for the current `BatchedMixedSystem` and alter the signatures however you need.
7979
80+
### `MultiplexingSystem` + `IMultiplexedJob`
81+
82+
This is very much like a batched system, but it acts as a sort of multiplexer to allow you to run multiple `jobs` within one update, a `job` is basically the same as a batched systems `Process` method but standalone.
83+
84+
> This is really useful if you have multiple systems which all require same components and execute at same time, this ensures that it only needs to lookup the batches once and then runs all jobs back to back with the batch data, this can allow for better utilisation of CPU/Memory.
85+
86+
```csharp
87+
// Make as many jobs as you want, you can also use ISystemPreProcessor/PostProcessor with it
88+
public class Job1 : IMultiplexedJob<ClassComponent, ClassComponent2, ClassComponent3>
89+
{
90+
// Notice we dont give it a schedule, it is handled by the systems schedule
91+
public void Process(Entity entity, ClassComponent component1, ClassComponent2 component2, ClassComponent3 component3)
92+
{
93+
component1.Position += Vector3.One;
94+
component1.Something += 10;
95+
component2.IsTrue = true;
96+
component2.Value += 10;
97+
}
98+
}
99+
100+
// Notice that even though we don't use Component2 here, and the previous didnt use Component1 it doesnt matter too much as we
101+
// still get a performance bonus due to it scheduling both things in same block
102+
public class Job2 : IMultiplexedJob<ClassComponent, ClassComponent2, ClassComponent3>
103+
{
104+
public void Process(Entity entity, ClassComponent component1, ClassComponent2 component2, ClassComponent3 component3)
105+
{
106+
component1.Position += Vector3.One;
107+
component1.Something += 10;
108+
component3.IsTrue = true;
109+
component3.Value += 10;
110+
}
111+
}
112+
113+
114+
// This acts like a normal batched system, but it just expects you to provide it the jobs you want to run
115+
public class ExampleMultiplexedSystem : MultiplexingBatchedSystem<ClassComponent, ClassComponent2, ClassComponent3>
116+
{
117+
public ExampleMultiplexedSystem(IComponentDatabase componentDatabase, IEntityComponentAccessor entityComponentAccessor, IComputedComponentGroupRegistry computedComponentGroupRegistry, IThreadHandler threadHandler) : base(componentDatabase, entityComponentAccessor, computedComponentGroupRegistry, threadHandler)
118+
{}
119+
120+
// This is scheduling when all jobs should be run
121+
protected override Observable<Unit> ReactWhen() => Observable.EveryUpdate();
122+
123+
// This is a simple example, but you can always DI in the jobs and pass them into here for more complex use cases
124+
protected override IEnumerable<IMultiplexedJob<ClassComponent, ClassComponent2, ClassComponent3>> ResolveJobs()
125+
{ return [new Job1(), new Job2()]; }
126+
}
127+
```
128+
On one hand this may seem slightly more complex but in a way it also makes things a bit simpler as your Jobs are lightweight objects that can just be scheduled together and you have a smaller logic footprint.
129+
130+
> Remember you dont need to have 100% overlap on required components etc, there will be a tipping point but if you have several systems which all use 75% of the same components you can possibly get a decent performance bonus making them into jobs and giving them all the same components, even if a few of the jobs ignore a component or two it may still end up being more efficient than running them as fully fledged systems.
131+
132+
### `MultiplexingBatchedRefSystem` + `IMultiplexedRefJob`
133+
Same as above but it lets you pass the components to jobs with `ref` keyword, mainly meant for struct scenarios.
134+
135+
> There is currently no mixed one but it may be added in the future, there is so many varieties of approaches to mix `ref` it is currently left to you to implement your own variants if you need them.
136+
80137
### `IBasicEntitySystem`
81138

82139
This system is like a `IBasicSystem` allowing you to process each entity within the group on every update cycle.

docs/systems-r3/framework/computeds.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,14 @@ Computed values are basically read only values which are proxy a data source and
66
77
## Computed Types
88

9-
There are 3 default computed types available within the system:
9+
There are 4 default computed types available within the system:
1010

1111
### `IComputed` (For computed single values)
1212
Simplest computed and provides a current value and allows subscription to when the value changes, this can be very useful for precomputing things based off other data, i.e calculating MaxHp once all buffs have been taken into account.
1313

14+
### `ILazyComputed` (Same as above)
15+
Same as normal Computed but only updates when value is read or `ForceRefresh` is called, triggering `OnChange` exposes `OnHasChange` as well to indicate the dependent data has changed but not been refreshed.
16+
1417
### `IComputedCollection` (For computed collections of data)
1518
A reactive collection which provides an up to date collection of values and allows you to subscribe to when it changes, this could be useful for tracking all beneficial buffs on a player where the source data is just ALL buffs/debuffs on the entity.
1619

@@ -64,6 +67,9 @@ var computedFirstPlaceRacer = new ComputedFirstPlace(collectionOfRacers); // inh
6467
RacerHud.CurrentWinner.Text = computedFirstPlaceRacer.Value.Name;
6568
```
6669

70+
### `LazyComputedFromData<TOutput, TInput>`
71+
Same as previous `ComputedFromData` but a lazily evaluated variant.
72+
6773
### `ComputedFromObservable<TOutput, TInput>`
6874

6975
Much like the above `ComputedFromData` but the `DataSource` needs to be an `Observable<TInput>`, and will listen for changes on the observable and update its internal state accordingly, these are often known as **Pure Computeds** as they just proxy the underlying Observable.

src/EcsR3.Benchmarks/Benchmarks/BatchSystemMultiThreadingBenchmark.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public override void Setup()
6363

6464
public override void Cleanup()
6565
{
66-
EntityCollection.RemoveAll();
66+
EntityCollection.Clear();
6767
BatchingSystem.StopSystem();
6868
}
6969

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
using System.Collections.Generic;
2+
using System.Numerics;
3+
using BenchmarkDotNet.Attributes;
4+
using EcsR3.Blueprints;
5+
using EcsR3.Components.Database;
6+
using EcsR3.Computeds.Components.Registries;
7+
using EcsR3.Entities;
8+
using EcsR3.Entities.Accessors;
9+
using EcsR3.Examples.Custom.BatchTests.Components;
10+
using EcsR3.Extensions;
11+
using EcsR3.Systems.Batching.Convention;
12+
using EcsR3.Systems.Batching.Convention.Multiplexing;
13+
using EcsR3.Systems.Batching.Convention.Multiplexing.Handlers;
14+
using R3;
15+
using SystemsR3.Threading;
16+
17+
namespace EcsR3.Benchmarks.Benchmarks;
18+
19+
[BenchmarkCategory("Systems")]
20+
public class BatchVsMultiplexedClassComponentBenchmark : EcsR3Benchmark
21+
{
22+
public class BatchVsMultiplexedBlueprint : IBlueprint, IBatchedBlueprint
23+
{
24+
public void Apply(IEntityComponentAccessor entityComponentAccessor, Entity entity)
25+
{ entityComponentAccessor.AddComponents(entity, new ClassComponent(), new ClassComponent2(), new ClassComponent3()); }
26+
27+
public void Apply(IEntityComponentAccessor entityComponentAccessor, Entity[] entities)
28+
{ entityComponentAccessor.CreateComponents<ClassComponent, ClassComponent2, ClassComponent3>(entities); }
29+
}
30+
31+
#region Batch Systems
32+
public class ClassBatchSystem1 : BatchedSystem<ClassComponent, ClassComponent2>
33+
{
34+
public ClassBatchSystem1(IComponentDatabase componentDatabase, IEntityComponentAccessor entityComponentAccessor, IComputedComponentGroupRegistry computedComponentGroupRegistry, IThreadHandler threadHandler) : base(componentDatabase, entityComponentAccessor, computedComponentGroupRegistry, threadHandler)
35+
{}
36+
37+
protected override Observable<Unit> ReactWhen() => Observable.Never<Unit>();
38+
public void ForceRun() => ProcessBatch();
39+
public bool UseMultithreading(bool should) => ShouldMultithread = should;
40+
41+
protected override void Process(Entity entity, ClassComponent component2, ClassComponent2 component3)
42+
{
43+
component2.Position += Vector3.One;
44+
component2.Something += 10;
45+
component3.IsTrue = true;
46+
component3.Value += 10;
47+
}
48+
}
49+
50+
public class ClassBatchSystem2 : BatchedSystem<ClassComponent, ClassComponent3>
51+
{
52+
public ClassBatchSystem2(IComponentDatabase componentDatabase, IEntityComponentAccessor entityComponentAccessor, IComputedComponentGroupRegistry computedComponentGroupRegistry, IThreadHandler threadHandler) : base(componentDatabase, entityComponentAccessor, computedComponentGroupRegistry, threadHandler)
53+
{}
54+
55+
protected override Observable<Unit> ReactWhen() => Observable.Never<Unit>();
56+
public void ForceRun() => ProcessBatch();
57+
public bool UseMultithreading(bool should) => ShouldMultithread = should;
58+
59+
protected override void Process(Entity entity, ClassComponent component2, ClassComponent3 component3)
60+
{
61+
component2.Position += Vector3.One;
62+
component2.Something += 10;
63+
component3.IsTrue = true;
64+
component3.Value += 10;
65+
}
66+
}
67+
68+
public class ClassBatchSystem3 : BatchedSystem<ClassComponent2, ClassComponent3>
69+
{
70+
public ClassBatchSystem3(IComponentDatabase componentDatabase, IEntityComponentAccessor entityComponentAccessor, IComputedComponentGroupRegistry computedComponentGroupRegistry, IThreadHandler threadHandler) : base(componentDatabase, entityComponentAccessor, computedComponentGroupRegistry, threadHandler)
71+
{}
72+
73+
protected override Observable<Unit> ReactWhen() => Observable.Never<Unit>();
74+
public void ForceRun() => ProcessBatch();
75+
public bool UseMultithreading(bool should) => ShouldMultithread = should;
76+
77+
protected override void Process(Entity entity, ClassComponent2 component2, ClassComponent3 component3)
78+
{
79+
component2.IsTrue = true;
80+
component2.Value += 10;
81+
component3.IsTrue = true;
82+
component3.Value += 10;
83+
}
84+
}
85+
#endregion
86+
87+
#region Multiplex Systems
88+
public class Job1 : IMultiplexedJob<ClassComponent, ClassComponent2, ClassComponent3>
89+
{
90+
public void Process(Entity entity, ClassComponent component1, ClassComponent2 component2, ClassComponent3 component3)
91+
{
92+
component1.Position += Vector3.One;
93+
component1.Something += 10;
94+
component2.IsTrue = true;
95+
component2.Value += 10;
96+
}
97+
}
98+
99+
public class Job2 : IMultiplexedJob<ClassComponent, ClassComponent2, ClassComponent3>
100+
{
101+
public void Process(Entity entity, ClassComponent component1, ClassComponent2 component2, ClassComponent3 component3)
102+
{
103+
component1.Position += Vector3.One;
104+
component1.Something += 10;
105+
component3.IsTrue = true;
106+
component3.Value += 10;
107+
}
108+
}
109+
110+
public class Job3 : IMultiplexedJob<ClassComponent, ClassComponent2, ClassComponent3>
111+
{
112+
public void Process(Entity entity, ClassComponent component1, ClassComponent2 component2, ClassComponent3 component3)
113+
{
114+
component2.IsTrue = true;
115+
component2.Value += 10;
116+
component3.IsTrue = true;
117+
component3.Value += 10;
118+
}
119+
}
120+
121+
public class ExampleMultiplexedSystem : MultiplexingBatchedSystem<ClassComponent, ClassComponent2, ClassComponent3>
122+
{
123+
public ExampleMultiplexedSystem(IComponentDatabase componentDatabase, IEntityComponentAccessor entityComponentAccessor, IComputedComponentGroupRegistry computedComponentGroupRegistry, IThreadHandler threadHandler) : base(componentDatabase, entityComponentAccessor, computedComponentGroupRegistry, threadHandler)
124+
{}
125+
126+
protected override Observable<Unit> ReactWhen() => Observable.Never<Unit>();
127+
public void ForceRun() => ProcessBatch();
128+
public bool UseMultithreading(bool should) => ShouldMultithread = should;
129+
130+
protected override IEnumerable<IMultiplexedJob<ClassComponent, ClassComponent2, ClassComponent3>> ResolveJobs()
131+
{ return [new Job1(), new Job2(), new Job3()]; }
132+
}
133+
134+
#endregion
135+
136+
[Params(1000)]
137+
public int EntityCount;
138+
139+
[Params(false, true)]
140+
public bool UseMultithreading;
141+
142+
[Params(1000)]
143+
public int Invocations;
144+
145+
public ClassBatchSystem1 BatchingSystem1 { get; private set; }
146+
public ClassBatchSystem2 BatchingSystem2 { get; private set; }
147+
public ClassBatchSystem3 BatchingSystem3 { get; private set; }
148+
public ExampleMultiplexedSystem MultiplexedSystem { get; private set; }
149+
150+
public override void Setup()
151+
{
152+
BatchingSystem1 = new ClassBatchSystem1(ComponentDatabase, EntityComponentAccessor, ComputedComponentGroupRegistry, new DefaultThreadHandler());
153+
BatchingSystem1.StartSystem();
154+
BatchingSystem2 = new ClassBatchSystem2(ComponentDatabase, EntityComponentAccessor, ComputedComponentGroupRegistry, new DefaultThreadHandler());
155+
BatchingSystem2.StartSystem();
156+
BatchingSystem3 = new ClassBatchSystem3(ComponentDatabase, EntityComponentAccessor, ComputedComponentGroupRegistry, new DefaultThreadHandler());
157+
BatchingSystem3.StartSystem();
158+
MultiplexedSystem = new ExampleMultiplexedSystem(ComponentDatabase, EntityComponentAccessor, ComputedComponentGroupRegistry, new DefaultThreadHandler());
159+
MultiplexedSystem.StartSystem();
160+
161+
EntityCollection.CreateMany<BatchVsMultiplexedBlueprint>(EntityComponentAccessor, EntityCount);
162+
}
163+
164+
public override void Cleanup()
165+
{
166+
EntityCollection.Clear();
167+
BatchingSystem1.StopSystem();
168+
BatchingSystem2.StopSystem();
169+
BatchingSystem3.StopSystem();
170+
MultiplexedSystem.StopSystem();
171+
}
172+
173+
[Benchmark]
174+
public void ForceBatchRuns_Class()
175+
{
176+
BatchingSystem1.UseMultithreading(UseMultithreading);
177+
BatchingSystem2.UseMultithreading(UseMultithreading);
178+
BatchingSystem3.UseMultithreading(UseMultithreading);
179+
for (var i = 0; i < Invocations; i++)
180+
{
181+
BatchingSystem1.ForceRun();
182+
BatchingSystem2.ForceRun();
183+
BatchingSystem3.ForceRun();
184+
}
185+
}
186+
187+
[Benchmark]
188+
public void ForceMultiplexRuns_Class()
189+
{
190+
MultiplexedSystem.UseMultithreading(UseMultithreading);
191+
for (var i = 0; i < Invocations; i++)
192+
{ MultiplexedSystem.ForceRun(); }
193+
}
194+
}

0 commit comments

Comments
 (0)