feat: add PlanningDurationService for estimating planning duration

- Implemented PlanningDurationService to provide an indicative wait time before calculation based on user input and calendar settings.
- Introduced duration estimation logic that considers horizon dates, balances, and rules without triggering a calculation.
- Enhanced user input component to display estimated duration using the new service.
- Updated translation files to include new duration estimate strings in multiple languages.
- Added tests for PlanningDurationService to ensure accurate duration estimation and calibration.
- Refactored leave planner service to remove hardcoded deadlines and improve worker responsiveness.
This commit is contained in:
nessar committed 2026-10-05 17:06:17 +02:00
1 parent 2edf2004f5
commit bc8570d8ca
29 files changed
+1064 -123

No files matched your search

+30 -1
View File
@@ -17,6 +17,8 @@ export interface BenchmarkMeasurement {
planningMs: number;
traceDeliveryMs: number;
maxUiDelayMs: number;
/** Chromium's main-thread heap only; worker memory needs a separate browser profile. */
peakMainThreadHeapBytes?: number;
planningWorkMs?: number;
traceCaptureMs?: number;
traceMaterializationMs?: number;
@@ -147,6 +149,25 @@ export function benchmarkScenarios(): BenchmarkScenario[] {
})),
},
},
...[2, 3].map(
(years): BenchmarkScenario => ({
name: years === 2 ? 'two years' : 'three years',
request: {
horizon: { start: '2024-01-01', end: `${2024 + years}-01-01` },
balances: Array.from({ length: years }, (_, index) => 2024 + index).flatMap(
(year) =>
(['cp', 'rtt'] as const).map((type) => ({
id: `${type}-${year}`,
type,
quantity: type === 'cp' ? 20 : 10,
availableFrom: `${year}-01-01`,
expiryDate: `${year}-12-31`,
})),
),
rules: [],
},
}),
),
];
}
@@ -200,11 +221,17 @@ async function measureRepetition(
const wallStart = performance.now();
let lastTick = wallStart;
let maxUiDelayMs = 0;
let peakMainThreadHeapBytes: number | undefined;
const measureDelay = () => {
const current = performance.now();
maxUiDelayMs = Math.max(maxUiDelayMs, current - lastTick - 50);
lastTick = current;
const heap = (performance as Performance & { memory?: { usedJSHeapSize: number } }).memory
?.usedJSHeapSize;
if (heap !== undefined)
peakMainThreadHeapBytes = Math.max(peakMainThreadHeapBytes ?? 0, heap);
};
measureDelay();
const heartbeat = setInterval(measureDelay, 50);
try {
const preparationStart = performance.now();
@@ -242,6 +269,7 @@ async function measureRepetition(
planningMs,
traceDeliveryMs,
maxUiDelayMs,
peakMainThreadHeapBytes,
...timings,
wallMs,
...summary,
@@ -258,7 +286,7 @@ async function measureRepetition(
export function benchmarkReport(measurements: BenchmarkMeasurement[]): string {
const header =
'scenario\trepetition\tpreparation_ms\tplanning_ms\ttrace_delivery_ms\twall_ms\toutcome\tlast_stage\tplacements\tdeterministic\ttrace_status\ttrace_candidates\tmax_ui_delay_ms\tplanning_work_ms\ttrace_capture_ms\ttrace_materialization_ms\ttrace_transfer_ms';
'scenario\trepetition\tpreparation_ms\tplanning_ms\ttrace_delivery_ms\twall_ms\toutcome\tlast_stage\tplacements\tdeterministic\ttrace_status\ttrace_candidates\tmax_ui_delay_ms\tplanning_work_ms\ttrace_capture_ms\ttrace_materialization_ms\ttrace_transfer_ms\tpeak_main_thread_heap_bytes';
return [
header,
...measurements.map((item) =>
@@ -280,6 +308,7 @@ export function benchmarkReport(measurements: BenchmarkMeasurement[]): string {
item.traceCaptureMs?.toFixed(1) ?? '',
item.traceMaterializationMs?.toFixed(1) ?? '',
item.traceTransferMs?.toFixed(1) ?? '',
item.peakMainThreadHeapBytes ?? '',
].join('\t'),
),
].join('\n');