Make metrics reflect autoscaling policies (Refs #137)
CI / checks (push) Successful in 4m49s
CI / checks (push) Successful in 4m49s
This commit is contained in:
+79
-62
@@ -1,113 +1,130 @@
|
||||
import type { MapMetrics } from './metrics.js';
|
||||
import { metricDuration, metricPercent } from './ui/metrics.js';
|
||||
|
||||
/** Product hypotheses for advisory experiments; these are not demand forecasts. */
|
||||
export const HOSTING_RULES = {
|
||||
pressureExpansion: 0.2,
|
||||
occupancyExpansion: 0.8,
|
||||
idleUtilization: 0.1,
|
||||
quietPressure: 0.01,
|
||||
minimumEvidenceHours: 1
|
||||
/** Advisory thresholds for reviewing a policy, not demand forecasts or scaling commands. */
|
||||
export const POLICY_SIGNAL_RULES = {
|
||||
noEmptyShare: 0.02,
|
||||
minimumEvidenceHours: 1,
|
||||
minimumKnownShare: 0.8
|
||||
} as const;
|
||||
|
||||
export interface MetricRecommendation {
|
||||
game: string;
|
||||
map: string;
|
||||
kind: 'expand' | 'consolidate' | 'capacity' | 'watch';
|
||||
kind: 'maximum' | 'spares' | 'policy' | 'reporting' | 'watch';
|
||||
title: string;
|
||||
reason: string;
|
||||
evidence: string;
|
||||
outcome: string;
|
||||
tentative: boolean;
|
||||
policySummary: string | null;
|
||||
href: string | null;
|
||||
action: string | null;
|
||||
}
|
||||
|
||||
/** Apply the same visible rules to all selected maps, including sparse observations. */
|
||||
/** Compare captured availability with the current policy; historical settings are not recorded. */
|
||||
export function metricRecommendations(maps: MapMetrics[]): MetricRecommendation[] {
|
||||
return maps
|
||||
.map((map) => {
|
||||
const utilization = map.observed > 0 ? map.active / map.observed : null;
|
||||
const pressure = map.capacityObserved > 0 ? map.full / map.capacityObserved : null;
|
||||
const knownShare =
|
||||
map.observed + map.unknown > 0 ? map.observed / (map.observed + map.unknown) : 0;
|
||||
.map((map): MetricRecommendation => {
|
||||
const availability = map.availabilityObserved + map.availabilityUnknown;
|
||||
const knownShare = availability > 0 ? map.availabilityObserved / availability : 0;
|
||||
const lobbyHours = map.observed + map.unknown;
|
||||
const knownLobbyShare = lobbyHours > 0 ? map.observed / lobbyHours : 0;
|
||||
const noEmptyShare =
|
||||
map.availabilityObserved > 0 ? map.noEmpty / map.availabilityObserved : null;
|
||||
const policy = map.policy;
|
||||
const base = {
|
||||
game: map.game,
|
||||
map: map.map,
|
||||
evidence:
|
||||
`${metricDuration(map.active)} with players / ${metricDuration(map.observed)} known server time (${metricPercent(map.active, map.observed)}). ` +
|
||||
(pressure === null
|
||||
? 'Capacity unavailable.'
|
||||
: `All lobbies full ${metricDuration(map.full)} / ${metricDuration(map.capacityObserved)} known map time (${metricPercent(map.full, map.capacityObserved)}).`),
|
||||
`${metricDuration(map.active)} with players / ${metricDuration(map.observed)} known lobby time (${metricPercent(map.active, map.observed)}). ` +
|
||||
(map.unknown > 0
|
||||
? `${metricDuration(map.unknown)} of lobby time has unknown occupancy. `
|
||||
: '') +
|
||||
(noEmptyShare === null
|
||||
? 'Lobby availability is unknown.'
|
||||
: `No empty lobby reported for ${metricDuration(map.noEmpty)} / ${metricDuration(map.availabilityObserved)} known map time (${metricPercent(map.noEmpty, map.availabilityObserved)}).`) +
|
||||
(policy && noEmptyShare !== null
|
||||
? ` At or above the current maximum (${policy.maximum}): ${metricDuration(map.noEmptyAtMaximum)}; below it: ${metricDuration(map.noEmptyBelowMaximum)}.`
|
||||
: ''),
|
||||
tentative:
|
||||
map.observed < HOSTING_RULES.minimumEvidenceHours ||
|
||||
pressure === null ||
|
||||
map.capacityObserved < HOSTING_RULES.minimumEvidenceHours ||
|
||||
knownShare < 0.8
|
||||
map.availabilityObserved < POLICY_SIGNAL_RULES.minimumEvidenceHours ||
|
||||
knownShare < POLICY_SIGNAL_RULES.minimumKnownShare ||
|
||||
knownLobbyShare < POLICY_SIGNAL_RULES.minimumKnownShare,
|
||||
policySummary: policy
|
||||
? `Current policy: ${policy.minimum}–${policy.maximum} lobbies, ${policy.spares} spare${policy.spares === 1 ? '' : 's'}`
|
||||
: null,
|
||||
href: policy ? `/autoscaling#${encodeURIComponent(policy.id)}` : null,
|
||||
action: policy ? 'Review policy' : null
|
||||
};
|
||||
if (utilization === null)
|
||||
if (noEmptyShare === null)
|
||||
return {
|
||||
...base,
|
||||
kind: 'capacity' as const,
|
||||
kind: 'reporting',
|
||||
title:
|
||||
map.unknown > 0
|
||||
map.availabilityUnknown > 0 || map.unknown > 0
|
||||
? 'Check occupancy reporting'
|
||||
: 'Let observations accumulate',
|
||||
reason:
|
||||
map.unknown > 0
|
||||
? 'Captured status is unknown. Check a server’s telemetry or RCON diagnostics before choosing a hosting change.'
|
||||
: 'This map has no elapsed observations in the selected window. Wait for another status check or try a wider range.',
|
||||
outcome: 'Watch for known server time in the next report.',
|
||||
map.availabilityUnknown > 0 || map.unknown > 0
|
||||
? 'Captured occupancy is unknown. Check a lobby’s telemetry or RCON diagnostics before choosing a policy change.'
|
||||
: 'This map has no elapsed availability observations in the selected window. Wait for another status check or try a wider range.',
|
||||
outcome: 'Watch for known lobby availability in the next report.',
|
||||
tentative: true
|
||||
};
|
||||
if (pressure !== null && pressure >= HOSTING_RULES.pressureExpansion)
|
||||
if (!policy)
|
||||
return {
|
||||
...base,
|
||||
kind: 'expand' as const,
|
||||
title: 'Trial one additional lobby',
|
||||
reason: 'Recorded lobbies were all full for at least 20% of known capacity time. Try one extra lobby during busy hours.',
|
||||
kind: 'policy',
|
||||
title: 'Consider adding an autoscaling policy',
|
||||
reason: 'This map has no current autoscaling policy. Set a minimum, maximum, and spare target to maintain an empty lobby as players arrive.',
|
||||
outcome:
|
||||
'Compare all-full time and use of the additional lobby over the next comparable period.'
|
||||
'Choose initial limits from observed lobby use, then compare empty-lobby availability over a comparable period.',
|
||||
href: '/autoscaling',
|
||||
action: 'Add policy'
|
||||
};
|
||||
if (pressure === null && utilization >= HOSTING_RULES.occupancyExpansion)
|
||||
if (policy.pausedReason || policy.mode === 'off')
|
||||
return {
|
||||
...base,
|
||||
kind: 'expand' as const,
|
||||
title: 'Trial an extra lobby during busy hours',
|
||||
reason: 'Players were present for at least 80% of known server time. Capacity is unknown, so this experiment uses occupancy as a proxy, not evidence of saturation.',
|
||||
kind: 'policy',
|
||||
title: policy.pausedReason
|
||||
? 'Review and resume the autoscaling policy'
|
||||
: 'Review and enable the autoscaling policy',
|
||||
reason: policy.pausedReason
|
||||
? `The current policy is paused: ${policy.pausedReason}`
|
||||
: 'The current policy is off, so it cannot maintain the spare target.',
|
||||
outcome:
|
||||
'Check whether players use the extra lobby, and measure capacity if the server reports it.',
|
||||
tentative: true
|
||||
'Review the policy and any blockers before enabling it, then observe availability before adjusting its limits.'
|
||||
};
|
||||
if (
|
||||
utilization <= HOSTING_RULES.idleUtilization &&
|
||||
(pressure === null || pressure < HOSTING_RULES.quietPressure)
|
||||
)
|
||||
if (noEmptyShare >= POLICY_SIGNAL_RULES.noEmptyShare) {
|
||||
if (map.noEmptyAtMaximum >= map.noEmptyBelowMaximum)
|
||||
return {
|
||||
...base,
|
||||
kind: 'maximum',
|
||||
title: 'Consider raising the policy maximum',
|
||||
reason: `No empty lobby was reported for at least 2% of known map time. At least half of that time had a tracked lobby count at or above the current maximum of ${policy.maximum}; review whether that limit leaves room for the spare target.`,
|
||||
outcome:
|
||||
'Review host resources and create time, then compare empty-lobby availability after a maximum change.'
|
||||
};
|
||||
return {
|
||||
...base,
|
||||
kind: 'consolidate' as const,
|
||||
title: 'Trial less idle hosting',
|
||||
reason: 'Players were present for at most 10% of known time, with less than 1% measured pressure or unavailable capacity. Review one idle lobby or a shorter hosting window; preserve access to this map.',
|
||||
kind: 'spares',
|
||||
title: 'Review the spare target and create time',
|
||||
reason: `No empty lobby was reported for at least 2% of known map time. Most of that time was below the current maximum of ${policy.maximum}; consider raising the spare target from ${policy.spares} and check how long new lobbies take to become ready.`,
|
||||
outcome:
|
||||
'Watch for increased all-full time or loss of player-hours, and restore the lineup if the trial hurts availability.'
|
||||
};
|
||||
if (pressure === null)
|
||||
return {
|
||||
...base,
|
||||
kind: 'capacity' as const,
|
||||
title: 'Check capacity during busy hours',
|
||||
reason: 'Occupancy is recorded, but player limits are unavailable. Check capacity reporting while keeping the current lineup.',
|
||||
outcome:
|
||||
'Look for known-capacity time and all-full periods before the next hosting trial.'
|
||||
'Review creation delays and blockers, then compare empty-lobby availability after a spare-target change.'
|
||||
};
|
||||
}
|
||||
return {
|
||||
...base,
|
||||
kind: 'watch' as const,
|
||||
title: 'Keep the lineup and watch peaks',
|
||||
reason: 'These observations do not trigger the expansion or idle-hosting rules. Keep the current lineup for another comparison period.',
|
||||
outcome: 'Watch whether occupied time and measured capacity pressure change.'
|
||||
kind: 'watch',
|
||||
title: 'Keep the policy and watch availability',
|
||||
reason: 'No-empty-lobby time is below the policy review threshold. These observations do not suggest increasing the maximum or spare target.',
|
||||
outcome: 'Watch for recurring periods without a reported empty lobby.'
|
||||
};
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const priority = { expand: 0, consolidate: 1, capacity: 2, watch: 3 };
|
||||
const priority = { maximum: 0, spares: 1, policy: 2, reporting: 3, watch: 4 };
|
||||
return (
|
||||
priority[a.kind] - priority[b.kind] ||
|
||||
a.map.localeCompare(b.map) ||
|
||||
|
||||
Reference in New Issue
Block a user