CI / policy (pull_request) Successful in 7s
CI / validate (pull_request) Successful in 22s
CI / benchmark (pull_request) Successful in 1m9s
CI / build (pull_request) Successful in 34s
CI / security (pull_request) Successful in 55s
CI / test (pull_request) Successful in 1m45s
CI / images (pull_request) Successful in 53s
CI / required (pull_request) Successful in 2s
CI / policy (push) Successful in 12s
CI / validate (push) Successful in 31s
CI / build (push) Successful in 29s
CI / benchmark (push) Successful in 1m53s
CI / security (push) Successful in 1m31s
CI / test (push) Successful in 2m12s
CI / images (push) Successful in 1m15s
CI / required (push) Successful in 3s
The expected wait sits near the middle of recent waits, so about half of all runs pass it. Between the expected wait and the top of the range shown before Calculate, the percentage now holds at 95% with no remaining-time line; "Taking longer than expected" appears and is announced only past that top. Refs #300 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
141 lines
7.9 KiB
Markdown
141 lines
7.9 KiB
Markdown
# Indicative wait before Calculate
|
||
|
||
The small range beside **Calculate** describes the configuration currently in
|
||
the form. Editing dates, balances or rules updates it without requesting a plan
|
||
or changing the previously displayed calendar. It is an approximation, with no
|
||
accuracy guarantee or automatic stopping deadline.
|
||
|
||
## Initial estimate
|
||
|
||
`PlanningDurationService` shares the date-only request with `CalendarYear` so
|
||
the estimate and calculation use the same horizon, balances and rules. Invalid
|
||
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 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 = 400 + 250 × (D / 365.25)^0.5 × (max(1, P) / 30)^1.5 × R
|
||
```
|
||
|
||
`P` approximates placement load. Each non-negative finite balance contributes
|
||
at most five sevenths of its calendar-day overlap with the horizon, availability
|
||
and inclusive expiry. An at-risk balance has weight 1; a deferred balance has
|
||
weight 0.25 to account for possible supporting placements. Already expired and
|
||
not-yet-available balances contribute nothing. Each enabled IF_THEN rule adds
|
||
its non-negative earned quantity times `D / 365.25` as a rough allowance.
|
||
|
||
`R` starts at 1. Each enabled rule adds `max(1, placeDays / 3)` times 0.5 for an
|
||
obligation or 0.15 for a preference. TIME_WINDOW rules wholly outside the
|
||
horizon contribute nothing. These weights approximate extra search, rather than
|
||
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 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
|
||
|
||
After each successfully rendered `Plan`, the service retains the ratio of its
|
||
observed elapsed time to the initial model for that calculation's immutable
|
||
submitted snapshot. The median of the most recent eight ratios scales the
|
||
initial estimate for the current form. A changed form therefore does not
|
||
relabel the completed measurement as a calculation of the new configuration.
|
||
The median limits the effect of isolated unusually slow runs; it cannot remove
|
||
systematic model error or predict arbitrary obligation searches.
|
||
|
||
Only eight numeric ratios are retained in memory for the current application
|
||
session. Reloading restores the initial estimate. Nothing is persisted or sent
|
||
to a remote service, so there is no saved-measurement restoration dependency.
|
||
Storage failure does not affect this service.
|
||
|
||
The displayed lower bound is half the estimated duration, rounded down; the
|
||
upper bound is three times that duration, rounded up. Values are whole seconds,
|
||
or whole minutes when the lower bound is at least one minute. The minimum
|
||
displayed value is 1 and the upper bound is at least one unit higher. Internal
|
||
duration rounding to whole milliseconds prevents floating-point noise at unit
|
||
boundaries. Very fast runs can finish before the displayed lower bound. The
|
||
range is explicitly indicative, not a confidence interval or a promise.
|
||
|
||
## During a calculation
|
||
|
||
The loading overlay's percentage follows the indicative wait, not the planning
|
||
stages completed. At Calculate, `PlanningDurationService` freezes the expected
|
||
duration `e` for the submitted snapshot, including the local refinement. On a
|
||
250 ms tick, it measures elapsed time from the same timestamp the calibration
|
||
uses. Progress rises linearly to 95% at `e`, holds there and shows 100% only
|
||
when the planner reports completion. Per-stage weights are not used, because
|
||
the costly stage depends on the configuration. Form edits and repeated stages
|
||
cannot move it backwards. On Cancel, the tick stops and the percentage freezes.
|
||
|
||
Below the status, one line names the current planning stage. For an
|
||
optimization or preference stage, it shows the rule's name. When `e` is at least
|
||
3 seconds, a second line shows "About N s left". This is in whole seconds under
|
||
10 seconds, 5-second steps under a minute and whole minutes after that. From
|
||
`e` to the top of the range shown before Calculate, the percentage holds at 95%
|
||
and that line is hidden. Because `e` sits near the middle of recent waits, about
|
||
half of all runs pass it, so it is no reason to warn the user. Past the top of
|
||
the range, for any `e`, the line reads "Taking longer than expected" and the
|
||
live status announces it once. Both lines are visible text, not live regions,
|
||
and are hidden after Cancel.
|
||
|
||
## What is timed
|
||
|
||
Every request, manual or initial, captures `performance.now()` inside
|
||
`PlanningCalculationService.start`.
|
||
The endpoint is Angular's `afterNextRender` callback after the complete plan has
|
||
been projected into the calendar, its months and placements have been rendered,
|
||
and the loading overlay has been removed. This measures request scheduling,
|
||
calendar preparation, worker startup, search, outcome transport, projection and Angular rendering. It is a DOM
|
||
readiness endpoint, rather than a claim to measure a physical screen's paint.
|
||
|
||
Neither core `planningWorkMs` nor artifact timings supply the calibration
|
||
measurement. The callback does not wait for the explanation promise;
|
||
later delivery, navigation and explanation errors never add a second measurement.
|
||
Cancellation (including a retained incumbent), incomplete outcomes, invalid
|
||
inputs, conflicts and calculation errors do not calibrate successful-plan
|
||
waiting time. Superseded calculations and callbacks for destroyed calendars
|
||
are ignored. Zero, negative and non-finite elapsed values are discarded.
|
||
|
||
Exceeding the range has no side effects. The estimate supplies no deadline,
|
||
timer, date adjustment or new calculation. **Cancel** remains explicit.
|
||
|
||
## Verification
|
||
|
||
Controlled-clock tests at the existing calendar and input seams cover initial
|
||
estimation, unsubmitted edits, minute formatting, local refinement, request
|
||
scheduling, a twenty-second calculation exceeding its initial range, an explanation
|
||
that arrives after calendar readiness, cancellation with and without
|
||
an incumbent, incomplete/error outcomes and superseded calculations. No timing
|
||
assertion depends on CI hardware speed. Existing settings, planner and worker
|
||
tests cover saved horizons, long calculations and interrupted-plan explanations.
|
||
|
||
Rendered annual, two-year and three-year references, with the actual displayed
|
||
range and historical artifact-delivery wait, are recorded in
|
||
[the benchmark report](benchmark/README.md).
|