fix(planning): recalibrate the duration estimate to the faster engine
CI / policy (push) Successful in 20s
CI / validate (push) Successful in 36s
CI / build (push) Successful in 23s
CI / policy (pull_request) Successful in 8s
CI / validate (pull_request) Successful in 25s
CI / benchmark (push) Successful in 1m29s
CI / security (push) Successful in 1m10s
CI / build (pull_request) Successful in 23s
CI / test (push) Successful in 2m16s
CI / benchmark (pull_request) Successful in 1m27s
CI / security (pull_request) Successful in 1m0s
CI / images (push) Successful in 59s
CI / required (push) Successful in 1s
CI / test (pull_request) Successful in 2m6s
CI / images (pull_request) Successful in 53s
CI / required (pull_request) Successful in 1s
CI / policy (push) Successful in 20s
CI / validate (push) Successful in 36s
CI / build (push) Successful in 23s
CI / policy (pull_request) Successful in 8s
CI / validate (pull_request) Successful in 25s
CI / benchmark (push) Successful in 1m29s
CI / security (push) Successful in 1m10s
CI / build (pull_request) Successful in 23s
CI / test (push) Successful in 2m16s
CI / benchmark (pull_request) Successful in 1m27s
CI / security (pull_request) Successful in 1m0s
CI / images (push) Successful in 59s
CI / required (push) Successful in 1s
CI / test (pull_request) Successful in 2m6s
CI / images (pull_request) Successful in 53s
CI / required (pull_request) Successful in 1s
Rendered waits after #302, in throttled headless Chromium, were well below the shown ranges: two years waited 1.9 s against 3-24 s, three years 4.5 s against 11-68 s, and the loaded year 4.0 s against 4-26 s. The prior keeps its structure with refitted, rounded constants, 400 + 250 x (D/365.25)^0.5 x (P/30)^1.5 x R, which stays within about 1.2 times of all six measured configurations. Five years with 30 days per year now shows 6-37 s instead of 44-265 s. Local refinement is unchanged. The 4x CPU throttle slows only the main thread, not the planning worker, so phones can still wait longer than these desktop samples. Part of #302. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
75320da685
commit
31cb4a499c
5 files changed
+100
-19
No files matched your search
@@ -238,6 +238,76 @@ outcome, and delivery of its stable explanation. Confirm
|
|||||||
that editing an input remains possible during active worker CPU work. These
|
that editing an input remains possible during active worker CPU work. These
|
||||||
measurements describe that device and workload; they impose no latency limit.
|
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
|
## 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.
|
The following records remain as evidence for their dated builds. Trace-related behavior and metrics below are superseded by issue #76.
|
||||||
|
|||||||
+25
-14
@@ -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
|
control. Other malformed inputs keep a rough fallback estimate; the planner
|
||||||
still performs its own validation.
|
still performs its own validation.
|
||||||
|
|
||||||
The initial model is a deliberately rough engineering prior grounded in the
|
The initial model is a deliberately rough engineering prior fitted to the
|
||||||
[browser reference measurements](benchmark/README.md): the 2026-10-03 annual
|
rendered waits recorded in the
|
||||||
30-day scenario returned its outcome in about 0.6 seconds, and the 2026-10-05
|
[2026-10-18 rendered-wait recalibration](benchmark/README.md): six
|
||||||
two- and three-year calendar runs with 30 days per year took 5.08 and 10.86
|
configurations, from a five-day request to a three-year horizon with 30 days
|
||||||
seconds. These are throttled desktop measurements, not phone measurements or a
|
per year, each timed from the Calculate click to the rendered final calendar.
|
||||||
validated prediction model. The annual outcome timing does not include final
|
They were taken after the engine speedups of
|
||||||
calendar rendering. The new rendered-calendar measurements below supplement
|
[issue #302](https://gitea.nessar.fr/nessar/vac-optimizer/issues/302), which
|
||||||
those references.
|
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:
|
For a half-open horizon of `D` calendar days, the model uses:
|
||||||
|
|
||||||
```text
|
```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
|
`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
|
obligations, date distribution, interacting rules, earned entitlements, device
|
||||||
load and browser differences can all change the actual wait substantially.
|
load and browser differences can all change the actual wait substantially.
|
||||||
|
|
||||||
The powers and weights are implementation choices, not complexity guarantees
|
The constants are fitted to a handful of runs, not derived. The fixed 400 ms
|
||||||
or fitted accuracy claims. With 30 available days per year, the model's search
|
approximates request scheduling, worker startup and rendering (a one-year
|
||||||
term grows as the number of years to the power 2.7, rather than assuming a
|
request for 5 CP waited 465 ms). The one-to-three-year runs grew roughly as the
|
||||||
linear increase. Horizons beyond the references are unvalidated extrapolation.
|
number of years squared above such an overhead (730, 1,909 and 4,477 ms).
|
||||||
The estimate performs one pass over balances and rules and runs no planner.
|
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
|
## Local refinement and precision
|
||||||
|
|
||||||
|
|||||||
@@ -1782,7 +1782,7 @@ describe('CalendarYear planning outcome boundary', () => {
|
|||||||
const fixture = TestBed.createComponent(CalendarYear);
|
const fixture = TestBed.createComponent(CalendarYear);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
now = 21_000;
|
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.querySelector('.loading-overlay')).not.toBeNull();
|
||||||
expect(fixture.nativeElement.querySelectorAll('button.selected')).toHaveLength(0);
|
expect(fixture.nativeElement.querySelectorAll('button.selected')).toHaveLength(0);
|
||||||
pending.resolve(plan);
|
pending.resolve(plan);
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ describe('Planning duration estimate', () => {
|
|||||||
|
|
||||||
it('offers a coarse seconds range on the first launch without calculating', () => {
|
it('offers a coarse seconds range on the first launch without calculating', () => {
|
||||||
const duration = TestBed.inject(PlanningDurationService);
|
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).isCalculating()).toBe(false);
|
||||||
expect(TestBed.inject(PlanningCalculationService).accepted()).toBeNull();
|
expect(TestBed.inject(PlanningCalculationService).accepted()).toBeNull();
|
||||||
expect(TestBed.inject(RuleService).rules).toEqual([]);
|
expect(TestBed.inject(RuleService).rules).toEqual([]);
|
||||||
@@ -133,7 +133,7 @@ describe('Planning duration estimate', () => {
|
|||||||
TestBed.resetTestingModule();
|
TestBed.resetTestingModule();
|
||||||
expect(TestBed.inject(PlanningDurationService).estimate()).toEqual({
|
expect(TestBed.inject(PlanningDurationService).estimate()).toEqual({
|
||||||
minimum: 1,
|
minimum: 1,
|
||||||
maximum: 3,
|
maximum: 2,
|
||||||
unit: 'seconds',
|
unit: 'seconds',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -44,9 +44,9 @@ function initialDurationMs(request: PlanningRequest): number {
|
|||||||
Math.max(1, 'placeDays' in rule ? rule.placeDays / 3 : 1);
|
Math.max(1, 'placeDays' in rule ? rule.placeDays / 3 : 1);
|
||||||
if ('rewardDays' in rule) placements += (Math.max(0, rule.rewardDays) * days) / 365.25;
|
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 =
|
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;
|
return Number.isFinite(duration) ? duration : 1_000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user