From 2fcba758c8744b8db0f73d78dfd5b63ceeca9f97 Mon Sep 17 00:00:00 2001 From: Nessar Date: Sun, 18 Oct 2026 19:46:31 +0200 Subject: [PATCH] perf(planning): retain the best valid plan more cheaply Retaining a better valid plan reconciled its optimization provenance, which evaluated every active objective once per credited placement with that placement released. Only the credited objectives are compared, so the released plan now evaluates just those and skips observing the others. Retention also stops computing quality metrics that nothing read. The retained plan and what Cancel returns are unchanged. Part of #302. Co-Authored-By: Claude Opus 5.5 --- docs/benchmark/issue-302.md | 24 +++++++++++++ src/app/services/leave-planner.core.ts | 47 +++++++++++++------------- 2 files changed, 48 insertions(+), 23 deletions(-) diff --git a/docs/benchmark/issue-302.md b/docs/benchmark/issue-302.md index 9159870..b1f877c 100644 --- a/docs/benchmark/issue-302.md +++ b/docs/benchmark/issue-302.md @@ -35,3 +35,27 @@ the placements in the same order with the same yields. | loaded year | 58.26 | 57.75 | | two years | 2.27 | 1.82 | | three years | 7.45 | 5.52 | + +### Retain the best valid plan more cheaply + +Retaining a better valid plan for cancellation reconciled its optimization +provenance. For every placement credited by an optimization rule, that +evaluated all active objectives with the placement released. Only the credited +objectives are compared, so the released plan now evaluates just those, and +the calendar observations of the other objectives are no longer computed. +Retention also no longer computes quality metrics that nothing read. An +instrumented three-year run spent 1.16 of its 1.22 retention seconds in that +reconciliation; afterwards retention took 0.26 seconds. + +| Scenario | Before (s) | After (s) | +| --------------------------- | ---------: | --------: | +| representative year | 0.45 | 0.37 | +| typed obligations | 4.76 | 5.03 | +| chained earned entitlements | 0.02 | 0.02 | +| deliberate conflict | 0.01 | 0.01 | +| loaded year | 55.59 | 53.58 | +| two years | 1.68 | 1.22 | +| three years | 5.29 | 3.85 | + +The typed-obligations difference is run-to-run noise; its retention cost was +22 milliseconds. diff --git a/src/app/services/leave-planner.core.ts b/src/app/services/leave-planner.core.ts index 04036f0..4337ffb 100644 --- a/src/app/services/leave-planner.core.ts +++ b/src/app/services/leave-planner.core.ts @@ -544,12 +544,8 @@ interface SearchContext { observerTime: number; completedPreferenceStates: Map; completedGenericState?: string; - /** Supplied metrics were evaluated after reconciling earned entitlement dates. */ - retain?: ( - state: State, - finalizedMetrics?: PlanningMetrics, - vector?: PlanningObjectiveVector, - ) => void; + /** A supplied vector was evaluated after reconciling earned entitlement dates. */ + retain?: (state: State, vector?: PlanningObjectiveVector) => void; } type PlanningCalendar = Pick< @@ -1869,7 +1865,15 @@ function reconcileOptimizationProvenance( ); if (!credits.length) continue; const withoutState = releasePlacement(state, placement.date); - const withoutPlacement = activeObjectiveVector(withoutState, context); + // Only credited objectives are compared, so skip observing the others. + const withoutPlacement = [ + ...activeObjectives( + withoutState, + context, + Number.POSITIVE_INFINITY, + new Set(credits.map(({ id }) => id)), + ), + ]; const ruleIds = new Set( [...placement.ruleIds].filter( (id) => !credits.some((objective) => objective.id === `optimization:${id}`), @@ -2468,7 +2472,7 @@ function evaluateRuleCandidate( const vector = objectiveVector(state, context, active); // Preserve the best valid candidate even if the active prefix rejects it. if (compareObjectiveLists(vector.objectives, incumbentVector.objectives) < 0) - context.retain?.(state, undefined, vector); + context.retain?.(state, vector); // shortcut: single additions cannot cross equal-value plateaus; add coordinated balance bundles when a measured configuration needs them. if (addition && comparison === 0) return undefined; return { state, vector }; @@ -2676,7 +2680,7 @@ function* optimizeRulePlacements( const active = activeObjectiveVector(result, context); const baseline = active.slice(0, throughIndex + 1); const beforeVector = objectiveVector(result, context, active); - context.retain?.(result, undefined, beforeVector); + context.retain?.(result, beforeVector); if ( allowAdd && prefixBounds.length > 0 && @@ -3395,6 +3399,7 @@ function* activeObjectives( state: State, context: SearchContext, throughIndex = Number.POSITIVE_INFINITY, + only?: ReadonlySet, ): Generator { let preferences: PreferenceResult[] | undefined; let balances: OptimizationObservations | undefined; @@ -3403,6 +3408,11 @@ function* activeObjectives( for (const rule of context.rules) { if (!rule.enabled || rule.force !== 'preference') continue; if (index++ > throughIndex) return; + if ( + only && + !only.has(`${rule.type === 'optimization' ? 'optimization' : 'preference'}:${rule.id}`) + ) + continue; if (rule.type === 'optimization') { const observations = rule.configuration.elements === 'leave-balances' @@ -4257,19 +4267,18 @@ function* calculatePlanningSteps( let checkpoint: PlanOutcome | undefined; let checkpointState: State | undefined; let retained: - | (Pick & { + | (Pick & { vector: PlanningObjectiveVector; }) | undefined; if (retainIncumbents) - context.retain = (candidate, finalizedMetrics, candidateVector) => { + context.retain = (candidate, candidateVector) => { const check = context.check; context.check = () => undefined; try { - const finalized = - finalizedMetrics || candidateVector - ? candidate - : finalizeEarnedState(candidate, context); + const finalized = candidateVector + ? candidate + : finalizeEarnedState(candidate, context); if (!finalized) return; if (unsatisfiedObligationCount(finalized, context)) return; if ( @@ -4314,14 +4323,6 @@ function* calculatePlanningSteps( ), vector, preferences: candidatePreferences, - metrics: finalizedMetrics - ? { - ...finalizedMetrics, - preferenceSatisfaction: candidatePreferences.map( - ({ satisfied }) => satisfied, - ), - } - : qualityMetrics(finalized, context, candidatePreferences), }; if (firstValidCandidate && checkpointCallback) observeCallback(context, () =>