diff --git a/.github/workflows/deploy-activity.yml b/.github/workflows/deploy-activity.yml index 0072d3cf..3f26ad7e 100644 --- a/.github/workflows/deploy-activity.yml +++ b/.github/workflows/deploy-activity.yml @@ -51,7 +51,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v4 with: - node-version: '20' + node-version: '22' - name: Install Dependencies run: npm ci diff --git a/.jules/bolt.md b/.jules/bolt.md index a6d28e4e..eb68917f 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -32,3 +32,7 @@ ## 2024-05-18 - Replacing O(N) array search inside nested loops with O(1) object properties **Learning:** During test optimizations, filtering candidate lists (like available tanks or healers) inside a heavy iterative loop using O(N) array checks (e.g. `Array.some()`) creates a severe performance bottleneck. **Action:** When filtering or excluding object references inside hot paths, prefer using inherent O(1) boolean properties on the object itself rather than building and parsing sub-arrays to check role inclusion. + +## 2026-07-02 - [O(N^2) Iteration vs O(N(N-1)/2) in Pair Counting] +**Learning:** In highly recursive inner loops, like the pairwise distance scoring in `parallelGroupCreator.ts`, checking all N×N combinations and skipping self-pairs (`if (i === j) continue`) wastes roughly 50% of CPU cycles and memory allocations for pair keys (e.g., `a|b` vs `b|a`). +**Action:** Always optimize internal matrix operations to an upper-triangular loop (`i = 0; i < len; i++`, `j = i + 1; j < len; j++`) when relationships are commutative/undirected, accumulating the values into both the `i` and `j` indexes simultaneously. diff --git a/activity/tests/__screenshots__/components.spec.ts/group-slide.png b/activity/tests/__screenshots__/components.spec.ts/group-slide.png index 90eaa5f1..30af0954 100644 Binary files a/activity/tests/__screenshots__/components.spec.ts/group-slide.png and b/activity/tests/__screenshots__/components.spec.ts/group-slide.png differ diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 9937707b..5fe0c325 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -27,6 +27,7 @@ export { topAffinityFor, shortestPath, parseSeasonPairs, + type SeasonPairs, } from './seasonPairs.js'; export { generateInviteCommand } from './inviteCommand.js'; diff --git a/packages/shared/src/parallelGroupCreator.ts b/packages/shared/src/parallelGroupCreator.ts index c015ff0d..86343344 100644 --- a/packages/shared/src/parallelGroupCreator.ts +++ b/packages/shared/src/parallelGroupCreator.ts @@ -98,19 +98,36 @@ function scoreGroups( ): { maxPerPlayer: number; total: number } { let maxPerPlayer = 0; let perPlayerSum = 0; + + // ⚡ Bolt Opt: Pre-allocate an array for the 5 slots to avoid inner loop allocations + const scores = new Array(5); + for (const g of groups) { const ms = g.players; - for (let i = 0; i < ms.length; i++) { - let perPlayer = 0; - for (let j = 0; j < ms.length; j++) { - if (i === j) continue; - perPlayer += pairCounts.get(pairKey(ms[i].name, ms[j].name)) ?? 0; + const len = ms.length; + + // Reset scores for this group + for (let i = 0; i < len; i++) { + scores[i] = 0; + } + + // ⚡ Bolt Opt: Use an upper-triangular loop to halve the pair iterations + for (let i = 0; i < len; i++) { + const p1 = ms[i].name; + for (let j = i + 1; j < len; j++) { + const count = pairCounts.get(pairKey(p1, ms[j].name)) ?? 0; + scores[i] += count; + scores[j] += count; + // The original code summed each unique pair twice across all players + perPlayerSum += count * 2; } - if (perPlayer > maxPerPlayer) maxPerPlayer = perPlayer; - perPlayerSum += perPlayer; + } + + for (let i = 0; i < len; i++) { + if (scores[i] > maxPerPlayer) maxPerPlayer = scores[i]; } } - // Each unique pair is summed twice across the players' perPlayer counts. + return { maxPerPlayer, total: perPlayerSum / 2 }; }