Skip to content

Perf/Fix: spawn and other functions that need closest by unit#3243

Open
VariableVince wants to merge 14 commits intomainfrom
spawn-perf
Open

Perf/Fix: spawn and other functions that need closest by unit#3243
VariableVince wants to merge 14 commits intomainfrom
spawn-perf

Conversation

@VariableVince
Copy link
Contributor

@VariableVince VariableVince commented Feb 19, 2026

Description:

Performance improvements.

  • PlayerImpl: for nukeSpawn, cache config to const.
  • Other files: for nukeSpawn and other functions doing the same, introduce findClosestBy function.
  • for TradeShipExecution, with the move from distSortUnit to findClosestBy, also add check if port isActive, !isMarkedForDeletion and !isUnderConstruction. These checks should have been there already, so now do it in one go to make use of the predicate isCandidate in findClosestBy.
  • for TradeShipExectution.test.ts, add mock functions for isMarkedForDeletion and isUnderConstruction because of the above. Also, set Unit tiles and Pathfinding node to actual valid TileRefs for the testing map. This prevents NaN as return value from manhattanDist. This problem was already present with the use of distSortUnit, but that function just did NaN - NaN, returned the first and only port unit in the array and called it a day. For findClosestBy we have to make sure the predicate manhattanDist actually returns a number instead of NaN so we need actually valid tiles. We now have a working test instead of a test that actually silently failed like before.
  • PlayerImpl: warshipSpawn and nukeSpawn: Make use of the isCandidate predicate of findClosestBy to have warshipSpawn not return ports under construction or (smaller change) inactive. This fixes a bug i have seen right away (where Warship spawns from under construction Port).
    Same for nukeSpawn silos, don't return inactive silo just to be sure now that we can easily add it to isCandidate predicate anyway. This costs no performance in the nukeSpawn benchmarks actually. This should as a by-effecft fix an edge case bug i have seen, where a nuke is sent from a phantom silo.

Some of this goes along with PR #3220 since playerImpl buildableUnits makes use of the underlying spawn functions via canBuild. Just like ConstructionExecution does. But i didn't want to add more to PR 3220 since there's already a lot in there.

The new function findClosestBy could also be applied to some other parts of code to benefit of it being faster, so i did that.

findClosestBy uses findMinimumBy, which is a little more generic in name. I think findMinimumBy could be used by other parts of code, while findClosestBy is more clear naming for what it does now. But we could ditch findMinimumBy and just leave findClosestBy?

Examples of synthetic benchmarks (not included in this PR):

BEFORE CHANGES (before Scamiv's PR #3241)
image

image

AFTER CHANGES (before Scamiv's PR #3241)
findunittoupgrade for loop 5th run

nukespawn for loop 3rd run

BEFORE CHANGES (after Scamiv's PR #3241)
findunittoupgrade BEFORE

nukespawn BEFORE

AFTER CHANGES (after Scamiv's PR #3241)
image

image

Also see more BEFORE and AFTER in this comment:
#3243 (comment)

And here a comparison in the flame charts:

  • based on the same replay and tried to get the performance recording going at the same speed and length but always end up with small differences
  • because of a bug in replays currently, it puts you in with the same clientID/persistantID currently. This means we can also record part of what is normally only recordable with live human input (the playerActions/playerBuildables).

BEFORE flame chart with nukeSpawn (human player) and maybeSendNuke (Nation players, uses nukeSpawn via canBuild):

BEFORE nukespawn Schermafbeelding 2026-03-04 231707

BEFORE maybesendnuke B Schermafbeelding 2026-03-04 230009

BEFORE main build Schermafbeelding 2026-03-04 222017

BEFORE flame chart with nukeSpawn (human player) and maybeSendNuke (Nation players, uses nukeSpawn via canBuild):

AFTER nukespawn Schermafbeelding 2026-03-04 230613

AFTER maybesendnuke B Schermafbeelding 2026-03-04 230009

AFTER maybesendnuke C Schermafbeelding 2026-03-04 230009

AFTER maybesendnuke D Schermafbeelding 2026-03-04 230009

AFTER main build Schermafbeelding 2026-03-04 222017

Please complete the following:

  • I have added screenshots for all UI updates
  • I process any text displayed to the user through translateText() and I've added it to the en.json file
  • I have added relevant tests to the test directory
  • I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced

Please put your Discord username so you can be contacted if a bug or regression is found:

tryout33

@VariableVince VariableVince added this to the v30 milestone Feb 19, 2026
@VariableVince VariableVince self-assigned this Feb 19, 2026
@VariableVince VariableVince requested a review from a team as a code owner February 19, 2026 05:53
@VariableVince VariableVince added the Performance Performance optimization label Feb 19, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 19, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds two generic utilities, findMinimumBy and findClosestBy, replaces ad-hoc nearest-item sorts with findClosestBy across client and core code, makes selection results null-safe, and updates tests and spawn/return-port logic accordingly.

Changes

Cohort / File(s) Summary
Core Utilities
src/core/Util.ts
Adds exported findMinimumBy<T> and findClosestBy<T> helpers that return the best item or null, with optional candidate predicates.
Client selection logic
src/client/ClientGameRunner.ts, src/client/graphics/layers/RadialMenuElements.ts
Replaces manual sort-by-distance selection with findClosestBy; adds null checks before emitting SendUpgradeStructureIntentEvent or deleting selected units.
Executions: Trade ship return
src/core/execution/TradeShipExecution.ts
Replaces sorted-port selection with findClosestBy, reads ship tile earlier, and early-returns/deletes the ship when no port candidate exists.
Game: Player spawn & upgrade
src/core/game/PlayerImpl.ts
Refactors nearest-unit/port/missile-silo/warship selection to use findClosestBy; consolidates owner/team checks and returns tile or false for spawn lookups.
Tests (mocks)
tests/core/executions/TradeShipExecution.test.ts
Updates mock tiles and path nodes, adds a second pirate port, and adjusts tests to reflect new closest-port behavior.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant UI as RadialMenu/UI
participant Client as ClientGameRunner
participant Util as findClosestBy (Util)
participant Game as Game State / Units
UI->>Client: user action (upgrade/delete)
Client->>Util: findClosestBy(candidates, distance, isCandidate?)
Util->>Game: (reads candidate list via reference)
Util-->>Client: returns nearest item or null
alt item found
Client->>Game: emit upgrade intent / delete unit
else not found
Client-->>UI: no-op / close menu
end

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

A finder rolls across the map,
Picks the closest, closes the gap.
Null-safe paths and tests aligned,
Cleaner logic, easy to find —
Small util, tidy map. 🧭

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title directly relates to the main change: introducing findClosestBy utility for performance improvements in spawn and selection functions across multiple files.
Description check ✅ Passed The pull request description clearly relates to the changeset: it describes performance improvements through introducing findClosestBy utility and applying it across multiple files (PlayerImpl, TradeShipExecution, ClientGameRunner, RadialMenuElements), includes specific rationale for changes and bug fixes, and provides comprehensive benchmarks demonstrating the improvements.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/core/Util.ts (1)

95-101: findClosestBy is a thin alias over findMinimumBy — consider whether both exports are needed long-term.

Right now findClosestBy just forwards to findMinimumBy with zero added logic. The only difference is the parameter name (distance vs score). This is fine for readability at call sites — "closest by distance" reads more naturally than "minimum by score" when dealing with spatial lookups. Just keep in mind that maintaining two nearly identical public exports adds surface area. If findMinimumBy never gets used directly outside of findClosestBy, you could collapse them later.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/Util.ts` around lines 95 - 101, findClosestBy is a trivial wrapper
around findMinimumBy with no added behavior (only a parameter rename), which
risks duplicated public surface area; either remove the redundant export or make
the intent explicit: search the repo for external usages of findClosestBy and
findMinimumBy and if both are used keep both, otherwise collapse into a single
export (prefer the clearer name for call sites, e.g., keep findClosestBy and
remove or inline findMinimumBy), or add a short comment above findClosestBy
documenting it's an intentional alias for readability; update any imports
accordingly to use the chosen single function (references: findClosestBy,
findMinimumBy).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/core/Util.ts`:
- Around line 95-101: findClosestBy is a trivial wrapper around findMinimumBy
with no added behavior (only a parameter rename), which risks duplicated public
surface area; either remove the redundant export or make the intent explicit:
search the repo for external usages of findClosestBy and findMinimumBy and if
both are used keep both, otherwise collapse into a single export (prefer the
clearer name for call sites, e.g., keep findClosestBy and remove or inline
findMinimumBy), or add a short comment above findClosestBy documenting it's an
intentional alias for readability; update any imports accordingly to use the
chosen single function (references: findClosestBy, findMinimumBy).

coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 19, 2026
@evanpelle
Copy link
Collaborator

looks like a test is failing

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/core/Util.ts (1)

110-116: findClosestBy is a pure alias — consider whether it pulls its own weight.

Right now findClosestBy adds no behaviour on top of findMinimumBy; they share the same signature and the body is a single delegation line. Two options worth considering:

  1. Keep as-is — the distinct name signals "this is a spatial distance query" at call sites, which is fine.
  2. Remove findClosestBy and have callers import findMinimumBy directly, reducing surface area.

Either is valid; this is just a nudge to settle the "possible consolidation" point the PR already raises before more callers are added.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/Util.ts` around lines 110 - 116, findClosestBy is a no-op alias of
findMinimumBy; decide whether to remove it or clarify intent: either delete the
findClosestBy function and replace all its call sites/imports to use
findMinimumBy (update tests and exports accordingly), or keep findClosestBy but
add a brief doc comment above the function explaining it's a semantic alias for
spatial/distance queries so callers understand the intent and keep the
single-line delegation as-is; reference the symbols findClosestBy and
findMinimumBy when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/core/Util.ts`:
- Around line 102-109: The JSDoc example for findClosestBy uses the incorrect
GameMap variable name "mg" — change it to "gm" to match the file's convention
and other helpers like distSort and distSortUnit; update the example line
containing mg.manhattanDist(...) to use gm.manhattanDist(...) so examples are
consistent and copy-pasteable for readers referencing findClosestBy.

---

Nitpick comments:
In `@src/core/Util.ts`:
- Around line 110-116: findClosestBy is a no-op alias of findMinimumBy; decide
whether to remove it or clarify intent: either delete the findClosestBy function
and replace all its call sites/imports to use findMinimumBy (update tests and
exports accordingly), or keep findClosestBy but add a brief doc comment above
the function explaining it's a semantic alias for spatial/distance queries so
callers understand the intent and keep the single-line delegation as-is;
reference the symbols findClosestBy and findMinimumBy when making the change.

@github-project-automation github-project-automation bot moved this from Triage to Development in OpenFront Release Management Feb 20, 2026
coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 20, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
src/core/game/PlayerImpl.ts (1)

1183-1193: warshipSpawn still uses this.mg instead of a local cache.

Not a bug, but nukeSpawn caches this.mg into a local mg for speed, while warshipSpawn does not. If this method is also on a hot path, the same local-cache pattern would be consistent. Low priority since warshipSpawn does fewer lookups.

Optional: cache mg locally for consistency
 warshipSpawn(tile: TileRef): TileRef | false {
-    if (!this.mg.isOcean(tile)) {
+    const mg = this.mg;
+    if (!mg.isOcean(tile)) {
       return false;
     }
 
-    const bestPort = findClosestBy(this.units(UnitType.Port), (port) =>
-      this.mg.manhattanDist(port.tile(), tile),
+    const bestPort = findClosestBy(this.units(UnitType.Port), (port) =>
+      mg.manhattanDist(port.tile(), tile),
     );
 
     return bestPort?.tile() ?? false;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/game/PlayerImpl.ts` around lines 1183 - 1193, warshipSpawn currently
accesses this.mg repeatedly; mirror the nukeSpawn optimization by caching
this.mg into a local const (e.g., const mg = this.mg) at the top of the
warshipSpawn method and then use mg.isOcean and mg.manhattanDist in the
findClosestBy callback (leave findClosestBy, units(UnitType.Port), and
UnitType.Port unchanged) so lookups are consistent and slightly faster on hot
paths.
src/core/Util.ts (1)

102-116: findClosestBy is a pure alias for findMinimumBy — consider if both are needed.

Right now findClosestBy just calls findMinimumBy with identical signature/behavior. The only difference is the parameter name (distance vs score). This is fine for readability at call sites, but be aware the two could drift apart if someone later adds logic to only one of them.

If the intent is purely a naming alias, a brief inline comment like // Semantic alias – keep in sync with findMinimumBy would help future readers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/Util.ts` around lines 102 - 116, findClosestBy currently just
forwards to findMinimumBy and should be documented as an intentional semantic
alias to avoid future drift; update the findClosestBy declaration to include a
short inline comment like "// Semantic alias of findMinimumBy — keep
behavior/signature in sync" (or alternatively remove the alias and replace call
sites with findMinimumBy if duplication isn't desired), referencing the
functions findClosestBy and findMinimumBy so readers know this is deliberate.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/core/game/PlayerImpl.ts`:
- Around line 1183-1193: warshipSpawn currently accesses this.mg repeatedly;
mirror the nukeSpawn optimization by caching this.mg into a local const (e.g.,
const mg = this.mg) at the top of the warshipSpawn method and then use
mg.isOcean and mg.manhattanDist in the findClosestBy callback (leave
findClosestBy, units(UnitType.Port), and UnitType.Port unchanged) so lookups are
consistent and slightly faster on hot paths.

In `@src/core/Util.ts`:
- Around line 102-116: findClosestBy currently just forwards to findMinimumBy
and should be documented as an intentional semantic alias to avoid future drift;
update the findClosestBy declaration to include a short inline comment like "//
Semantic alias of findMinimumBy — keep behavior/signature in sync" (or
alternatively remove the alias and replace call sites with findMinimumBy if
duplication isn't desired), referencing the functions findClosestBy and
findMinimumBy so readers know this is deliberate.

@VariableVince VariableVince marked this pull request as draft February 22, 2026 01:47
…or map "ocean_and_land" which lead to NaN as outcome of manhattanDist. Until now we used distSortUnit which just returned the first and only port it was given in the test, even though manhattanDist both returned NaN. Now with findClosestBy we need it to have actual existing tiles because we need actual comparable number to come out of manhattanDist.)

Also for TradeShipExecution use some already cached values as input for findClosestBy.
coderabbitai[bot]

This comment was marked as resolved.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a single-pass “closest/minimum” selection utility to replace repeated sort(...)[0] patterns in hot paths, improving performance and (in some cases) tightening candidate filtering to avoid inactive/invalid spawn sources.

Changes:

  • Added findMinimumBy / findClosestBy helpers to select the best element without sorting.
  • Migrated several “pick nearest unit” call sites (core + client) from sort-based selection to findClosestBy.
  • Tightened port/silo candidate selection in core logic and updated TradeShip tests to use valid TileRefs and new unit predicates.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/core/Util.ts Adds findMinimumBy/findClosestBy single-pass selection helpers.
src/core/game/PlayerImpl.ts Uses findClosestBy for upgrade/spawn selection; adds additional candidate checks for silos/ports.
src/core/execution/TradeShipExecution.ts Replaces sort-based nearest-port selection with findClosestBy and adds candidate filtering.
tests/core/executions/TradeShipExecution.test.ts Updates mocks and tile refs to support new candidate filtering and avoid NaN distances.
src/client/graphics/layers/RadialMenuElements.ts Uses findClosestBy to pick nearest deletable unit.
src/client/ClientGameRunner.ts Uses findClosestBy to pick nearest upgrade candidate without sorting.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 28, 2026
@github-project-automation github-project-automation bot moved this from Development to Complete in OpenFront Release Management Mar 1, 2026
@VariableVince VariableVince deleted the spawn-perf branch March 1, 2026 21:21
@VariableVince VariableVince restored the spawn-perf branch March 2, 2026 18:27
@VariableVince VariableVince reopened this Mar 2, 2026
@github-project-automation github-project-automation bot moved this from Complete to Triage in OpenFront Release Management Mar 2, 2026
@evanpelle evanpelle closed this Mar 2, 2026
@github-project-automation github-project-automation bot moved this from Triage to Complete in OpenFront Release Management Mar 2, 2026
@evanpelle evanpelle reopened this Mar 2, 2026
@github-project-automation github-project-automation bot moved this from Complete to Triage in OpenFront Release Management Mar 2, 2026
@evanpelle
Copy link
Collaborator

Looking at the diff, i think the original is more clear. Do we have a flame graph to show how much cpu we are saving? I'm not sure if these are hot-paths

It is mostly noticable when doing 'spam' actions. Keep in mind the wins are with added checks as fixes in warshipSpawn and nukeSpawn. For RadialMenuElements delete action, there isn't much to spam. But while i didn't measure TradeShipExecution when it needs to find port of new owner on capture, this can happen a lot with hundreds of tradeships and many warships out there. And every bit of time that the CPU doesn't have to work on something it can work on something else?

Also these go hand in hand with the changes in the other PR, when you count the wins in here to those there it makes an even bigger impact as a whole.

  • findAndUpgradeNearestBuilding in ClientGameRunner is about 16% faster (on spam clicking middle mouse button to auto-upgrade nearest building)

BEFORE findAndUpgradeNearestBuilding AFTER findAndUpgradeNearestBuilding in ClientGameRunner continious clicks middle mouse button on silo 21s

  • nukeSpawn is over 50% faster, even with the extra .isActive() test as fix (on swinging over the map with a a hovering atom bomb from the Build bar, where NukeTrajectoryPreviewLayer needs to continiously check if it can build the nuke and what the nearest silo is):

BEFORE nukeSpawn AFTER nukeSpawn

  • warshipSpawn is about 72% faster, even with extra checks if port isUnderConstruction and isActive as fixes (on spam clicking Warship from legacy Build menu with 3 ports the warship can spawn from):

BEFORE warshipspawn 3 ports spam clicking somewhere in middle above island above russia on legacy build menu 21s AFTER warshipspawn 3 ports spam clicking somewhere in middle above island above russia on legacy build menu 21s

would be nice to get what percentage of total cpu time was spent on that function. Otherwise we don't know if this is in the hot path or not

coderabbitai[bot]
coderabbitai bot previously approved these changes Mar 4, 2026
@VariableVince
Copy link
Contributor Author

VariableVince commented Mar 5, 2026

would be nice to get what percentage of total cpu time was spent on that function. Otherwise we don't know if this is in the hot path or not

@evanpelle Do the newly added screenshots in the PR description help you in assessing this? Used a replay and played it on main and on this branch, there are small differences in recording time and game speed though but it comes closest to a good comparison. The replay was done with 60 Nations on World map.

Nations call nukeSpawn and warshipSpawn from their executions. You see maybeSendNuke in the screenshots which calls canBuild which calls nukeSpawn.

I imagine the more Nations (up to 400 now in v30, compared to the 60 in this replay) the more their Executions check to send Warships and especially Nukes, it could be seen as a hotpath.

And as already shown in the screenshots in the earlier comment, when spamming Warships as the human player and hovering an Atom bomb over the map which calls nukeSpawn for each tile, we also get a nice - albeit small in terms of ms - reduction.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Performance Performance optimization

Projects

Status: Triage

Development

Successfully merging this pull request may close these issues.

3 participants