Make metrics reflect autoscaling policies (Refs #137)
CI / checks (push) Successful in 4m49s

This commit is contained in:
2026-10-10 14:48:52 -04:00
parent bd34c3c9e7
commit 33da4482a6
24 changed files with 1264 additions and 370 deletions
+141 -26
View File
@@ -24,8 +24,12 @@ const map = (i: number, changes: Partial<MapMetrics> = {}): MapMetrics => ({
idle: 5,
unknown: 0,
playerHours: i,
full: 1,
capacityObserved: 4,
noEmpty: 1,
availabilityObserved: 4,
availabilityUnknown: 0,
noEmptyAtMaximum: 0,
noEmptyBelowMaximum: 0,
policy: null,
...changes
});
const server = (i: number): ServerMetrics => ({
@@ -52,23 +56,33 @@ function report(): MetricsData {
scopeFirstSampleAt: now - 14 * 86400000,
scopeLatestSampleAt: now,
summary,
running: { average: 12, peak: 24, capturedHours: 1 },
maps: Array.from({ length: 24 }, (_, i) => map(i + 1)),
servers: Array.from({ length: 24 }, (_, i) => server(i + 1)),
points: [
{ ...summary, at: now - 86400000, startAt: now - 86400000, endAt: now, partial: true }
{
...summary,
at: now - 86400000,
startAt: now - 86400000,
endAt: now,
partial: true,
capturedHours: 1
}
],
previous: {
since: now - 14 * 86400000,
until: now - 7 * 86400000,
summary: { ...summary, active: 4, idle: 6, playerHours: 10 },
running: { average: 12, peak: 24, capturedHours: 1 },
firstSampleAt: now - 14 * 86400000,
lastSampleAt: now - 7 * 86400000,
partialHistory: true
},
pooledPressure: {
pooledAvailability: {
value: 25,
fullMapHours: 1,
noEmptyMapHours: 1,
knownMapHours: 4,
unknownMapHours: 0,
coveredMaps: 24,
totalMaps: 24
},
@@ -95,6 +109,9 @@ it('shows a summary and one paginated breakdown, preserving scope on range links
expect(body).toContain('game=t5&amp;days=30&amp;view=maps');
expect(body).toContain('Partial previous history');
expect(body).toContain('About this report');
expect(body).toContain('Average lobbies running:');
expect(body.replace(/\s+/g, ' ')).toContain('Captured wall time:');
expect(body).not.toContain('Percentage points show');
expect(body).not.toContain('Explore this period');
expect(body).toContain('Updates every minute');
expect(body).not.toContain('>Refresh<');
@@ -107,28 +124,44 @@ it('shows a summary and one paginated breakdown, preserving scope on range links
expect(body).not.toContain('class="weekly');
});
it('keeps percentage points, relative changes and unavailable capacity distinct', () => {
it('shows concurrent lobbies and empty spare share with unknown lobby-hours in the denominator', () => {
const data = report();
let body = render(MetricsSummary, { props: { report: data } }).body;
expect(body).toContain('+10 percentage points');
expect(body).toContain('12 avg');
expect(body).toContain('Peak 24 lobbies at once');
expect(body.replace(/\s+/g, ' ')).toContain(
'5 h of 12 h captured lobby-hours were empty spares (41.7%)'
);
expect(body).toContain('+100%');
data.pooledPressure = {
expect(body).toContain('Time with no empty lobby');
expect(body).toContain('25.0%');
expect(body).not.toContain('capacity data');
data.pooledAvailability = {
value: null,
fullMapHours: 0,
noEmptyMapHours: 0,
knownMapHours: 0,
unknownMapHours: 4,
coveredMaps: 0,
totalMaps: 24
};
data.maps = [map(1, { capacityObserved: 0 })];
data.maps = [map(1, { availabilityObserved: 0, availabilityUnknown: 4 })];
data.previous.summary.playerHours = 0;
body = render(MetricsSummary, { props: { report: data } }).body;
expect(body).toContain('Capacity data unavailable');
expect(body).toContain('Lobby availability unavailable');
expect(body).toContain('New · no previous player-hours');
expect(body.replace(/\s+/g, ' ')).toContain('1 of 1 maps lack capacity data');
expect(body).not.toContain('Infinity');
data.summary.observed = 0;
data.summary = { observed: 0, active: 0, idle: 0, playerHours: 0, unknown: 12 };
body = render(MetricsSummary, { props: { report: data } }).body;
expect(body).toContain('Comparison unavailable');
expect(body).toContain('12 avg');
expect(body.replace(/\s+/g, ' ')).toContain(
'0 h of 12 h captured lobby-hours were empty spares (0.0%)'
);
data.running = { average: null, peak: 0, capturedHours: 0 };
data.summary.unknown = 0;
body = render(MetricsSummary, { props: { report: data } }).body;
expect(body).toContain('Peak unavailable');
expect(body).not.toContain('Peak 0 lobbies');
});
it('searches and sorts the complete map set before pagination and clamps invalid pages', () => {
@@ -147,14 +180,20 @@ it('searches and sorts the complete map set before pagination and clamps invalid
expect(metricPage([], '20')).toMatchObject({ page: 1, start: 0, end: 0, total: 0 });
});
it('keeps missing capacity last when sorting and never presents it as zero full time', () => {
scope('?mapSort=full&mapDirection=asc');
const maps = [map(1, { capacityObserved: 0 }), map(2, { full: 0 }), map(3)];
it('keeps unknown availability last when sorting and distinguishes it from known empty lobbies', () => {
scope('?mapSort=noEmpty&mapDirection=asc');
const maps = [
map(1, { availabilityObserved: 0, availabilityUnknown: 4 }),
map(2, { noEmpty: 0 }),
map(3)
];
const body = render(MetricsTables, { props: { maps, servers: [] } }).body;
expect(body.indexOf('Map 02')).toBeLessThan(body.indexOf('Map 03'));
expect(body.indexOf('Map 03')).toBeLessThan(body.indexOf('Map 01'));
expect(body).toContain('0.0%');
expect(body).toContain('Capacity data unavailable');
expect(body).toContain('Lobby availability unavailable');
expect(body).toContain('No empty lobby');
expect(body).not.toContain('All lobbies full');
});
it('preserves instance return context and distinguishes historic names without current links', () => {
@@ -170,18 +209,18 @@ it('preserves instance return context and distinguishes historic names without c
expect(body).not.toContain('Open current server');
});
it('highlights at most three supported trials and collapses tentative findings', () => {
it('highlights at most three supported policy changes and collapses tentative findings', () => {
const data = report();
data.maps.push(map(25, { observed: 0.1, active: 0.1, idle: 0, capacityObserved: 0 }));
data.maps.push(map(25, { observed: 0.1, active: 0.1, idle: 0, availabilityObserved: 0 }));
const body = render(MetricsExplore, { props: { report: data } }).body;
const fold = body.indexOf('Other findings');
expect(body.slice(0, fold).match(/class="recommendation /g)).toHaveLength(3);
expect(body.indexOf('Map 25')).toBeGreaterThan(fold);
expect(body).toContain('Limited evidence');
expect(body).not.toMatch(/<details[^>]*open/);
data.maps = [map(1, { capacityObserved: 0 })];
data.maps = [map(1, { availabilityObserved: 0 })];
expect(render(MetricsExplore, { props: { report: data } }).body).toContain(
'No hosting trials meet the evidence threshold'
'No policy changes meet the evidence threshold'
);
});
@@ -196,20 +235,65 @@ it('keeps accessible chart selection, partial periods and gaps while collapsing
active: 0,
idle: 0,
unknown: 0,
playerHours: 0
playerHours: 0,
capturedHours: 0
});
const body = render(MetricsChart, {
props: { points: data.points, days: data.days, timeZone: data.scope.timeZone }
}).body;
expect(body).toContain('aria-label="Selected period summary"');
expect(body).toContain('No captured time · not evidence of idle servers');
expect(body).toContain('No captured time · not evidence of empty lobbies');
expect(body).toContain('tabindex="0"');
expect(body).toContain('Inspect a period');
expect(body).toContain('Partial period');
expect(body).not.toMatch(/<details[^>]*open/);
expect(body).toContain('Weekdays show when each period starts in America/New York.');
expect(body).toContain('Daily periods are 24-hour windows.');
expect(body).toContain('Server activity over time');
expect(body).toContain('Lobbies running over time');
expect(body).toContain('Average lobbies running');
expect(body).toContain('5 with players · 5 empty spares · 2 unknown on average');
});
it('keeps average lobby bar heights comparable across partial periods and collection gaps', () => {
const data = report();
const point = data.points[0];
data.points = [
{
...point,
observed: 48,
active: 12,
idle: 36,
unknown: 24,
capturedHours: 12,
partial: false
},
{
...point,
at: data.now,
startAt: data.now,
endAt: data.now + 2 * 3600000,
observed: 8,
active: 2,
idle: 6,
unknown: 4,
capturedHours: 2,
partial: true
}
];
const body = render(MetricsChart, {
props: { points: data.points, days: data.days, timeZone: data.scope.timeZone }
}).body;
const bars = body.match(/<button class="bar\b[\s\S]*?<\/button>/g) ?? [];
expect(bars).toHaveLength(2);
const heights = bars.map((bar) =>
[...bar.matchAll(/style="height:\s*([^"]+)"/g)].map((match) => match[1])
);
expect(heights[0]).toHaveLength(3);
expect(heights[0]).toEqual(heights[1]);
expect(heights[0]).toContain('50%;');
expect(
bars.every((bar) => bar.includes('1 with players · 3 empty spares · 2 unknown on average'))
).toBe(true);
});
it('uses shallow URL state for the selected breakdown while retaining the common summary', () => {
@@ -225,15 +309,46 @@ it('uses shallow URL state for the selected breakdown while retaining the common
props: { data: { ...report(), user: null, setupRequired: false } }
}).body;
expect(body).toContain('id="metrics-explore-title"');
expect(body).toContain('Time all lobbies were full');
expect(body).toContain('Time with no empty lobby');
expect(body).toContain('Autoscaling signals');
expect(body).not.toContain('aria-label="Map metrics table"');
expect(body).not.toContain('aria-label="Instance metrics table"');
});
it('filters other insights without hiding the highlighted supported trials', () => {
it('filters other signals without hiding the highlighted supported policy changes', () => {
scope('?view=insights&insightQ=Map+24&insightSort=map');
const body = render(MetricsExplore, { props: { report: report() } }).body;
expect(body).toContain('Map 01');
expect(body).toContain('Map 24');
expect(body).not.toContain('Map 23');
});
it('links autoscaling signals to current policies and explains their historical comparison', () => {
const data = report();
data.maps = [
map(1, {
noEmptyAtMaximum: 0.75,
noEmptyBelowMaximum: 0.25,
policy: {
id: 'policy-1',
mode: 'create-destroy',
minimum: 1,
maximum: 3,
spares: 1,
pausedReason: null
}
})
];
let body = render(MetricsExplore, { props: { report: data } }).body;
expect(body).toContain('href="/autoscaling#policy-1"');
expect(body).toContain('Review policy');
expect(body).toContain('Current policy: 1–3 lobbies, 1 spare');
expect(body.replace(/\s+/g, ' ')).toContain(
'Current settings are applied to historical lobby counts'
);
expect(body).not.toContain('player limits, and observation');
data.maps[0].policy = null;
body = render(MetricsExplore, { props: { report: data } }).body;
expect(body).toContain('href="/autoscaling"');
expect(body).toContain('Add policy');
});