mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
fix(perf): preflight profile artifact capacity
This commit is contained in:
1 parent
02b966895d
commit
5533ac0fc1
3 files changed
+204
-1
No files matched your search
@@ -35,6 +35,7 @@ import {
|
||||
createCancellationBenchmarkSummary,
|
||||
createCancellationIterationResult,
|
||||
} from './m3u-refresh-cancellation-report';
|
||||
import { assertPerformanceArtifactCapacity } from './performance-artifact-preflight';
|
||||
import {
|
||||
installMainCapture,
|
||||
readMainCaptureStatus,
|
||||
@@ -147,10 +148,11 @@ async function runIteration(
|
||||
server: SyntheticServer
|
||||
): Promise<CancellationIterationResult> {
|
||||
const iterationDirectory = join(config.outputDirectory, definition.runId);
|
||||
await assertPerformanceArtifactCapacity(iterationDirectory);
|
||||
await mkdir(iterationDirectory, { recursive: false });
|
||||
const dataDirectory = await mkdtemp(
|
||||
join(tmpdir(), 'iptvnator-m3u-performance-')
|
||||
);
|
||||
await mkdir(iterationDirectory, { recursive: false });
|
||||
let app: LaunchedElectronApp | null = null;
|
||||
let rendererConsoleListener: ((message: ConsoleMessage) => void) | null =
|
||||
null;
|
||||
@@ -363,6 +365,7 @@ async function resolveConfiguration(): Promise<BenchmarkConfiguration> {
|
||||
}
|
||||
const outputDirectory = join(outputRoot, variant);
|
||||
await assertMissing(outputDirectory);
|
||||
await assertPerformanceArtifactCapacity(outputDirectory);
|
||||
await mkdir(outputDirectory, { recursive: true });
|
||||
return Object.freeze({
|
||||
electronVersion: electronPackage.version,
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
/* eslint-disable playwright/expect-expect -- These are Node assertion-based performance contract tests. */
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import test from 'node:test';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
|
||||
import {
|
||||
assertPerformanceArtifactCapacity,
|
||||
MIN_PERFORMANCE_ARTIFACT_FREE_BYTES,
|
||||
} from './performance-artifact-preflight';
|
||||
|
||||
test('checks the nearest existing ancestor using available blocks', async () => {
|
||||
const requestedPath = resolve(
|
||||
'/workspace/dist/performance/2026-07-26/baseline'
|
||||
);
|
||||
const checkedPaths: string[] = [];
|
||||
const existingAncestor = dirname(dirname(requestedPath));
|
||||
|
||||
const result = await assertPerformanceArtifactCapacity(requestedPath, {
|
||||
statfs: async (path) => {
|
||||
checkedPaths.push(path);
|
||||
if (path !== existingAncestor) {
|
||||
throw Object.assign(new Error('missing'), { code: 'ENOENT' });
|
||||
}
|
||||
return {
|
||||
bavail: 4_194_304n,
|
||||
bsize: 1_024n,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(checkedPaths, [
|
||||
requestedPath,
|
||||
dirname(requestedPath),
|
||||
existingAncestor,
|
||||
]);
|
||||
assert.deepEqual(result, {
|
||||
availableBytes: 4_294_967_296n,
|
||||
checkedPath: existingAncestor,
|
||||
minimumBytes: MIN_PERFORMANCE_ARTIFACT_FREE_BYTES,
|
||||
});
|
||||
});
|
||||
|
||||
test('accepts exactly two GiB of available capacity', async () => {
|
||||
const result = await assertPerformanceArtifactCapacity('/existing', {
|
||||
statfs: async () => ({
|
||||
bavail: 2_097_152n,
|
||||
bsize: 1_024n,
|
||||
}),
|
||||
});
|
||||
|
||||
assert.equal(result.availableBytes, MIN_PERFORMANCE_ARTIFACT_FREE_BYTES);
|
||||
});
|
||||
|
||||
test('fails before artifact creation when available capacity is below two GiB', async () => {
|
||||
await assert.rejects(
|
||||
assertPerformanceArtifactCapacity('/existing', {
|
||||
statfs: async () => ({
|
||||
bavail: 2_097_151n,
|
||||
bsize: 1_024n,
|
||||
}),
|
||||
}),
|
||||
/requires at least 2147483648 available bytes.*2147482624/
|
||||
);
|
||||
});
|
||||
|
||||
test('does not substitute free blocks that are unavailable to the process', async () => {
|
||||
await assert.rejects(
|
||||
assertPerformanceArtifactCapacity('/existing', {
|
||||
statfs: async () => ({
|
||||
bavail: 1n,
|
||||
bfree: 9_999_999_999n,
|
||||
bsize: 1_024n,
|
||||
}),
|
||||
}),
|
||||
/performance-artifact-space-insufficient/
|
||||
);
|
||||
});
|
||||
|
||||
test('does not hide filesystem errors other than a missing path', async () => {
|
||||
const denied = Object.assign(new Error('denied'), { code: 'EACCES' });
|
||||
let calls = 0;
|
||||
|
||||
await assert.rejects(
|
||||
assertPerformanceArtifactCapacity('/denied/child', {
|
||||
statfs: async () => {
|
||||
calls += 1;
|
||||
throw denied;
|
||||
},
|
||||
}),
|
||||
denied
|
||||
);
|
||||
assert.equal(calls, 1);
|
||||
});
|
||||
|
||||
test('the benchmark preflights capacity before creating output directories', () => {
|
||||
const source = readFileSync(
|
||||
new URL('./m3u-refresh-cancellation.benchmark.ts', import.meta.url),
|
||||
'utf8'
|
||||
);
|
||||
const runIteration = source.slice(
|
||||
source.indexOf('async function runIteration('),
|
||||
source.indexOf('async function seedPlaylist(')
|
||||
);
|
||||
const resolveConfiguration = source.slice(
|
||||
source.indexOf('async function resolveConfiguration('),
|
||||
source.indexOf('function readGitSourceState(')
|
||||
);
|
||||
|
||||
for (const [name, section] of [
|
||||
['iteration', runIteration],
|
||||
['variant', resolveConfiguration],
|
||||
] as const) {
|
||||
const preflight = section.indexOf(
|
||||
'await assertPerformanceArtifactCapacity('
|
||||
);
|
||||
const createDirectory = section.indexOf('await mkdir(');
|
||||
assert.ok(preflight >= 0, `${name} preflight is missing`);
|
||||
assert.ok(
|
||||
preflight < createDirectory,
|
||||
`${name} preflight must run before mkdir`
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
import { statfs } from 'node:fs/promises';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
|
||||
export const MIN_PERFORMANCE_ARTIFACT_FREE_BYTES =
|
||||
2n * 1_024n * 1_024n * 1_024n;
|
||||
|
||||
interface FileSystemCapacity {
|
||||
readonly bavail: bigint;
|
||||
readonly bsize: bigint;
|
||||
}
|
||||
|
||||
interface PerformanceArtifactPreflightDependencies {
|
||||
readonly statfs?: (path: string) => Promise<FileSystemCapacity>;
|
||||
}
|
||||
|
||||
export interface PerformanceArtifactCapacity {
|
||||
readonly availableBytes: bigint;
|
||||
readonly checkedPath: string;
|
||||
readonly minimumBytes: bigint;
|
||||
}
|
||||
|
||||
async function readFileSystemCapacity(
|
||||
path: string
|
||||
): Promise<FileSystemCapacity> {
|
||||
const capacity = await statfs(path, { bigint: true });
|
||||
return {
|
||||
bavail: capacity.bavail,
|
||||
bsize: capacity.bsize,
|
||||
};
|
||||
}
|
||||
|
||||
function isMissingPath(error: unknown): boolean {
|
||||
return (
|
||||
typeof error === 'object' &&
|
||||
error !== null &&
|
||||
'code' in error &&
|
||||
error.code === 'ENOENT'
|
||||
);
|
||||
}
|
||||
|
||||
export async function assertPerformanceArtifactCapacity(
|
||||
targetPath: string,
|
||||
dependencies: PerformanceArtifactPreflightDependencies = {}
|
||||
): Promise<PerformanceArtifactCapacity> {
|
||||
const readCapacity = dependencies.statfs ?? readFileSystemCapacity;
|
||||
let checkedPath = resolve(targetPath);
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
const capacity = await readCapacity(checkedPath);
|
||||
const availableBytes = capacity.bavail * capacity.bsize;
|
||||
if (availableBytes < MIN_PERFORMANCE_ARTIFACT_FREE_BYTES) {
|
||||
throw new Error(
|
||||
`performance-artifact-space-insufficient: benchmark requires at least ${MIN_PERFORMANCE_ARTIFACT_FREE_BYTES} available bytes; ${availableBytes} available at ${checkedPath}`
|
||||
);
|
||||
}
|
||||
return Object.freeze({
|
||||
availableBytes,
|
||||
checkedPath,
|
||||
minimumBytes: MIN_PERFORMANCE_ARTIFACT_FREE_BYTES,
|
||||
});
|
||||
} catch (error) {
|
||||
if (!isMissingPath(error)) {
|
||||
throw error;
|
||||
}
|
||||
const parent = dirname(checkedPath);
|
||||
if (parent === checkedPath) {
|
||||
throw new Error(
|
||||
`performance-artifact-space-unavailable: no existing ancestor for ${targetPath}`,
|
||||
{ cause: error }
|
||||
);
|
||||
}
|
||||
checkedPath = parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user