perf(planning): reuse immutable non-working date counts
This commit is contained in:
1 parent
c7e7abcd24
commit
433a8b0ade
3 files changed
+95
-3
No files matched your search
@@ -0,0 +1,61 @@
|
||||
# PR #278: reuse counts for immutable calendar dates
|
||||
|
||||
Measured on 2026-10-16 against `c7e7abc`, which already includes the
|
||||
[direct date-key optimization](pr-278-date-keys.md).
|
||||
|
||||
A loaded-year CPU profile attributed 4.06 of 69.37 seconds to repeatedly counting
|
||||
distinct non-working dates for time-off-period objectives. The existing calendar
|
||||
cache already shares frozen date arrays between candidate plans, but each score
|
||||
rebuilt a `Set` to count the same dates again.
|
||||
|
||||
The objective evaluator now keeps those counts in a `WeakMap` keyed by the frozen
|
||||
array. Mutable arrays are recounted on every evaluation. The weak keys allow
|
||||
unused arrays to be collected, and configuration validation and duplicate-date
|
||||
handling remain intact. This change touches only that scoring operation.
|
||||
|
||||
## Before and after
|
||||
|
||||
Both protocol runs used Node 24.18.0, Bun 1.3.14 and Vitest 5.0.3 on the same local
|
||||
machine. The baseline is the completed run recorded for `c7e7abc`; its saved core
|
||||
bundle is byte-for-byte identical to a fresh bundle of that commit. No other
|
||||
test, build or profiling workload ran alongside either protocol measurement.
|
||||
|
||||
```sh
|
||||
NODE_OPTIONS=--max-old-space-size=2048 timeout 300s bunx vitest run src/app/benchmark/leave-planner-benchmark.spec.ts --maxWorkers=1
|
||||
```
|
||||
|
||||
All seven warm-ups, fourteen measured calculations, the typed-obligation precheck,
|
||||
fixtures, assertions and resource limits remain unchanged.
|
||||
|
||||
| Measurement | Before | After |
|
||||
| ---------------------------------------------- | --------: | --------: |
|
||||
| Entire command, external wall time | 268.573 s | 256.034 s |
|
||||
| Benchmark test | 263.265 s | 250.652 s |
|
||||
| Loaded year, mean of two measured calculations | 67.747 s | 64.360 s |
|
||||
| Headroom before timeout | 31.427 s | 43.966 s |
|
||||
|
||||
The full command was 4.7% faster in this pair. Both runs completed successfully,
|
||||
and every repeated scenario remained deterministic. Live CI performance still
|
||||
needs confirmation.
|
||||
|
||||
An isolated check using the loaded-year plan's frozen date arrays took
|
||||
551–554 ms before and 46–57 ms after for 100,000 scoring calls, following 5,000
|
||||
warm-ups per run in before/after/after/before order. That result isolates the repeated
|
||||
counting cost; the protocol above measures its effect on complete calculations.
|
||||
|
||||
## Correctness checks
|
||||
|
||||
Complete outcomes, stable explanations and final-state snapshots match `c7e7abc`
|
||||
for all seven scenarios. These separate comparisons ran alongside functional
|
||||
checks; their diagnostic timings are not used in the protocol comparison.
|
||||
|
||||
The functional coverage suite passes all 638 tests across 33 files. Three new
|
||||
regression cases cover duplicate dates and changes to mutable arrays alongside
|
||||
frozen arrays for sum, count and maximum scores. Application and spec type checks,
|
||||
lint, formatting and `git diff --check` also pass.
|
||||
|
||||
No browser timing run or live CI rerun was performed for this change.
|
||||
|
||||
Diagnostic sources, profiles, bundles and full logs are retained in
|
||||
`/tmp/vac-opt278-step2`. The original core diagnostic runner is documented in
|
||||
[issue-78.md](issue-78.md#reproduction).
|
||||
@@ -75,6 +75,29 @@ describe('composable planning objectives', () => {
|
||||
).toEqual([3, 7, 4, 6, 5, 2]);
|
||||
});
|
||||
|
||||
it.each(['sum', 'count', 'maximum'] as const)(
|
||||
'keeps %s distinct-date scores current with frozen and mutable period dates',
|
||||
(aggregation) => {
|
||||
const mutableDates = ['2024-05-08'];
|
||||
const observations: OptimizationObservations = {
|
||||
...empty,
|
||||
timeOffPeriods: Object.freeze([
|
||||
Object.freeze({
|
||||
nonWorkingDates: Object.freeze(['2024-01-06', '2024-01-07', '2024-01-07']),
|
||||
}),
|
||||
Object.freeze({ nonWorkingDates: mutableDates }),
|
||||
]),
|
||||
};
|
||||
const configuration = { ...OPTIMIZATION_PRESETS.nonWorkingDayGain, aggregation };
|
||||
const initial = { sum: 3, count: 2, maximum: 2 };
|
||||
expect(evaluateOptimization(configuration, observations)).toBe(initial[aggregation]);
|
||||
expect(evaluateOptimization(configuration, observations)).toBe(initial[aggregation]);
|
||||
mutableDates.push('2024-05-09', '2024-05-09', '2024-05-11');
|
||||
const updated = { sum: 5, count: 2, maximum: 3 };
|
||||
expect(evaluateOptimization(configuration, observations)).toBe(updated[aggregation]);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(['sum', 'count', 'maximum'] as const)(
|
||||
'defines empty %s as zero for every measure',
|
||||
(aggregation) => {
|
||||
|
||||
@@ -203,6 +203,9 @@ export interface OptimizationObservations {
|
||||
leavePlacements: readonly { supporting: boolean }[];
|
||||
}
|
||||
|
||||
// Calendar geometry reuses frozen date arrays across candidate plans.
|
||||
const nonWorkingDateCounts = new WeakMap<readonly string[], number>();
|
||||
|
||||
export function evaluateOptimization(
|
||||
configuration: OptimizationConfiguration,
|
||||
observations: OptimizationObservations,
|
||||
@@ -224,9 +227,14 @@ export function evaluateOptimization(
|
||||
.map((week) => week.value);
|
||||
break;
|
||||
case 'time-off-periods':
|
||||
values = observations.timeOffPeriods.map(
|
||||
(period) => new Set(period.nonWorkingDates).size,
|
||||
);
|
||||
values = observations.timeOffPeriods.map(({ nonWorkingDates }) => {
|
||||
const cached = nonWorkingDateCounts.get(nonWorkingDates);
|
||||
if (cached !== undefined) return cached;
|
||||
const count = new Set(nonWorkingDates).size;
|
||||
if (Object.isFrozen(nonWorkingDates))
|
||||
nonWorkingDateCounts.set(nonWorkingDates, count);
|
||||
return count;
|
||||
});
|
||||
break;
|
||||
case 'work-sequences':
|
||||
values = observations.workSequences
|
||||
|
||||
Reference in new issue
Block a user