164 lines
7.6 KiB
Markdown
164 lines
7.6 KiB
Markdown
# Politics tick performance — design brief
|
|
|
|
A working document for a session on making the hourly **politics/migration tick**
|
|
cheap in a large, settled world.
|
|
|
|
---
|
|
|
|
## 1. Shared project context (read this first)
|
|
|
|
*Battle for 'Tismo* is an HTML5 port of a turn-of-the-millennium strategy game.
|
|
Pure JavaScript, no build step, no runtime dependencies.
|
|
|
|
- **Server-authoritative.** `shared/game_state.js` (`GameState`) is a
|
|
framework-free model, ticked hourly by `server/server.js`; the browser only
|
|
renders snapshots.
|
|
- **Shared logic runs in Node and the browser.** `shared/` stays framework-free
|
|
(no Node, no DOM).
|
|
- **GameState is mixins.** `shared/game_state.js` imports each
|
|
`shared/game_state/<topic>.js` and adds its methods to the prototype.
|
|
- **Version counters** are how expensive derived data is memoised on the snapshot
|
|
and tick paths: `_gdpEpoch`, `_populationVersion`, `_territoryVersion`,
|
|
`_modifiersVersion`, `_tileImprovementVersion`, `_visibleVersion`,
|
|
`_ethnicityVersion`, `_regionVersion`, and the trade graph's own stamp.
|
|
- **The trade graph is the reference pattern**: a walk from each city floods the
|
|
ocean (a sea lane runs any distance), so `_tradeGraph` is cached across ticks
|
|
*and* days under a stamp built from the road/railway, territory and
|
|
storage-node counters. Any network change rebuilds lazily; a quiet world reuses
|
|
it. The tile-improvement version is deliberately **not** in the stamp.
|
|
- **Do not memoise `getCityEconomy` outside a snapshot** — a test pins that a
|
|
simulation read reflects a direct `tilePopulation` edit immediately.
|
|
|
|
### Commands
|
|
|
|
- Install once: `npm install`.
|
|
- One suite: `node tests/run_tests.js --file <name>.js`. **Never the full suite**
|
|
during development — the commit hook runs it.
|
|
- Time a settle: `node server/server.js --port 27015 --bind 127.0.0.1` logs how
|
|
many days the warm-up took (and how long). `--warmup 0` skips it.
|
|
|
|
### Conventions
|
|
|
|
- 2-space indent; no new libraries.
|
|
- Tests in `tests/<topic>_test.js` extending `TestCase`. Force RNG with
|
|
`state._random = () => value`.
|
|
- Commit only when asked; one imperative sentence; the hook runs the full suite.
|
|
|
|
---
|
|
|
|
## 2. The problem (from `ROADMAP.md`)
|
|
|
|
> - [ ] Faster politics tick: with the trade-graph walks cached, `_tickPolitics`
|
|
> is the largest remaining cost in a settled world — `_migrateForIncome`
|
|
> through `_regionIncomePerCapita` and `_movePopulation`, and the repeated
|
|
> `_cityMechanicLevel` lookups each of them makes. Memoise the per-region
|
|
> income the way the trade graph is now kept, and let a migration sweep
|
|
> visit only the regions whose pull actually changed.
|
|
|
|
---
|
|
|
|
## 3. Where the time goes
|
|
|
|
`shared/game_state/politics.js`:
|
|
|
|
- `_tickPolitics()` (line 71) runs hourly and calls, among others,
|
|
`_migrateForIncome()`.
|
|
- `_migrateForIncome()` (744) computes `_regionIncomePerCapita(this.cities)`
|
|
(852) and then, per city, `_cityMechanicLevel(city, AIR_IMMIGRATION)` and
|
|
nested loops over origin/destination cities, calling `_movePopulation` (865)
|
|
which bumps `_populationVersion` (922).
|
|
- `_regionIncomePerCapita` (852) is the expensive walk; it recomputes income for
|
|
every region on every tick even when nothing that feeds it changed.
|
|
- `_cityMechanicLevel` is called repeatedly for the same city/building within one
|
|
tick.
|
|
- Some politics walks are already memoised (`politics.js:222`, `:251` use
|
|
`_ethnicityVersion:_populationVersion:_territoryVersion[: _regionVersion]`
|
|
stamps), so there is a local idiom to follow.
|
|
|
|
---
|
|
|
|
## 4. Design space (settle before coding)
|
|
|
|
1. **Memoise `_regionIncomePerCapita`** under a stamp of the counters that can
|
|
change a region's per-capita income — likely
|
|
`_populationVersion`, `_territoryVersion`, `_ethnicityVersion`,
|
|
`_gdpEpoch`, `_modifiersVersion`, and the building/mechanic version (check
|
|
what `_regionIncomePerCapita` actually reads). Keep the stamp minimal: the
|
|
trade-graph lesson is that folding in a fast-changing counter throws the cache
|
|
away constantly. Rebuild lazily on demand.
|
|
2. **Visit only changed regions.** If a region's "pull" (income, or the
|
|
migration allowance) did not change, skip its whole migration sweep. Needs a
|
|
per-region pull signature / version and a place to keep it across ticks.
|
|
3. **Hoist `_cityMechanicLevel`.** Compute each city's mechanic levels once per
|
|
tick into a small map and pass it down, instead of calling it per city inside
|
|
the loops.
|
|
4. **Bound the inner loop.** `_migrateForIncome` is effectively all-pairs over
|
|
cities in a region (or the world?). If it is world-wide, restrict to regions
|
|
that are connected / have a pull difference, or use a nearest-first sweep.
|
|
5. **Avoid `_populationVersion` churn from no-op moves.** `_movePopulation` bumps
|
|
the version even for trivial/fractional moves; only bump when the population
|
|
actually changes. (This also keeps every population-keyed memo alive longer.)
|
|
6. **Measure first.** Build a benchmark that settles the standard map N days and
|
|
times `_tickPolitics`, and/or count calls to `_regionIncomePerCapita` and
|
|
`_cityMechanicLevel` per tick. Change nothing until the hot spot is confirmed.
|
|
|
|
---
|
|
|
|
## 5. Suggested order of work
|
|
|
|
1. Add a benchmark/profiling test (or temporary counters) that reports the
|
|
per-tick politics cost and call counts on a settled standard map, so the
|
|
before/after is objective.
|
|
2. Memoise `_regionIncomePerCapita` with the right stamp (item 1). Re-measure.
|
|
3. Hoist `_cityMechanicLevel` (item 3). Re-measure.
|
|
4. Skip unchanged regions (item 2) if still hot. Re-measure.
|
|
5. Keep the full politics/upkeep suites green; the migration tests pin the
|
|
observable behaviour, so the refactor must not change outcomes.
|
|
|
|
---
|
|
|
|
## 6. Tests to add / extend
|
|
|
|
- A performance-oriented test that asserts `_regionIncomePerCapita` is computed
|
|
once per tick when nothing changed (e.g. count invocations via a spy or a
|
|
counter), and is recomputed when a counter it depends on moves.
|
|
- A correctness test that a direct `tilePopulation` edit is still reflected in a
|
|
simulation read (do not break the `getCityEconomy` rule).
|
|
- Keep `tests/politics_test.js`, `tests/migration_test.js`, `tests/growth_test.js`,
|
|
`tests/warmup_test.js` green. `warmup_test.js` timing is a coarse guard.
|
|
|
|
---
|
|
|
|
## 7. Gotchas
|
|
|
|
- **A wrong stamp is worse than no cache.** If a counter that changes income is
|
|
missing, migration reads stale data and tests will flake; if a fast counter is
|
|
included, the cache is useless.
|
|
- **Deltas.** Any new persisted/cached structure that is serialised must pair
|
|
with a `DELTA_COLLECTIONS` entry and a version (see §1), or the snapshot grows.
|
|
- **`_populationVersion` is load-bearing** for GDP/population memoisation; do not
|
|
bump it in a hot loop for no reason, and do not *stop* bumping it when
|
|
population truly changes.
|
|
- **Same numbers, different cost.** The goal is identical simulation output at
|
|
lower cost; verify by comparing settlement figures before/after, not just
|
|
timing.
|
|
|
|
---
|
|
|
|
## 8. Relevant files
|
|
|
|
- `shared/game_state/politics.js` — `_tickPolitics`, `_migrateForIncome`,
|
|
`_regionIncomePerCapita`, `_movePopulation`, `_cityMechanicLevel`, existing
|
|
memoisation at lines ~222/~251.
|
|
- `shared/game_state/resources.js` — `_tradeGraph` cache/stamp (the template),
|
|
`_tickResources`.
|
|
- `shared/game_state.js` — version counters, tick order.
|
|
- `server/server.js` — the hourly tick and warm-up timing logs.
|
|
- Tests: `tests/politics_test.js`, `tests/migration_test.js`,
|
|
`tests/growth_test.js`, `tests/warmup_test.js`, `tests/trade_graph_test.js`.
|
|
|
|
## 9. Related design docs
|
|
|
|
`INTELLIGENCE.md`, `STACKS.md`, `AIR_MOVEMENT.md`, `ECONOMY_BALANCE.md`,
|
|
`TRAINING_UI.md`, `ICONS.md` (repo root).
|