Files
homarr/scripts/benchmarks/docker-runtime-lib.spec.ts

433 lines
17 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
getBrowserNetworkIsolationLaunchArgs,
getSpotlightIdleMountFailure,
getSpotlightIdleNetworkFailure,
getTrpcRequestIdentities,
getUnrecoveredWidgetDataRequests,
getTrpcProcedures,
getRuntimeClaimIneligibleReasons,
getRuntimeMetricEligibility,
hasBlockingWidgetDataAborts,
normalizeDockerArchitecture,
isRuntimeWidgetDataProcedure,
isRuntimeWidgetDataRequest,
inspectTrpcBatchStreamResponse,
parseRuntimeMemorySnapshot,
recordWidgetDataRequestOutcome,
summarizeRuntimeMemory,
} from "./docker-runtime-lib.mts";
const snapshot = (current: number, peak: number, cpuUsageUsec = 1_000) => `memory_current=${current}
memory_peak=${peak}
cgroup_anon=100
cgroup_file=20
cpu_usage_usec=${cpuUsageUsec}
cpu_user_usec=${cpuUsageUsec - 300}
cpu_system_usec=300
cpu_nr_throttled=2
cpu_throttled_usec=50
process=42|1|200|next-server|180
process=7|1|10|redis-server|8
redis_used_memory=30
redis_used_memory_peak=40`;
const trpcRequestUrl = (input: string) =>
`https://homarr.test/api/trpc/widget.weather.atLocation,app.byIds?batch=1&input=${input}`;
describe("Docker runtime memory parsing", () => {
it("classifies every procedure in batched tRPC requests", () => {
expect(getTrpcProcedures("https://homarr.test/api/trpc/widget.foo,queryCache.setItem?batch=1")).toEqual([
"widget.foo",
"queryCache.setItem",
]);
expect(getTrpcProcedures("https://homarr.test/api/trpc/widget.foo%2Csearch.query?batch=1")).toEqual([
"widget.foo",
"search.query",
]);
expect(getTrpcProcedures("https://homarr.test/api/health/ready")).toEqual([]);
expect(isRuntimeWidgetDataProcedure("widget.calendar.findAllEvents")).toBe(true);
expect(isRuntimeWidgetDataProcedure("app.byIds")).toBe(true);
expect(isRuntimeWidgetDataProcedure("integration.byIds")).toBe(true);
expect(isRuntimeWidgetDataProcedure("docker.getContainers")).toBe(true);
expect(isRuntimeWidgetDataProcedure("widget.beszel.getSystemStats")).toBe(true);
expect(isRuntimeWidgetDataProcedure("widget.beszel.subscribeSystemStats")).toBe(false);
expect(isRuntimeWidgetDataProcedure("widget.app.ping")).toBe(false);
expect(isRuntimeWidgetDataProcedure("board.getBoardByName")).toBe(false);
expect(isRuntimeWidgetDataRequest("GET", "widget.weather.atLocation")).toBe(true);
expect(isRuntimeWidgetDataRequest("POST", "widget.options.saveItemOptions")).toBe(false);
});
it("normalizes Node's x64 architecture to Docker's amd64 name", () => {
expect(normalizeDockerArchitecture("x64")).toBe("amd64");
expect(normalizeDockerArchitecture("arm64")).toBe("arm64");
});
it("keys batched tRPC operations by procedure and canonical serialized input", () => {
const first = encodeURIComponent(
JSON.stringify({
0: { json: { location: "Paris", units: "metric" } },
1: { json: { ids: ["app-1"] } },
}),
);
const reordered = encodeURIComponent(
JSON.stringify({
0: { json: { units: "metric", location: "Paris" } },
1: { json: { ids: ["app-1"] } },
}),
);
const different = encodeURIComponent(
JSON.stringify({
0: { json: { location: "Lyon", units: "metric" } },
1: { json: { ids: ["app-1"] } },
}),
);
const identities = getTrpcRequestIdentities(trpcRequestUrl(first));
expect(identities.map(({ procedure }) => procedure)).toEqual(["widget.weather.atLocation", "app.byIds"]);
expect(identities.map(({ batchIndex }) => batchIndex)).toEqual([0, 1]);
expect(identities).toEqual(getTrpcRequestIdentities(trpcRequestUrl(reordered)));
expect(getTrpcRequestIdentities(trpcRequestUrl(different))[0]?.identity).not.toBe(identities[0]?.identity);
expect(getTrpcRequestIdentities(trpcRequestUrl(different))[1]?.identity).toBe(identities[1]?.identity);
expect(
identities.every(
({ identity, inputHash, procedure }) =>
procedure.includes(".") && /^[a-f0-9]{64}$/.test(inputHash) && identity === `${procedure}#${inputHash}`,
),
).toBe(true);
expect(JSON.stringify(identities)).not.toContain("Paris");
expect(() =>
getTrpcRequestIdentities("https://homarr.test/api/trpc/widget.weather.atLocation?batch=1&input=%7Bmalformed"),
).toThrow("Malformed serialized tRPC request input");
});
it("requires every aborted widget-data request identity to be recovered by an exact successful request", () => {
const recoveryBalance = new Map<string, number>();
recordWidgetDataRequestOutcome(recoveryBalance, ["weather#paris", "app#1"], "aborted");
recordWidgetDataRequestOutcome(recoveryBalance, ["app#1", "weather#lyon"], "completed");
expect(getUnrecoveredWidgetDataRequests(recoveryBalance)).toEqual(["weather#paris"]);
recordWidgetDataRequestOutcome(recoveryBalance, ["weather#paris"], "completed");
expect(getUnrecoveredWidgetDataRequests(recoveryBalance)).toEqual([]);
});
it("credits an earlier successful response when a superseding refetch is aborted", () => {
const recoveryBalance = new Map<string, number>();
recordWidgetDataRequestOutcome(recoveryBalance, ["widget.weather.atLocation"], "completed");
recordWidgetDataRequestOutcome(recoveryBalance, ["widget.weather.atLocation"], "aborted");
expect(getUnrecoveredWidgetDataRequests(recoveryBalance)).toEqual([]);
});
it("allows unrecovered aborts only for the non-claim login transition", () => {
expect(hasBlockingWidgetDataAborts(true, 2)).toBe(false);
expect(hasBlockingWidgetDataAborts(false, 0)).toBe(false);
expect(hasBlockingWidgetDataAborts(false, 1)).toBe(true);
});
it("allows eager baseline Spotlight work but keeps preload-only preparation network-free", () => {
expect(getSpotlightIdleNetworkFailure("mounted", 1)).toBeNull();
expect(getSpotlightIdleNetworkFailure("preload-only", 0)).toBeNull();
expect(getSpotlightIdleNetworkFailure("preload-only", 1)).toBe(
"Idle Spotlight preload-only preparation made 1 non-widget tRPC requests",
);
expect(getSpotlightIdleMountFailure("mounted", 1)).toBeNull();
expect(getSpotlightIdleMountFailure("preload-only", 0)).toBeNull();
expect(getSpotlightIdleMountFailure("preload-only", 1)).toBe(
"Idle Spotlight preload mounted the UI before user intent",
);
});
it("fails closed on HTTP, malformed, missing, and per-operation streamed tRPC errors", async () => {
const errorShape = {
code: -32603,
data: { code: "INTERNAL_SERVER_ERROR", httpStatus: 500 },
message: "boom",
};
const head = `${JSON.stringify({ json: { 0: [[0], [null, 0, 0]] } })}\n`;
const errorBody = `${head}${JSON.stringify({ json: [0, 0, [[{ error: errorShape }]]] })}\n`;
const rejectedBody = `${head}${JSON.stringify({ json: [0, 1, errorShape] })}\n`;
const rejectedDataBody = [
head.trimEnd(),
JSON.stringify({ json: [0, 0, [[{ result: 0 }], ["result", 0, 1]]] }),
JSON.stringify({ json: [1, 0, [[{ data: 0 }], ["data", 0, 2]]] }),
JSON.stringify({ json: [2, 1, errorShape] }),
"",
].join("\n");
const successfulUserDataBody = [
head.trimEnd(),
JSON.stringify({ json: [0, 0, [[{ result: 0 }], ["result", 0, 1]]] }),
JSON.stringify({ json: [1, 0, [[{ data: 0 }], ["data", 0, 2]]] }),
JSON.stringify({ json: [2, 0, [[{ error: errorShape }]]] }),
"",
].join("\n");
const mixedBody = [
JSON.stringify({ json: { 0: [[0], [null, 0, 0]], 1: [[0], [null, 0, 1]] } }),
JSON.stringify({ json: [0, 0, [[{ result: 0 }], ["result", 0, 2]]] }),
JSON.stringify({ json: [1, 0, [[{ error: errorShape }]]] }),
JSON.stringify({ json: [2, 0, [[{ data: "ok" }]]] }),
"",
].join("\n");
await expect(inspectTrpcBatchStreamResponse(500, errorBody, 2)).resolves.toEqual({
failures: [
{ batchIndex: 0, kind: "http-status", status: 500 },
{ batchIndex: 1, kind: "http-status", status: 500 },
],
successfulIndexes: [],
});
for (const body of ["", "not-json\n", "{}\n", head.trimEnd()]) {
await expect(inspectTrpcBatchStreamResponse(200, body, 1)).resolves.toEqual({
failures: [{ batchIndex: 0, kind: "invalid-jsonl" }],
successfulIndexes: [],
});
}
await expect(inspectTrpcBatchStreamResponse(200, head, 2)).resolves.toEqual({
failures: [
{ batchIndex: 0, kind: "index-mismatch" },
{ batchIndex: 1, kind: "index-mismatch" },
],
successfulIndexes: [],
});
await expect(inspectTrpcBatchStreamResponse(200, errorBody, 1)).resolves.toEqual({
failures: [
{
batchIndex: 0,
httpStatus: 500,
jsonRpcCode: -32603,
kind: "trpc-error",
trpcCode: "INTERNAL_SERVER_ERROR",
},
],
successfulIndexes: [],
});
for (const body of [rejectedBody, rejectedDataBody]) {
await expect(inspectTrpcBatchStreamResponse(200, body, 1)).resolves.toEqual({
failures: [{ batchIndex: 0, kind: "stream-error" }],
successfulIndexes: [],
});
}
await expect(inspectTrpcBatchStreamResponse(200, successfulUserDataBody, 1)).resolves.toEqual({
failures: [],
successfulIndexes: [0],
});
await expect(inspectTrpcBatchStreamResponse(200, mixedBody, 2)).resolves.toEqual({
failures: [
{
batchIndex: 1,
httpStatus: 500,
jsonRpcCode: -32603,
kind: "trpc-error",
trpcCode: "INTERNAL_SERVER_ERROR",
},
],
successfulIndexes: [0],
});
});
it("isolates external browser traffic without disabling Chromium's HTTP cache", () => {
expect(getBrowserNetworkIsolationLaunchArgs("http://127.0.0.1:43123")).toEqual([
"--proxy-server=http://127.0.0.1:43123",
"--proxy-bypass-list=127.0.0.1;localhost",
"--host-resolver-rules=MAP * ~NOTFOUND, EXCLUDE 127.0.0.1, EXCLUDE localhost",
]);
expect(getBrowserNetworkIsolationLaunchArgs("http://127.0.0.1:43123").join(" ")).not.toContain("disable-cache");
});
it("only permits claims with native, SHA-pinned images and enough settle samples", () => {
expect(
getRuntimeClaimIneligibleReasons({
expectedSha: "abc123",
expectedSourceFingerprint: "source-hash",
browserNetworkIsolated: true,
serverNetworkIsolated: true,
imageArchitecture: "amd64",
lcpObservationMs: 5_000,
lcpSampleCount: 20,
nodeArchitecture: "x64",
pageSampleCount: 20,
requireImplementationMarkers: true,
revision: "abc123",
sourceFingerprint: "source-hash",
settleMs: 10 * 60 * 1_000,
settleSampleCount: 20,
widgetDataQuietMs: 500,
widgetDataSampleCount: 20,
}),
).toEqual([]);
expect(
getRuntimeClaimIneligibleReasons({
expectedSha: "abc123",
expectedSourceFingerprint: "source-hash",
browserNetworkIsolated: false,
serverNetworkIsolated: false,
imageArchitecture: "arm64",
lcpObservationMs: 1_000,
lcpSampleCount: 1,
nodeArchitecture: "x64",
pageSampleCount: 1,
requireImplementationMarkers: false,
revision: null,
sourceFingerprint: null,
settleMs: 0,
settleSampleCount: 0,
widgetDataQuietMs: 100,
widgetDataSampleCount: 1,
}),
).toEqual([
"image architecture arm64 is not host amd64",
"image revision label is missing",
"image source fingerprint label is missing",
"browser external network isolation is not active",
"server external network isolation is not active",
"widget implementation markers are not required",
"page sample count 1 is below 20",
"LCP observation 1000ms is below 5000ms",
"LCP sample count 1 is below 20",
"widget data quiet window 100ms is below 500ms",
"widget data sample count 1 is below 20",
"settle duration 0ms is below 600000ms",
"settle sample count 0 is below 20",
]);
});
it("scopes repeatable, within-run, and single-sample metrics separately", () => {
const eligibility = getRuntimeMetricEligibility({
expectedSha: "abc123",
expectedSourceFingerprint: "source-hash",
browserNetworkIsolated: true,
serverNetworkIsolated: true,
imageArchitecture: "amd64",
lcpObservationMs: 5_000,
lcpSampleCount: 20,
nodeArchitecture: "x64",
pageSampleCount: 20,
requireImplementationMarkers: true,
revision: "abc123",
sourceFingerprint: "source-hash",
settleMs: 10 * 60 * 1_000,
settleSampleCount: 20,
widgetDataQuietMs: 500,
widgetDataSampleCount: 20,
});
expect(eligibility.pageLoads).toMatchObject({
claimEligible: true,
kind: "comparative",
requiredSampleCount: 20,
sampleCount: 20,
});
expect(eligibility.largestContentfulPaint).toMatchObject({
claimEligible: true,
kind: "comparative",
requiredSampleCount: 20,
sampleCount: 20,
});
expect(eligibility.widgetDataSettled).toMatchObject({
claimEligible: true,
kind: "comparative",
requiredSampleCount: 20,
sampleCount: 20,
});
expect(eligibility.settleStability).toMatchObject({
claimEligible: true,
kind: "within-run",
requiredSampleCount: 20,
sampleCount: 20,
});
expect(eligibility.settleStability.limitations).toContain("one container instance");
expect(eligibility.startup).toMatchObject({ claimEligible: false, kind: "diagnostic", sampleCount: 1 });
expect(eligibility.memoryLevels).toMatchObject({ claimEligible: false, kind: "diagnostic", sampleCount: 1 });
expect(eligibility.coldSpotlightInteraction).toMatchObject({
claimEligible: false,
kind: "diagnostic",
sampleCount: 1,
});
});
it("normalizes cgroup, process, and Redis values", () => {
expect(parseRuntimeMemorySnapshot("startup", snapshot(1_000, 2_000))).toMatchObject({
name: "startup",
container: { currentBytes: 1_000, peakBytes: 2_000, anonymousBytes: 100, fileBytes: 20 },
cpu: { usageUsec: 1_000, userUsec: 700, systemUsec: 300, nrThrottled: 2, throttledUsec: 50 },
processes: [
{ pid: 42, parentPid: 1, rssBytes: 200 * 1024, pssBytes: 180 * 1024, command: "next-server" },
{ pid: 7, parentPid: 1, rssBytes: 10 * 1024, pssBytes: 8 * 1024, command: "redis-server" },
],
redis: { usedMemoryBytes: 30, peakMemoryBytes: 40 },
});
});
it("keeps a failed Redis collection absent instead of reporting zero bytes", () => {
const withoutRedis = snapshot(1_000, 2_000)
.split("\n")
.filter((line) => !line.startsWith("redis_"))
.join("\n");
expect(parseRuntimeMemorySnapshot("startup", withoutRedis).redis).toBeNull();
});
it("reports workload-to-settled growth and the largest sample", () => {
const checkpoints = [
parseRuntimeMemorySnapshot("startup", snapshot(1_000, 2_000)),
parseRuntimeMemorySnapshot("workload", snapshot(1_100, 2_500, 1_500)),
parseRuntimeMemorySnapshot("settle-1", snapshot(1_200, 2_500, 1_800)),
];
checkpoints.forEach((checkpoint, index) => {
checkpoint.capturedAt = new Date(Date.UTC(2026, 0, 1, 0, index)).toISOString();
});
expect(summarizeRuntimeMemory(checkpoints)).toEqual({
startupBytes: 1_000,
workloadBytes: 1_100,
settledBytes: 1_200,
maximumSampleBytes: 1_200,
peakBytes: 2_500,
growthBytes: 100,
growthPercent: 100 / 11,
settle: {
count: 1,
minBytes: 1_200,
medianBytes: 1_200,
p95Bytes: 1_200,
maxBytes: 1_200,
standardDeviationBytes: 0,
trendBytesPerHour: null,
anonymous: {
count: 1,
minBytes: 100,
medianBytes: 100,
p95Bytes: 100,
maxBytes: 100,
standardDeviationBytes: 0,
trendBytesPerHour: null,
},
file: {
count: 1,
minBytes: 20,
medianBytes: 20,
p95Bytes: 20,
maxBytes: 20,
standardDeviationBytes: 0,
trendBytesPerHour: null,
},
},
cpu: {
startupToWorkload: { usageUsec: 500, userUsec: 500, systemUsec: 0, nrThrottled: 0, throttledUsec: 0 },
workloadToSettled: { usageUsec: 300, userUsec: 300, systemUsec: 0, nrThrottled: 0, throttledUsec: 0 },
},
});
});
it("derives memory trends from observed checkpoint timestamps", () => {
const first = parseRuntimeMemorySnapshot("settle-1", snapshot(1_000, 2_000));
const second = parseRuntimeMemorySnapshot("settle-2", snapshot(1_100, 2_000));
first.capturedAt = "2026-01-01T00:00:00.000Z";
second.capturedAt = "2026-01-01T02:00:00.000Z";
expect(summarizeRuntimeMemory([first, second]).settle?.trendBytesPerHour).toBe(50);
});
});