perf(planning): reuse the donor ranking across public-holiday weeks

The holiday-week stage ranked every placement as a donor once per
public-holiday week, although the ranking depends only on the plan. The
weeks of one plan now share each placement's released objectives; each
week still skips its own dates and walks the placements in the same
order with the same yields.

Part of #302.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
nessarandClaude Opus 5.5 committed 2026-10-18 19:45:21 +02:00
1 parent b356117263
commit 0b0bb8f9a8
2 files changed
+54 -5

No files matched your search

+37
View File
@@ -0,0 +1,37 @@
# Issue #302: plan from the best-ranked obligation solution
Step-by-step record for [issue #302](https://gitea.nessar.fr/nessar/vac-optimizer/issues/302).
The exact steps keep planning outcomes, stable explanations and final states
identical to the frozen engine at `a871f8f`. The
[frozen-engine differential check](README.md#frozen-engine-differential-check),
including its cancelled asynchronous comparisons, passes after every step.
## Per-step measurements
Each step was timed against its parent in the same session, on the development
machine (AMD Ryzen 7 7800X3D, WSL2, Bun 1.4.3), with no other test, build or
profiling workload running. Both commits' cores were bundled with the
frozen-engine build command and called through `calculatePlanningAsyncInternal`
in one Bun process, with `calculationDate: '2024-01-01'`. This is the
benchmark's in-thread asynchronous path, without the planning-calculation
service. For every scenario, each engine ran one warm-up and then one measured
calculation, alternating engines. Outcomes, explanations and final states were
hashed; every pair below matched. Only same-session pairs are comparable.
### Reuse the donor ranking across public-holiday weeks
For every public-holiday week, the holiday-week stage ranked every placement as
a potential donor by releasing it and evaluating the active objectives. That
ranking depends only on the plan, so the weeks of one plan now share each
placement's released objectives. Each week still skips its own dates and walks
the placements in the same order with the same yields.
| Scenario | Before (s) | After (s) |
| --------------------------- | ---------: | --------: |
| representative year | 0.66 | 0.66 |
| typed obligations | 5.11 | 4.89 |
| chained earned entitlements | 0.02 | 0.02 |
| deliberate conflict | 0.01 | 0.01 |
| loaded year | 58.26 | 57.75 |
| two years | 2.27 | 1.82 |
| three years | 7.45 | 5.52 |
+17 -5
View File
@@ -2208,6 +2208,7 @@ function* calendarPlacementCandidates(
ruleId: string, ruleId: string,
context: SearchContext, context: SearchContext,
accept: (candidate: State, addition?: boolean) => boolean, accept: (candidate: State, addition?: boolean) => boolean,
donorObjectives = new Map<string, PlanningObjective[]>(),
): PlanningSteps<boolean> { ): PlanningSteps<boolean> {
const missing = targetDates.filter( const missing = targetDates.filter(
(date) => !state.placements.has(date) && !context.declaredDates.has(date), (date) => !state.placements.has(date) && !context.declaredDates.has(date),
@@ -2221,10 +2222,12 @@ function* calendarPlacementCandidates(
yield; yield;
context.check(); context.check();
if (targetDates.includes(placement.date)) continue; if (targetDates.includes(placement.date)) continue;
rankedDonors.push({ let objectives = donorObjectives.get(placement.date);
date: placement.date, if (!objectives) {
objectives: activeObjectiveVector(releasePlacement(state, placement.date), context), objectives = activeObjectiveVector(releasePlacement(state, placement.date), context);
}); donorObjectives.set(placement.date, objectives);
}
rankedDonors.push({ date: placement.date, objectives });
} }
// shortcut: recursion retains the original donor ranking; rerank when a measured bundle misses a better allocation. // shortcut: recursion retains the original donor ranking; rerank when a measured bundle misses a better allocation.
rankedDonors.sort( rankedDonors.sort(
@@ -2330,8 +2333,17 @@ function* holidayWeekPlacementCandidates(
context: SearchContext, context: SearchContext,
accept: (candidate: State, addition?: boolean) => boolean, accept: (candidate: State, addition?: boolean) => boolean,
): PlanningSteps<void> { ): PlanningSteps<void> {
// Releasing a donor scores the same for every week of this plan.
const donorObjectives = new Map<string, PlanningObjective[]>();
for (const week of context.holidayWeeks) for (const week of context.holidayWeeks)
yield* calendarPlacementCandidates(state, week.dates, ruleId, context, accept); yield* calendarPlacementCandidates(
state,
week.dates,
ruleId,
context,
accept,
donorObjectives,
);
} }
function* timeOffPeriodPlacementCandidates( function* timeOffPeriodPlacementCandidates(