diff --git a/docs/benchmark/issue-302.md b/docs/benchmark/issue-302.md index 2d46a57..b88873e 100644 --- a/docs/benchmark/issue-302.md +++ b/docs/benchmark/issue-302.md @@ -89,3 +89,22 @@ largest gap stayed between 58 and 67 ms in every run. Cancelling `three years` one second after the request returned the retained plan 3.9 to 4.0 ms after the cancel message reached the worker, against 7.2 to 8.7 ms before. This is desktop evidence, not a reference-phone measurement. + +### Validate each optimization configuration once + +Every objective evaluation revalidated its optimization configuration against +the catalog. Rule normalization already validates each configuration once per +calculation and keeps its own copy. The presets are constant, and the summed +variant of a valid configuration is valid because every catalog row accepts +the sum. The core therefore scores them without revalidating; +`evaluateOptimization` still validates for its other callers. + +| Scenario | Before (s) | After (s) | +| --------------------------- | ---------: | --------: | +| representative year | 0.35 | 0.30 | +| typed obligations | 4.03 | 3.46 | +| chained earned entitlements | 0.01 | 0.01 | +| deliberate conflict | 0.00 | 0.00 | +| loaded year | 48.12 | 44.03 | +| two years | 1.04 | 0.96 | +| three years | 3.35 | 3.03 | diff --git a/src/app/services/leave-planner.core.ts b/src/app/services/leave-planner.core.ts index 7cd2908..357b65e 100644 --- a/src/app/services/leave-planner.core.ts +++ b/src/app/services/leave-planner.core.ts @@ -24,7 +24,7 @@ import { addPlanningYears, isPlanningDate, planningDateInParis } from './plannin import { compareObjectiveLists, compareObjectiveValues, - evaluateOptimization, + evaluateValidatedOptimization, firstDifferentObjective, OPTIMIZATION_PRESETS, validateOptimizationConfiguration, @@ -1896,11 +1896,12 @@ function reconcileOptimizationProvenance( // Another tied maximum can mask a placement's improvement to its own element. observations ??= optimizationObservations(state, context); const withoutObservations = optimizationObservations(withoutState, context); + // Every catalog row accepts the sum, so this stays a valid configuration. const summed = { ...rule.configuration, aggregation: 'sum' as const }; const summedComparison = compareObjectiveValues( objective.direction, - evaluateOptimization(summed, observations), - evaluateOptimization(summed, withoutObservations), + evaluateValidatedOptimization(summed, observations), + evaluateValidatedOptimization(summed, withoutObservations), ); contributes = summedComparison < 0 || @@ -1930,14 +1931,14 @@ function supportingCalendarBenefit( ): boolean { const observations = optimizationObservations(after, context); return ( - evaluateOptimization(OPTIMIZATION_PRESETS.holidayWeekValue, observations) > - evaluateOptimization(OPTIMIZATION_PRESETS.holidayWeekValue, before) || - evaluateOptimization(OPTIMIZATION_PRESETS.nonWorkingDayGain, observations) > - evaluateOptimization(OPTIMIZATION_PRESETS.nonWorkingDayGain, before) || - evaluateOptimization(OPTIMIZATION_PRESETS.longestWorkSequence, observations) < - evaluateOptimization(OPTIMIZATION_PRESETS.longestWorkSequence, before) || - evaluateOptimization(OPTIMIZATION_PRESETS.shortSequenceDeficit, observations) < - evaluateOptimization(OPTIMIZATION_PRESETS.shortSequenceDeficit, before) + evaluateValidatedOptimization(OPTIMIZATION_PRESETS.holidayWeekValue, observations) > + evaluateValidatedOptimization(OPTIMIZATION_PRESETS.holidayWeekValue, before) || + evaluateValidatedOptimization(OPTIMIZATION_PRESETS.nonWorkingDayGain, observations) > + evaluateValidatedOptimization(OPTIMIZATION_PRESETS.nonWorkingDayGain, before) || + evaluateValidatedOptimization(OPTIMIZATION_PRESETS.longestWorkSequence, observations) < + evaluateValidatedOptimization(OPTIMIZATION_PRESETS.longestWorkSequence, before) || + evaluateValidatedOptimization(OPTIMIZATION_PRESETS.shortSequenceDeficit, observations) < + evaluateValidatedOptimization(OPTIMIZATION_PRESETS.shortSequenceDeficit, before) ); } @@ -2654,7 +2655,7 @@ function* optimizeRulePlacements( item.configuration.measure === 'public-holiday-week-value' && item.configuration.direction === 'maximize' ) - return evaluateOptimization(item.configuration, { + return evaluateValidatedOptimization(item.configuration, { leaveBalances: [], publicHolidayWeeks: context.holidayWeeks.map(({ value }) => ({ value, diff --git a/src/app/services/plan-observations.ts b/src/app/services/plan-observations.ts index 6c54ccd..69b25fd 100644 --- a/src/app/services/plan-observations.ts +++ b/src/app/services/plan-observations.ts @@ -1,7 +1,7 @@ import { isWorkingPlanningDate } from './declared-non-working-days'; import type { TimeOffPeriod } from './leave-planner.core'; import { - evaluateOptimization, + evaluateValidatedOptimization, type OptimizationConfiguration, type OptimizationObservations, } from './planning-objectives'; @@ -683,11 +683,12 @@ export function workSequenceDates(state: ObservedPlan, context: ObservationCalen })); } +/** The configuration was validated when the calculation's rules were normalized. */ export function evaluateStateOptimization( configuration: OptimizationConfiguration, state: ObservedPlan, context: ObservationCalendar, observations = optimizationObservations(state, context), ): number { - return evaluateOptimization(configuration, observations); + return evaluateValidatedOptimization(configuration, observations); } diff --git a/src/app/services/planning-objectives.ts b/src/app/services/planning-objectives.ts index 1efc89d..2398468 100644 --- a/src/app/services/planning-objectives.ts +++ b/src/app/services/planning-objectives.ts @@ -212,6 +212,14 @@ export function evaluateOptimization( ): number { const validation = validateOptimizationConfiguration(configuration); if (!validation.valid) throw new Error(`${validation.code}: ${validation.message}`); + return evaluateValidatedOptimization(configuration, observations); +} + +/** Scores a configuration that {@link validateOptimizationConfiguration} already accepted. */ +export function evaluateValidatedOptimization( + configuration: OptimizationConfiguration, + observations: OptimizationObservations, +): number { const selected = (condition: OptimizationCondition) => configuration.conditions.includes(condition); let values: number[];