Cover large-estate Proxmox section navigation

Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot]
2026-09-01 13:14:47 +01:00
parent 12b90e2e5e
commit 095d1cb107
2 changed files with 237 additions and 2 deletions
+4 -2
View File
@@ -143,8 +143,10 @@ Credentials used by the E2E suite can be overridden:
- `PULSE_E2E_ALLOW_NODE_MUTATION=1` to enable the optional "Add Proxmox node" test (disabled by default for safety)
- `PULSE_E2E_PERF=1` to enable navigation performance budget checks
- `PULSE_E2E_PERF_ITERATIONS` (default `3`)
- `PULSE_E2E_PERF_INFRA_TO_WORKLOADS_BUDGET_MS` (default `2200`)
- `PULSE_E2E_PERF_WORKLOADS_TO_INFRA_BUDGET_MS` (default `2200`)
- `PULSE_E2E_PERF_PROXMOX_TO_DOCKER_BUDGET_MS` (default `2200`)
- `PULSE_E2E_PERF_DOCKER_TO_PROXMOX_BUDGET_MS` (default `2200`)
- `PULSE_E2E_PERF_PROXMOX_OVERVIEW_TO_STORAGE_BUDGET_MS` (default `2200`)
- `PULSE_E2E_PERF_PROXMOX_STORAGE_TO_OVERVIEW_BUDGET_MS` (default `2200`)
### Run Against An Existing Pulse Instance
```bash
@@ -96,6 +96,123 @@ const measureTabTransition = async (
return Date.now() - start;
};
const makeLargeProxmoxEstate = () => {
const nodes = Array.from({ length: 50 }, (_, index) => {
const nodeNumber = index + 1;
return {
id: `synthetic:proxmox:node-${nodeNumber}`,
type: 'agent',
name: `pve-${String(nodeNumber).padStart(2, '0')}`,
status: 'online',
lastSeen: '2026-09-01T10:00:00Z',
sources: ['proxmox'],
platformScopes: ['proxmox-pve'],
metrics: {
cpu: { percent: (nodeNumber % 70) + 10 },
memory: { used: 24, total: 64, percent: 37.5 },
disk: { used: 320, total: 1024, percent: 31.25 },
},
proxmox: {
nodeName: `pve-${String(nodeNumber).padStart(2, '0')}`,
clusterName: 'synthetic-large-estate',
instance: 'synthetic-lab',
pveVersion: '9.0.3',
uptime: 1_000_000 + nodeNumber,
},
};
});
const guests = Array.from({ length: 900 }, (_, index) => {
const guestNumber = index + 1;
const nodeIndex = index % nodes.length;
const node = nodes[nodeIndex];
const nodeName = node.proxmox.nodeName;
const vmid = 100 + guestNumber;
return {
id: `synthetic:proxmox:${nodeName}:${vmid}`,
type: guestNumber % 4 === 0 ? 'system-container' : 'vm',
name: `workload-${String(guestNumber).padStart(4, '0')}`,
status: guestNumber % 9 === 0 ? 'stopped' : 'online',
parentId: node.id,
parentName: nodeName,
lastSeen: '2026-09-01T10:00:00Z',
sources: ['proxmox'],
platformScopes: ['proxmox-pve'],
metrics: {
cpu: { percent: guestNumber % 80 },
memory: { used: 2, total: 8, percent: 25 },
disk: { used: 20, total: 100, percent: 20 },
},
proxmox: {
nodeName,
clusterName: 'synthetic-large-estate',
instance: 'synthetic-lab',
vmid,
cpus: 4,
uptime: 100_000 + guestNumber,
},
};
});
const storage = Array.from({ length: 300 }, (_, index) => {
const storageNumber = index + 1;
const node = nodes[index % nodes.length];
const physicalDisk = storageNumber % 3 === 0;
return {
id: `synthetic:storage:${storageNumber}`,
type: physicalDisk ? 'physical_disk' : 'storage',
name: physicalDisk
? `/dev/disk/by-id/synthetic-${storageNumber}`
: `pool-${String(storageNumber).padStart(3, '0')}`,
status: 'online',
parentId: node.id,
parentName: node.proxmox.nodeName,
lastSeen: '2026-09-01T10:00:00Z',
sources: physicalDisk ? ['agent', 'proxmox'] : ['proxmox'],
platformScopes: ['proxmox-pve'],
metrics: { disk: { used: 400, total: 1000, percent: 40 } },
storage: physicalDisk ? undefined : { type: 'zfs', isZfs: true },
physicalDisk: physicalDisk
? { device: `/dev/sd${String.fromCharCode(97 + (storageNumber % 20))}` }
: undefined,
proxmox: {
nodeName: node.proxmox.nodeName,
clusterName: 'synthetic-large-estate',
instance: 'synthetic-lab',
},
};
});
return { overview: [...nodes, ...guests], storage: [...nodes, ...storage] };
};
const measureProxmoxSectionTransition = async (
page: Page,
section: 'Overview' | 'Storage',
): Promise<number> => {
const start = Date.now();
await page
.getByRole('navigation', { name: 'Proxmox sections' })
.getByRole('link', { name: section, exact: true })
.click();
if (section === 'Storage') {
await expect(page).toHaveURL(/\/proxmox\/storage(?:\?|$)/);
await expect(page.getByTestId('storage-page')).toBeVisible();
await expect(
page.getByTestId('storage-page').locator('table').first(),
).toBeVisible();
} else {
await expect(page).toHaveURL(/\/proxmox\/overview(?:\?|$)/);
await expect(page.getByTestId('proxmox-guests-section')).toBeVisible();
await expect(
page.getByTestId('proxmox-guests-section').locator('table').first(),
).toBeVisible();
}
return Date.now() - start;
};
test.describe.serial('Navigation performance budgets', () => {
test.skip(!truthy(process.env.PULSE_E2E_PERF), 'Set PULSE_E2E_PERF=1 to enable navigation perf checks');
@@ -162,4 +279,120 @@ test.describe.serial('Navigation performance budgets', () => {
}
}
});
test('large-estate Proxmox section switches stay within budget', async ({
page,
browserName,
isMobile,
}) => {
test.skip(
browserName !== 'chromium' || Boolean(isMobile),
'Perf budgets are pinned to desktop Chromium',
);
test.slow();
const iterations = toPositiveInt(process.env.PULSE_E2E_PERF_ITERATIONS, 3);
const overviewToStorageBudgetMs = toPositiveInt(
process.env.PULSE_E2E_PERF_PROXMOX_OVERVIEW_TO_STORAGE_BUDGET_MS,
2200,
);
const storageToOverviewBudgetMs = toPositiveInt(
process.env.PULSE_E2E_PERF_PROXMOX_STORAGE_TO_OVERVIEW_BUDGET_MS,
2200,
);
const estate = makeLargeProxmoxEstate();
const typeCounts = {
agent: 50,
vm: 675,
'system-container': 225,
storage: 200,
physical_disk: 100,
};
await page.routeWebSocket('**/ws*', () => {});
await page.route('**/api/resources?**', async (route) => {
const url = new URL(route.request().url());
const types = url.searchParams.get('type');
const sources = url.searchParams.get('source');
let resources: readonly Record<string, unknown>[] | undefined;
if (
types === 'agent,vm,system-container,oci-container' &&
sources === 'proxmox'
) {
resources = estate.overview;
} else if (
types === 'agent,pbs,storage,physical_disk,ceph' &&
sources === 'proxmox,pbs,agent'
) {
resources = estate.storage;
}
if (!resources) {
await route.continue();
return;
}
const pageNumber = Math.max(1, Number(url.searchParams.get('page')) || 1);
const limit = Math.max(1, Number(url.searchParams.get('limit')) || 100);
const totalPages = Math.max(1, Math.ceil(resources.length / limit));
const start = (pageNumber - 1) * limit;
const data = resources.slice(start, start + limit);
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
data,
meta: {
page: pageNumber,
limit,
total: resources.length,
totalPages,
},
aggregations: { total: 1_250, byType: typeCounts },
}),
});
});
await ensureAuthenticated(page);
await gotoWithBackendRetry(page, '/proxmox/overview');
await waitForProxmoxReady(page);
await expect(page.locator('tr[data-guest-id]').first()).toBeVisible();
const mountedGuestRows = await page.locator('tr[data-guest-id]').count();
expect(mountedGuestRows).toBeGreaterThan(0);
expect(mountedGuestRows).toBeLessThanOrEqual(140);
// Warm both retained sections before measuring the operator's steady-state
// workflow. The synthetic estate matches the demo performance profile but
// keeps this browser gate independent of mutable backend fixture settings.
await measureProxmoxSectionTransition(page, 'Storage');
await measureProxmoxSectionTransition(page, 'Overview');
const overviewToStorageSamples: number[] = [];
const storageToOverviewSamples: number[] = [];
for (let index = 0; index < iterations; index++) {
overviewToStorageSamples.push(
await measureProxmoxSectionTransition(page, 'Storage'),
);
storageToOverviewSamples.push(
await measureProxmoxSectionTransition(page, 'Overview'),
);
}
const overviewToStorageMedianMs = median(overviewToStorageSamples);
const storageToOverviewMedianMs = median(storageToOverviewSamples);
console.log(
`[perf] proxmox overview->storage samples=${overviewToStorageSamples.join(',')} median=${overviewToStorageMedianMs}ms budget=${overviewToStorageBudgetMs}ms`,
);
console.log(
`[perf] proxmox storage->overview samples=${storageToOverviewSamples.join(',')} median=${storageToOverviewMedianMs}ms budget=${storageToOverviewBudgetMs}ms`,
);
expect(overviewToStorageMedianMs).toBeLessThanOrEqual(
overviewToStorageBudgetMs,
);
expect(storageToOverviewMedianMs).toBeLessThanOrEqual(
storageToOverviewBudgetMs,
);
});
});