Files
homarr/scripts/benchmarks/request-handler-memory.mts

108 lines
4.0 KiB
TypeScript

import { createIntegrationRequestHandler } from "../../packages/request-handler/src/lib/integration-request-handler.ts";
import { createRequestHandler } from "../../packages/request-handler/src/lib/request-handler.ts";
const allocations = Number(process.env.REQUEST_MEMORY_ALLOCATIONS ?? 64);
const allocationBytes = Number(process.env.REQUEST_MEMORY_ALLOCATION_BYTES ?? 1024 * 1024);
const maxRetainedBytes = Number(process.env.REQUEST_MEMORY_MAX_RETAINED_BYTES ?? 16 * 1024 * 1024);
const positiveTtlMs = Number(process.env.REQUEST_MEMORY_POSITIVE_TTL_MS ?? 25);
if (!globalThis.gc) throw new Error("Run this benchmark with node --expose-gc");
if (!Number.isInteger(allocations) || allocations < 2) throw new Error("REQUEST_MEMORY_ALLOCATIONS must be >= 2");
if (!Number.isInteger(allocationBytes) || allocationBytes < 1) {
throw new Error("REQUEST_MEMORY_ALLOCATION_BYTES must be positive");
}
const collectGarbage = async () => {
for (let index = 0; index < 4; index++) {
globalThis.gc?.();
await new Promise((resolve) => setImmediate(resolve));
}
};
const retainedRoots: unknown[] = [];
const measureArrayBufferGrowthAsync = async (workload: () => Promise<unknown>) => {
await collectGarbage();
const before = process.memoryUsage().arrayBuffers;
const retainedRoot = await workload();
retainedRoots.push(retainedRoot);
await collectGarbage();
const growth = process.memoryUsage().arrayBuffers - before;
retainedRoots.pop();
return growth;
};
const zeroTtlGrowthBytes = await measureArrayBufferGrowthAsync(async () => {
const handler = createRequestHandler<Uint8Array, { id: number }>({
cacheTtlMs: 0,
requestAsync: async () => new Uint8Array(allocationBytes),
});
for (let id = 0; id < allocations; id++) {
await handler.handler({ id }).getDataAsync();
}
return handler;
});
const integrationGrowthBytes = await measureArrayBufferGrowthAsync(async () => {
const handler = createIntegrationRequestHandler<number, "mock", Record<string, never>>({
cacheTtlMs: 0,
requestAsync: async (integration) =>
(integration as typeof integration & { benchmarkPayload: Uint8Array }).benchmarkPayload.byteLength,
});
for (let id = 0; id < allocations; id++) {
await handler
.handler(
{
id: `benchmark-${id}`,
kind: "mock",
name: "benchmark",
url: "http://benchmark.invalid",
externalUrl: null,
decryptedSecrets: [],
benchmarkPayload: new Uint8Array(allocationBytes),
} as never,
{},
)
.getDataAsync();
}
return handler;
});
const expiredPositiveTtlGrowthBytes = await measureArrayBufferGrowthAsync(async () => {
const handler = createRequestHandler<Uint8Array, { id: number }>({
cacheTtlMs: positiveTtlMs,
requestAsync: async () => new Uint8Array(allocationBytes),
});
for (let id = 0; id < allocations; id++) {
await handler.handler({ id }).getDataAsync();
}
await new Promise((resolve) => setTimeout(resolve, positiveTtlMs + 25));
return handler;
});
const positiveControlGrowthBytes = await measureArrayBufferGrowthAsync(async () =>
Array.from({ length: allocations }, () => new Uint8Array(allocationBytes)),
);
const positiveControlMinimumBytes = allocations * allocationBytes * 0.8;
const result = {
allocations,
allocationBytes,
allocatedPerScenarioBytes: allocations * allocationBytes,
maxRetainedBytes,
positiveTtlMs,
zeroTtlGrowthBytes,
integrationGrowthBytes,
expiredPositiveTtlGrowthBytes,
positiveControlGrowthBytes,
positiveControlMinimumBytes,
zeroTtlPass: zeroTtlGrowthBytes <= maxRetainedBytes,
integrationPass: integrationGrowthBytes <= maxRetainedBytes,
expiredPositiveTtlPass: expiredPositiveTtlGrowthBytes <= maxRetainedBytes,
positiveControlPass: positiveControlGrowthBytes >= positiveControlMinimumBytes,
};
console.log(JSON.stringify(result, null, 2));
if (!result.zeroTtlPass || !result.integrationPass || !result.expiredPositiveTtlPass || !result.positiveControlPass) {
process.exitCode = 1;
}