diff --git a/docs/benchmark/README.md b/docs/benchmark/README.md index debaed2..4e8b78a 100644 --- a/docs/benchmark/README.md +++ b/docs/benchmark/README.md @@ -238,6 +238,76 @@ outcome, and delivery of its stable explanation. Confirm that editing an input remains possible during active worker CPU work. These measurements describe that device and workload; they impose no latency limit. +## Indicative wait recalibration — 2026-10-18 + +[Issue #302](issue-302.md) made planning much faster, so the rough prior +behind the range beside **Calculate** (see +[planning-duration.md](../planning-duration.md)) overstated every request with +many obligation solutions or a multi-year horizon. These measurements refit it. + +The NEW builds are the production build of `75320da` (the #302 engine); the OLD +column is the production build of `b356117`, the last commit before the #302 +engine changes. Both ran in headless Chromium 149.0.7827.55 on an AMD Ryzen 7 +7800X3D desktop under Debian 13 on WSL2. Emulation used 412×915 pixels, scale +2.625, touch, reduced motion and 4× CPU throttling; external font downloads +were blocked. **This is desktop emulation, not a phone measurement.** +`Emulation.setCPUThrottlingRate` slows the page's main thread but does not +throttle the dedicated planning worker, so the search ran at desktop speed. +A phone, whose worker is also slower, can wait longer than any figure here. + +Every run used a fresh browser. The application's own initial calculation (one +saved day) finished first. The run then enabled any custom rules with the real +toggles and set the planning dates through the real date controls; the saved +balances, rules and dates were checked, and the calendar stayed unchanged until +one Calculate click. The wait is the time from that click to the displayed +final placements with the loading overlay gone, the endpoint described in +[planning-duration.md](../planning-duration.md). Every run returned a `Plan` +with the placement count shown. NEW has three runs per configuration; OLD has +two, and was not run for configurations 4 and 5. Configurations 1 to 3 give each +year 20 CP and 10 RTT days available January 1 and expiring December 31; every +configuration keeps the six default optimization presets, and 4 and 5 are the +benchmark's "loaded year" and "typed obligations" scenarios from +`benchmarkScenarios()`. + +| # | Configuration | Placements | NEW wait, ms mean [min–max] | OLD wait, ms mean [min–max] | Range shown, old constants | Range shown, new constants | +| --- | ----------------------------------------------------- | ---------: | --------------------------- | --------------------------- | -------------------------- | -------------------------- | +| 1 | One year 2024, 20 CP + 10 RTT | 30 | 730 [716–752] | 1,014 [990–1,037] | 1–5 s | 1–3 s | +| 2 | Two years 2024–2025, 20 CP + 10 RTT per year | 60 | 1,909 [1,880–1,943] | 3,137 [3,121–3,153] | 3–24 s | 1–7 s | +| 3 | Three years 2024–2026, 20 CP + 10 RTT per year | 90 | 4,477 [4,450–4,498] | 8,250 [8,241–8,258] | 11–68 s | 2–15 s | +| 4 | Loaded year (6×12 days, 3 obligation + 2 preferences) | 72 | 4,024 [3,981–4,076] | not run | 4–26 s | 1–12 s | +| 5 | Typed obligations | 10 | 640 [599–687] | not run | 1–2 s | 1–2 s | +| 6 | One year 2024, 5 CP only | 5 | 465 [457–472] | 518 [518–518] | 1–2 s | 1–2 s | + +The old ranges are the application's no-sample ranges before the change, and +the new ones were confirmed in the rebuilt application for the same six saved +configurations. The earlier 2026-10-05 references (1.74, 5.08 and about 11 +seconds) came from a different build, before many engine speedups, which is why +the OLD column is lower than them. + +The refit keeps the model's structure and rounds its constants: +`400 + 250 × (D / 365.25)^0.5 × (max(1, P) / 30)^1.5 × R`. With `D` the horizon +in days, `P` the placement load and `R` the rule weight, it gives: + +| # | `P` | `R` | `D` | Old estimate, ms | New estimate, ms | Measured, ms | New / measured | +| --- | --- | --- | ----- | ---------------: | ---------------: | -----------: | -------------: | +| 1 | 30 | 1.9 | 366 | 1,443 | 875 | 730 | 1.20 | +| 2 | 60 | 1.9 | 731 | 7,714 | 2,301 | 1,909 | 1.21 | +| 3 | 90 | 1.9 | 1,096 | 22,444 | 4,675 | 4,477 | 1.04 | +| 4 | 72 | 3.7 | 366 | 8,574 | 3,843 | 4,024 | 0.95 | +| 5 | 10 | 2.9 | 366 | 636 | 540 | 640 | 0.84 | +| 6 | 5 | 1.9 | 366 | 378 | 432 | 465 | 0.93 | + +Every measured mean lies well inside the unrounded range from half to three +times the estimate. Configurations 1, 5 and 6 finished below the displayed +minimum of 1 second, which the whole-second display cannot go under. Horizon +length and placements grew together in configurations 1 to 3, so the data does +not separate the 0.5 horizon power from the 1.5 placement power; the sum +reproduces the roughly squared growth with years above a fixed overhead. The +same prior estimates five years with 30 days per year (`P` = 150, `D` = 1,826) +at about 12 seconds, displayed as 6–37 seconds; the old constants gave +44–265 seconds. Horizons beyond three years remain unvalidated extrapolation. +These samples set a rough prior and impose no duration ceiling. + ## Historical measurements before trace removal The following records remain as evidence for their dated builds. Trace-related behavior and metrics below are superseded by issue #76. diff --git a/docs/planning-duration.md b/docs/planning-duration.md index 6271a2d..62b4e9a 100644 --- a/docs/planning-duration.md +++ b/docs/planning-duration.md @@ -13,19 +13,24 @@ horizon dates hide the estimate and retain the existing disabled Calculate control. Other malformed inputs keep a rough fallback estimate; the planner still performs its own validation. -The initial model is a deliberately rough engineering prior grounded in the -[browser reference measurements](benchmark/README.md): the 2026-10-03 annual -30-day scenario returned its outcome in about 0.6 seconds, and the 2026-10-05 -two- and three-year calendar runs with 30 days per year took 5.08 and 10.86 -seconds. These are throttled desktop measurements, not phone measurements or a -validated prediction model. The annual outcome timing does not include final -calendar rendering. The new rendered-calendar measurements below supplement -those references. +The initial model is a deliberately rough engineering prior fitted to the +rendered waits recorded in the +[2026-10-18 rendered-wait recalibration](benchmark/README.md): six +configurations, from a five-day request to a three-year horizon with 30 days +per year, each timed from the Calculate click to the rendered final calendar. +They were taken after the engine speedups of +[issue #302](https://gitea.nessar.fr/nessar/vac-optimizer/issues/302), which +made the earlier 2026-10-03 and 2026-10-05 references obsolete as grounding. +These are desktop emulation measurements: 4× CPU throttling +slows the main thread but not the dedicated planning worker, so they are not +phone measurements, and a slower device can wait longer than the prior says. +The prior is not a validated prediction model; its rounded constants stay +within about 1.2× of every measured mean. For a half-open horizon of `D` calendar days, the model uses: ```text -initial_ms = 300 + 600 × (D / 365.25)^1.2 × (max(1, P) / 30)^1.5 × R +initial_ms = 400 + 250 × (D / 365.25)^0.5 × (max(1, P) / 30)^1.5 × R ``` `P` approximates placement load. Each non-negative finite balance contributes @@ -42,11 +47,17 @@ solving constraints or counting holiday occurrences in the form. Typed obligations, date distribution, interacting rules, earned entitlements, device load and browser differences can all change the actual wait substantially. -The powers and weights are implementation choices, not complexity guarantees -or fitted accuracy claims. With 30 available days per year, the model's search -term grows as the number of years to the power 2.7, rather than assuming a -linear increase. Horizons beyond the references are unvalidated extrapolation. -The estimate performs one pass over balances and rules and runs no planner. +The constants are fitted to a handful of runs, not derived. The fixed 400 ms +approximates request scheduling, worker startup and rendering (a one-year +request for 5 CP waited 465 ms). The one-to-three-year runs grew roughly as the +number of years squared above such an overhead (730, 1,909 and 4,477 ms). +Horizon length and placements grew together in them, so the 0.5 horizon power +is a fit, not a finding about horizon length alone. Five years with 30 +available days per year (20 CP plus 10 RTT and the six default optimization +presets) now estimates about 12 seconds, displayed as 6–37 seconds; the +previous constants gave 44–265 seconds. Horizons beyond three years are +unvalidated extrapolation. The estimate performs one pass over balances and +rules and runs no planner. ## Local refinement and precision diff --git a/src/app/components/calendar-year/calendar-year-planning.spec.ts b/src/app/components/calendar-year/calendar-year-planning.spec.ts index 42ab989..fefddd7 100644 --- a/src/app/components/calendar-year/calendar-year-planning.spec.ts +++ b/src/app/components/calendar-year/calendar-year-planning.spec.ts @@ -1782,7 +1782,7 @@ describe('CalendarYear planning outcome boundary', () => { const fixture = TestBed.createComponent(CalendarYear); fixture.detectChanges(); now = 21_000; - expect(duration.estimate()).toEqual({ minimum: 1, maximum: 3, unit: 'seconds' }); + expect(duration.estimate()).toEqual({ minimum: 1, maximum: 2, unit: 'seconds' }); expect(fixture.nativeElement.querySelector('.loading-overlay')).not.toBeNull(); expect(fixture.nativeElement.querySelectorAll('button.selected')).toHaveLength(0); pending.resolve(plan); diff --git a/src/app/services/planning-duration.service.spec.ts b/src/app/services/planning-duration.service.spec.ts index c4ed358..fd3b655 100644 --- a/src/app/services/planning-duration.service.spec.ts +++ b/src/app/services/planning-duration.service.spec.ts @@ -31,7 +31,7 @@ describe('Planning duration estimate', () => { it('offers a coarse seconds range on the first launch without calculating', () => { const duration = TestBed.inject(PlanningDurationService); - expect(duration.estimate()).toEqual({ minimum: 1, maximum: 3, unit: 'seconds' }); + expect(duration.estimate()).toEqual({ minimum: 1, maximum: 2, unit: 'seconds' }); expect(TestBed.inject(PlanningCalculationService).isCalculating()).toBe(false); expect(TestBed.inject(PlanningCalculationService).accepted()).toBeNull(); expect(TestBed.inject(RuleService).rules).toEqual([]); @@ -133,7 +133,7 @@ describe('Planning duration estimate', () => { TestBed.resetTestingModule(); expect(TestBed.inject(PlanningDurationService).estimate()).toEqual({ minimum: 1, - maximum: 3, + maximum: 2, unit: 'seconds', }); }); diff --git a/src/app/services/planning-duration.service.ts b/src/app/services/planning-duration.service.ts index e6192e2..f2b0767 100644 --- a/src/app/services/planning-duration.service.ts +++ b/src/app/services/planning-duration.service.ts @@ -44,9 +44,9 @@ function initialDurationMs(request: PlanningRequest): number { Math.max(1, 'placeDays' in rule ? rule.placeDays / 3 : 1); if ('rewardDays' in rule) placements += (Math.max(0, rule.rewardDays) * days) / 365.25; } - // Rough prior from the browser references in docs/planning-duration.md, not a deadline. + // Rough prior fitted to the 2026-10-18 rendered waits in docs/planning-duration.md, not a deadline. const duration = - 300 + 600 * (days / 365.25) ** 1.2 * (Math.max(1, placements) / 30) ** 1.5 * ruleWeight; + 400 + 250 * (days / 365.25) ** 0.5 * (Math.max(1, placements) / 30) ** 1.5 * ruleWeight; return Number.isFinite(duration) ? duration : 1_000; }