Skip to content

Latest commit

 

History

History
203 lines (151 loc) · 7.88 KB

development.md

File metadata and controls

203 lines (151 loc) · 7.88 KB

The NationSim development guide

Everything a contributor needs to build, test, benchmark and extend NationSim.


1. Prerequisites

Tool Version Notes
.NET SDK 10.0.100 or later (the SDK is pinned in global.json with rollForward: latestFeature) The ASP.NET Core runtime is needed for the server
An IDE Rider, Visual Studio 2022 17.13+ or the .NET SDK + your favourite editor The solution is a classic .sln

No other tool is required: the package restore pulls the ECS framework, the serializer, the console library and the benchmark harness from NuGet.

dotnet --version     # 10.0.201 for example

2. The everyday commands

dotnet restore                                  # restore the packages
dotnet build                                     # build the debug configuration
dotnet build -c Release                          # build the release configuration
dotnet test                                      # run the 125 tests
dotnet test --filter "FullyQualifiedName~Replay" # run one suite
dotnet run --project src/NationSim.UI.Console    # start the interactive console
dotnet run --project src/NationSim.Server       # start the headless server

The benchmark

dotnet run -c Release --project tests/NationSim.Core.Tests -- --filter *Benchmark*
dotnet run -c Release --project tests/NationSim.Core.Tests -- --filter *Benchmark* --job short

The memory diagnoser of BenchmarkDotNet prints the allocated bytes per tick; it must stay at 0 B. The benchmark runs with 12,000 citizens, 1,200 firms and 16 regions, so the hot path is exercised at a realistic size.

The benchmark project is an executable: the test project has a Program.cs that hands the command line to BenchmarkSwitcher. dotnet test still discovers and runs the xUnit tests of the same assembly.


3. The repository layout

NationSim.sln
├── src/
│   ├── NationSim.Core/          The primitives: the tick lifecycle, the event bus, the PRNG,
│   │                            the macro math, the modifiers, the commands, the metrics
│   ├── NationSim.Domain/        The aggregates and the value objects
│   ├── NationSim.Simulation/    The ECS world, the components, the systems, the scheduler
│   ├── NationSim.Infrastructure/ The file formats, the telemetry, the channel queue, the host
│   ├── NationSim.Server/        The headless server (ASP.NET Core)
│   └── NationSim.UI.Console/    The interactive console
├── tests/
│   └── NationSim.Core.Tests/    The unit, integration, time travel, replay and server tests
├── docs/                        This documentation
├── Directory.Build.props        The shared compiler settings
└── global.json                  The pinned SDK

4. The compiler settings

Directory.Build.props applies to every project:

Setting Value Why
TreatWarningsAsErrors true The build must be clean, a warning is a defect
Nullable enable The nullability annotations are part of the contract
ImplicitUsings enable Keeps the files small
LangVersion latest The modern C# syntax is used throughout
Deterministic true The builds are reproducible
InvariantGlobalization true The formatting must not depend on the locale of the host
AllowUnsafeBlocks true Available for the low level buffers

5. The test suites

Suite Tests What it covers
Primitives 8 The PRNG: the reproducibility, the ranges, the capture and restore
Events 5 The event bus: the order, the unsubscription, the allocation free publishing
Tick 4 The tick lifecycle: the phase order, the clock, the context, the allocation
Domain 24 The aggregates: the voting, the bills, the treasury, the ministries
Economics 28 The macro engine, the national accounts and the emergent economy
Regions 9 The regional graph, the topology and the distribution
Ministries 9 The ministries, the modifiers and the law routing
Commands 6 The command queue and the input phase
Metrics 6 The telemetry ring buffer
Memory 2 The allocation budgets of the tick and of the state hash
Determinism 15 The determinism and the time travel across the snapshot
Replay 5 The recording, the binary format and the replay
Server 4 The WebSocket stream and the HTTP endpoints
Total 125

6. How to extend the simulation

Adding a system

  1. Derive from ISimulationSystem and pick the phase:

    public sealed class MySystem : ISimulationSystem
    {
        public string Name => nameof(MySystem);
        public TickPhase Phase => TickPhase.Execution;
        public void Update(in SimulationContext context) { /* ... */ }
    }
  2. If the system reads the components, add a query description to QueryDescriptions and cache the handler in a field:

    private readonly ForEach<Citizen> _handler;
    public MySystem() => _handler = OnCitizen;

    A closure that captures this would allocate a delegate on every tick.

  3. If the system runs in parallel, take a RegionScheduler and a cached Action<int> and remember that every region has to be independent.

  4. Register the system in SimulationKernel in the right phase.

Adding a domain aggregate

  1. Put the aggregate in NationSim.Domain and keep it free of any reference to the simulation layer.
  2. Publish its changes as immutable record struct events on the event bus.
  3. Let a system in NationSim.Simulation translate the event into the state of the ECS.

Adding an API endpoint

The endpoints live in NationSimServerHost.MapEndpoints. Capture the engine in the closure; a parameter of the handler would be inferred as the request body.


7. The invariants to respect

The complete list is in the architecture reference. The three that break the most often:

  • Never destroy an entity inside the tick. Use the IsDead flag: the chunk layout of the ECS is LIFO and a removal would permute the iteration order, which changes the order in which the random source is consumed.
  • Never rely on the iteration order of a query. Use the agent id: the working set uses it as its slot, which is what makes a restored run identical to the original.
  • Never allocate on the hot path. The benchmark is the judge.

8. Debugging

Symptom Where to look
The state hash differs between two runs The random streams (the region streams are in the snapshot), the order of the agents (the slot is the agent id)
A replay diverges The tick at which the command was recorded; the input phase applies it before the tick
The benchmark allocates GC.GetTotalAllocatedBytes in a diagnostic; look for LINQ, closures and Parallel.For
The economy collapses The EmergentEconomyTests show the wealth and the demand; a tax rate that is too high starves the demand
The server does not answer The engine host logs; the UseWebSockets middleware is not part of the default pipeline

A useful diagnostic

The process wide allocation of a tick catches the allocations of the worker threads that the thread local probe cannot see:

long before = GC.GetTotalAllocatedBytes(precise: true);
for (int i = 0; i < 20; i++) kernel.RunTick();
long allocated = GC.GetTotalAllocatedBytes(precise: true) - before;

9. The release checklist

Before a release:

dotnet clean
dotnet build            # 0 warnings, 0 errors
dotnet build -c Release # 0 warnings, 0 errors
dotnet test             # 125 tests
dotnet run -c Release --project tests/NationSim.Core.Tests -- --filter *Benchmark*

And then update the release audit report with the measured numbers.