feat(benchmark): enhance benchmark documentation and add emulated run details
fix(benchmark): update deterministic checks and improve measurement accuracy fix(service): refine candidate recording and enhance balance assignment logic test(service): add test for balance recording with preference assignment
This commit is contained in:
1 parent
ff86cf48b4
commit
f1f08356df
5 files changed
+300
-98
No files matched your search
@@ -582,15 +582,19 @@ class TraceRecorder {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
recordCandidate(candidate: PlanningTraceCandidate): void {
|
||||
this.latestDecision().candidates.push({
|
||||
...candidate,
|
||||
dates: candidate.dates ? [...candidate.dates] : undefined,
|
||||
reason: candidate.reason ? { ...candidate.reason } : undefined,
|
||||
placement: candidate.placement
|
||||
? { ...candidate.placement, ruleIds: [...candidate.placement.ruleIds] }
|
||||
: undefined,
|
||||
});
|
||||
recordCandidate(candidate: PlanningTraceCandidate, copy = true): void {
|
||||
this.latestDecision().candidates.push(
|
||||
copy
|
||||
? {
|
||||
...candidate,
|
||||
dates: candidate.dates ? [...candidate.dates] : undefined,
|
||||
reason: candidate.reason ? { ...candidate.reason } : undefined,
|
||||
placement: candidate.placement
|
||||
? { ...candidate.placement, ruleIds: [...candidate.placement.ruleIds] }
|
||||
: undefined,
|
||||
}
|
||||
: candidate,
|
||||
);
|
||||
}
|
||||
|
||||
recordDecision(
|
||||
@@ -891,7 +895,8 @@ function observeTrace(context: SearchContext, observe: (trace: TraceRecorder) =>
|
||||
}
|
||||
|
||||
function recordTraceCandidate(context: SearchContext, candidate: PlanningTraceCandidate): void {
|
||||
observeTrace(context, (trace) => trace.recordCandidate(candidate));
|
||||
// These internal callsites pass fresh literals; retain the full snapshot copy for plan data.
|
||||
observeTrace(context, (trace) => trace.recordCandidate(candidate, false));
|
||||
}
|
||||
|
||||
function recordTraceIneligible(
|
||||
@@ -1633,7 +1638,7 @@ function assignDate(
|
||||
ruleId: string,
|
||||
context: SearchContext,
|
||||
): State | null {
|
||||
return assignDateVariants(state, date, selector, ruleId, context)[0] ?? null;
|
||||
return assignDateVariants(state, date, selector, ruleId, context, true)[0] ?? null;
|
||||
}
|
||||
|
||||
function assignDateVariants(
|
||||
@@ -1642,6 +1647,7 @@ function assignDateVariants(
|
||||
selector: LeaveSelector,
|
||||
ruleId: string,
|
||||
context: SearchContext,
|
||||
firstOnly = false,
|
||||
): State[] {
|
||||
context.check();
|
||||
const existing = state.placements.get(date);
|
||||
@@ -1724,6 +1730,40 @@ function assignDateVariants(
|
||||
'No compatible leave balance can fund this candidate date.',
|
||||
),
|
||||
});
|
||||
if (firstOnly && balances.length) {
|
||||
const first = balances[0];
|
||||
const result = cloneState(state);
|
||||
result.balances.find((item) => item.id === first.id)!.remaining--;
|
||||
result.placements.set(date, {
|
||||
date,
|
||||
balanceId: first.id,
|
||||
type: first.type,
|
||||
ruleIds: ruleId ? new Set([ruleId]) : new Set(),
|
||||
supporting: false,
|
||||
});
|
||||
for (const [index, balance] of balances.entries()) {
|
||||
recordTraceCandidate(context, {
|
||||
status: index === 0 ? 'evaluated-rejected' : 'not-examined',
|
||||
date,
|
||||
...(index === 0
|
||||
? {}
|
||||
: {
|
||||
reason: reason(
|
||||
'first-compatible-balance',
|
||||
'This balance was not examined after the first compatible balance was selected.',
|
||||
),
|
||||
}),
|
||||
placement: {
|
||||
date,
|
||||
balanceId: balance.id,
|
||||
type: balance.type,
|
||||
ruleIds: ruleId ? [ruleId] : [],
|
||||
supporting: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
return [result];
|
||||
}
|
||||
return balances.map((balance) => {
|
||||
const result = cloneState(state);
|
||||
const target = result.balances.find((item) => item.id === balance.id)!;
|
||||
@@ -1806,23 +1846,40 @@ function assignDateToBalanceInPlace(
|
||||
date: string,
|
||||
balanceId: string,
|
||||
context: SearchContext,
|
||||
ruleIds = new Set<string>(),
|
||||
): boolean {
|
||||
context.check();
|
||||
if (state.placements.has(date)) return false;
|
||||
if (state.placements.has(date)) {
|
||||
recordTraceCandidate(context, {
|
||||
status: 'ineligible',
|
||||
date,
|
||||
reason: reason('occupied', 'Another placement already uses this date.'),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
const balance = state.balances.find((item) => item.id === balanceId);
|
||||
if (
|
||||
!balance ||
|
||||
balance.remaining <= 0 ||
|
||||
compareDate(balance.availableFrom, date) > 0 ||
|
||||
compareDate(date, balance.expiryDate) > 0
|
||||
)
|
||||
) {
|
||||
recordTraceCandidate(context, {
|
||||
status: 'evaluated-rejected',
|
||||
date,
|
||||
reason: reason(
|
||||
'balance-unavailable',
|
||||
'The selected leave balance cannot fund this date.',
|
||||
),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
balance.remaining--;
|
||||
state.placements.set(date, {
|
||||
date,
|
||||
balanceId,
|
||||
type: balance.type,
|
||||
ruleIds: new Set(),
|
||||
ruleIds: new Set(ruleIds),
|
||||
supporting: false,
|
||||
});
|
||||
recordTraceCandidate(context, {
|
||||
@@ -1830,7 +1887,7 @@ function assignDateToBalanceInPlace(
|
||||
date,
|
||||
placement: {
|
||||
...state.placements.get(date)!,
|
||||
ruleIds: [],
|
||||
ruleIds: [...state.placements.get(date)!.ruleIds],
|
||||
},
|
||||
});
|
||||
return true;
|
||||
@@ -2637,60 +2694,69 @@ function optimizeMovablePlacements(
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const candidate = assignDateToBalance(
|
||||
without,
|
||||
date,
|
||||
current.balanceId,
|
||||
context,
|
||||
current.ruleIds,
|
||||
);
|
||||
if (!candidate) continue;
|
||||
const feasibleCandidate = finalizeEarnedState(candidate, context);
|
||||
if (!feasibleCandidate) continue;
|
||||
feasibleCandidate.placements.get(date)!.supporting = current.supporting;
|
||||
const metrics = qualityMetrics(feasibleCandidate, context, preferences);
|
||||
recordTraceEvaluation(
|
||||
context,
|
||||
result,
|
||||
feasibleCandidate,
|
||||
preferences,
|
||||
baseline,
|
||||
metrics,
|
||||
[date],
|
||||
current.date,
|
||||
);
|
||||
if (metrics.unplacedAtRisk !== baseline.unplacedAtRisk) continue;
|
||||
if (metrics.holidayWeekValue < baseline.holidayWeekValue) continue;
|
||||
if (
|
||||
metrics.preferenceSatisfaction.length !== baseline.preferenceSatisfaction.length ||
|
||||
metrics.preferenceSatisfaction.some(
|
||||
(value, index) => value !== baseline.preferenceSatisfaction[index],
|
||||
!assignDateToBalanceInPlace(
|
||||
without,
|
||||
date,
|
||||
current.balanceId,
|
||||
context,
|
||||
current.ruleIds,
|
||||
)
|
||||
)
|
||||
continue;
|
||||
const better =
|
||||
metrics.holidayWeekValue > bestMetrics.holidayWeekValue ||
|
||||
(metrics.holidayWeekValue === bestMetrics.holidayWeekValue &&
|
||||
(stage === 'non-working-day-gain'
|
||||
? metrics.nonWorkingDayGain > bestMetrics.nonWorkingDayGain
|
||||
: stage === 'longest-work-sequence'
|
||||
? metrics.nonWorkingDayGain === bestMetrics.nonWorkingDayGain &&
|
||||
metrics.longestWorkSequence < bestMetrics.longestWorkSequence
|
||||
: stage === 'short-sequence-deficit'
|
||||
try {
|
||||
const feasibleCandidate = finalizeEarnedState(without, context);
|
||||
if (!feasibleCandidate) continue;
|
||||
feasibleCandidate.placements.get(date)!.supporting = current.supporting;
|
||||
const metrics = qualityMetrics(feasibleCandidate, context, preferences);
|
||||
recordTraceEvaluation(
|
||||
context,
|
||||
result,
|
||||
feasibleCandidate,
|
||||
preferences,
|
||||
baseline,
|
||||
metrics,
|
||||
[date],
|
||||
current.date,
|
||||
);
|
||||
if (metrics.unplacedAtRisk !== baseline.unplacedAtRisk) continue;
|
||||
if (metrics.holidayWeekValue < baseline.holidayWeekValue) continue;
|
||||
if (
|
||||
metrics.preferenceSatisfaction.length !==
|
||||
baseline.preferenceSatisfaction.length ||
|
||||
metrics.preferenceSatisfaction.some(
|
||||
(value, index) => value !== baseline.preferenceSatisfaction[index],
|
||||
)
|
||||
)
|
||||
continue;
|
||||
const better =
|
||||
metrics.holidayWeekValue > bestMetrics.holidayWeekValue ||
|
||||
(metrics.holidayWeekValue === bestMetrics.holidayWeekValue &&
|
||||
(stage === 'non-working-day-gain'
|
||||
? metrics.nonWorkingDayGain > bestMetrics.nonWorkingDayGain
|
||||
: stage === 'longest-work-sequence'
|
||||
? metrics.nonWorkingDayGain === bestMetrics.nonWorkingDayGain &&
|
||||
metrics.longestWorkSequence ===
|
||||
bestMetrics.longestWorkSequence &&
|
||||
metrics.shortSequenceDeficit < bestMetrics.shortSequenceDeficit
|
||||
: metrics.nonWorkingDayGain === bestMetrics.nonWorkingDayGain &&
|
||||
metrics.longestWorkSequence ===
|
||||
bestMetrics.longestWorkSequence &&
|
||||
metrics.shortSequenceDeficit ===
|
||||
bestMetrics.shortSequenceDeficit &&
|
||||
date < bestDate));
|
||||
if (better) {
|
||||
best = feasibleCandidate;
|
||||
bestMetrics = metrics;
|
||||
bestDate = date;
|
||||
metrics.longestWorkSequence < bestMetrics.longestWorkSequence
|
||||
: stage === 'short-sequence-deficit'
|
||||
? metrics.nonWorkingDayGain === bestMetrics.nonWorkingDayGain &&
|
||||
metrics.longestWorkSequence ===
|
||||
bestMetrics.longestWorkSequence &&
|
||||
metrics.shortSequenceDeficit <
|
||||
bestMetrics.shortSequenceDeficit
|
||||
: metrics.nonWorkingDayGain === bestMetrics.nonWorkingDayGain &&
|
||||
metrics.longestWorkSequence ===
|
||||
bestMetrics.longestWorkSequence &&
|
||||
metrics.shortSequenceDeficit ===
|
||||
bestMetrics.shortSequenceDeficit &&
|
||||
date < bestDate));
|
||||
if (better) {
|
||||
best = cloneState(feasibleCandidate);
|
||||
bestMetrics = metrics;
|
||||
bestDate = date;
|
||||
}
|
||||
} finally {
|
||||
without.placements.delete(date);
|
||||
without.balances.find((item) => item.id === current.balanceId)!.remaining++;
|
||||
}
|
||||
}
|
||||
if (best !== result) recordTraceState(context, best);
|
||||
|
||||
Reference in new issue
Block a user