feat: publish Regex Tools 0.2.0
This commit is contained in:
819
src/regex/analysis/analysis-runner.ts
Normal file
819
src/regex/analysis/analysis-runner.ts
Normal file
@@ -0,0 +1,819 @@
|
||||
import { WorkerRequestError } from "../execution/WorkerSupervisor";
|
||||
import {
|
||||
DEFAULT_REGEX_LIMITS,
|
||||
utf8ByteLength,
|
||||
} from "../execution/request-limits";
|
||||
import { ANALYSIS_LIMITS } from "./analysis-limits";
|
||||
import type { AnalysisWorkerClient } from "./AnalysisSupervisor";
|
||||
import type {
|
||||
AnalysisProgress,
|
||||
AnalysisRunOptions,
|
||||
AnalysisRunStatus,
|
||||
BenchmarkWorkerSample,
|
||||
GrowthAnalysisRequest,
|
||||
GrowthAnalysisResult,
|
||||
GrowthSample,
|
||||
GrowthStopReason,
|
||||
RegexBenchmarkRequest,
|
||||
RegexBenchmarkResult,
|
||||
RegexRiskFinding,
|
||||
} from "./analysis.types";
|
||||
import { summarizeBenchmarkSamples } from "./statistics";
|
||||
|
||||
interface Clock {
|
||||
now(): number;
|
||||
}
|
||||
|
||||
const SYSTEM_CLOCK: Clock = {
|
||||
now: () => performance.now(),
|
||||
};
|
||||
|
||||
function assertInteger(
|
||||
value: number,
|
||||
label: string,
|
||||
minimum: number,
|
||||
maximum: number,
|
||||
): void {
|
||||
if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
|
||||
throw new RangeError(
|
||||
`${label} must be an integer from ${minimum.toLocaleString()} to ${maximum.toLocaleString()}.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function assertFiniteRange(
|
||||
value: number,
|
||||
label: string,
|
||||
minimum: number,
|
||||
maximum: number,
|
||||
): void {
|
||||
if (!Number.isFinite(value) || value < minimum || value > maximum) {
|
||||
throw new RangeError(
|
||||
`${label} must be from ${minimum.toLocaleString()} to ${maximum.toLocaleString()}.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function validateBenchmarkRequest(request: RegexBenchmarkRequest): void {
|
||||
if (request.flavour !== "ecmascript") {
|
||||
throw new RangeError("Benchmarking currently supports ECMAScript only.");
|
||||
}
|
||||
if (request.pattern.length > DEFAULT_REGEX_LIMITS.patternHardLengthUtf16) {
|
||||
throw new RangeError("Pattern exceeds the configured hard limit.");
|
||||
}
|
||||
if (
|
||||
utf8ByteLength(request.subject) >
|
||||
DEFAULT_REGEX_LIMITS.interactiveSubjectHardBytes
|
||||
) {
|
||||
throw new RangeError(
|
||||
"Benchmark subject exceeds the configured hard limit.",
|
||||
);
|
||||
}
|
||||
if (
|
||||
request.replacement.length >
|
||||
DEFAULT_REGEX_LIMITS.maximumReplacementTemplateUtf16
|
||||
) {
|
||||
throw new RangeError(
|
||||
"Benchmark replacement exceeds the configured hard limit.",
|
||||
);
|
||||
}
|
||||
assertInteger(
|
||||
request.maximumMatches,
|
||||
"Maximum matches",
|
||||
1,
|
||||
DEFAULT_REGEX_LIMITS.maximumMatches,
|
||||
);
|
||||
assertInteger(
|
||||
request.settings.warmupIterations,
|
||||
"Warm-up iterations",
|
||||
0,
|
||||
ANALYSIS_LIMITS.maximumWarmupIterations,
|
||||
);
|
||||
assertInteger(
|
||||
request.settings.measuredIterations,
|
||||
"Measured iterations",
|
||||
1,
|
||||
ANALYSIS_LIMITS.maximumMeasuredIterations,
|
||||
);
|
||||
assertInteger(
|
||||
request.settings.sampleTimeoutMs,
|
||||
"Sample timeout",
|
||||
ANALYSIS_LIMITS.minimumSampleTimeoutMs,
|
||||
ANALYSIS_LIMITS.maximumSampleTimeoutMs,
|
||||
);
|
||||
assertInteger(
|
||||
request.settings.maximumWallTimeMs,
|
||||
"Benchmark wall time",
|
||||
request.settings.sampleTimeoutMs,
|
||||
ANALYSIS_LIMITS.maximumAnalysisWallTimeMs,
|
||||
);
|
||||
if (
|
||||
request.settings.warmupIterations +
|
||||
request.settings.measuredIterations +
|
||||
1 >
|
||||
DEFAULT_REGEX_LIMITS.maximumBenchmarkIterations
|
||||
) {
|
||||
throw new RangeError(
|
||||
`Benchmark exceeds the ${DEFAULT_REGEX_LIMITS.maximumBenchmarkIterations.toLocaleString()}-iteration limit.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function validateGrowthRequest(request: GrowthAnalysisRequest): void {
|
||||
if (request.flavour !== "ecmascript") {
|
||||
throw new RangeError(
|
||||
"Dynamic growth analysis currently supports ECMAScript only.",
|
||||
);
|
||||
}
|
||||
if (request.pattern.length > DEFAULT_REGEX_LIMITS.patternHardLengthUtf16) {
|
||||
throw new RangeError("Pattern exceeds the configured hard limit.");
|
||||
}
|
||||
assertInteger(
|
||||
request.maximumMatches,
|
||||
"Maximum matches",
|
||||
1,
|
||||
DEFAULT_REGEX_LIMITS.maximumMatches,
|
||||
);
|
||||
if (request.settings.repeatedFragment.length === 0) {
|
||||
throw new RangeError("Repeated fragment must not be empty.");
|
||||
}
|
||||
if (
|
||||
request.settings.repeatedFragment.length >
|
||||
ANALYSIS_LIMITS.maximumGrowthFragmentUtf16
|
||||
) {
|
||||
throw new RangeError(
|
||||
`Repeated fragment exceeds ${ANALYSIS_LIMITS.maximumGrowthFragmentUtf16.toLocaleString()} UTF-16 units.`,
|
||||
);
|
||||
}
|
||||
for (const [label, value] of [
|
||||
["Growth prefix", request.settings.prefix],
|
||||
["Growth suffix", request.settings.suffix],
|
||||
] as const) {
|
||||
if (value.length > ANALYSIS_LIMITS.maximumGrowthAffixUtf16) {
|
||||
throw new RangeError(
|
||||
`${label} exceeds ${ANALYSIS_LIMITS.maximumGrowthAffixUtf16.toLocaleString()} UTF-16 units.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
assertInteger(
|
||||
request.settings.startingRepetitions,
|
||||
"Starting repetitions",
|
||||
1,
|
||||
ANALYSIS_LIMITS.maximumGrowthRepetitions,
|
||||
);
|
||||
assertInteger(
|
||||
request.settings.maximumRepetitions,
|
||||
"Maximum repetitions",
|
||||
request.settings.startingRepetitions,
|
||||
ANALYSIS_LIMITS.maximumGrowthRepetitions,
|
||||
);
|
||||
assertFiniteRange(request.settings.multiplier, "Growth multiplier", 1.1, 10);
|
||||
assertInteger(
|
||||
request.settings.maximumSteps,
|
||||
"Maximum growth steps",
|
||||
1,
|
||||
ANALYSIS_LIMITS.maximumGrowthSteps,
|
||||
);
|
||||
assertInteger(
|
||||
request.settings.sampleTimeoutMs,
|
||||
"Sample timeout",
|
||||
ANALYSIS_LIMITS.minimumSampleTimeoutMs,
|
||||
ANALYSIS_LIMITS.maximumSampleTimeoutMs,
|
||||
);
|
||||
assertInteger(
|
||||
request.settings.maximumWallTimeMs,
|
||||
"Growth wall time",
|
||||
request.settings.sampleTimeoutMs,
|
||||
ANALYSIS_LIMITS.maximumAnalysisWallTimeMs,
|
||||
);
|
||||
assertInteger(
|
||||
request.settings.maximumSubjectBytes,
|
||||
"Maximum generated subject bytes",
|
||||
1,
|
||||
DEFAULT_REGEX_LIMITS.interactiveSubjectHardBytes,
|
||||
);
|
||||
assertFiniteRange(
|
||||
request.settings.normalizedGrowthThreshold,
|
||||
"Normalized growth threshold",
|
||||
1.1,
|
||||
100,
|
||||
);
|
||||
}
|
||||
|
||||
function errorKind(
|
||||
error: unknown,
|
||||
): "timeout" | "cancelled" | "crash" | "error" {
|
||||
if (error instanceof WorkerRequestError) {
|
||||
if (error.kind === "timeout") return "timeout";
|
||||
if (error.kind === "cancelled") return "cancelled";
|
||||
if (error.kind === "crash") return "crash";
|
||||
return "error";
|
||||
}
|
||||
if (
|
||||
error &&
|
||||
typeof error === "object" &&
|
||||
"kind" in error &&
|
||||
typeof error.kind === "string"
|
||||
) {
|
||||
if (
|
||||
error.kind === "timeout" ||
|
||||
error.kind === "cancelled" ||
|
||||
error.kind === "crash"
|
||||
) {
|
||||
return error.kind;
|
||||
}
|
||||
}
|
||||
return "error";
|
||||
}
|
||||
|
||||
function errorMessage(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
|
||||
function aborted(signal: AbortSignal | undefined): boolean {
|
||||
return signal?.aborted === true;
|
||||
}
|
||||
|
||||
function timeoutWithinDeadline(
|
||||
configuredTimeoutMs: number,
|
||||
deadline: number,
|
||||
clock: Clock,
|
||||
): number {
|
||||
return Math.max(
|
||||
1,
|
||||
Math.min(configuredTimeoutMs, Math.floor(deadline - clock.now())),
|
||||
);
|
||||
}
|
||||
|
||||
function benchmarkResult(
|
||||
request: RegexBenchmarkRequest,
|
||||
started: number,
|
||||
clock: Clock,
|
||||
values: {
|
||||
readonly status: AnalysisRunStatus;
|
||||
readonly identity?: RegexBenchmarkResult["identity"];
|
||||
readonly coldStartMs?: number;
|
||||
readonly coldSample?: BenchmarkWorkerSample;
|
||||
readonly warmSamples: readonly BenchmarkWorkerSample[];
|
||||
readonly completedWarmups: number;
|
||||
readonly stoppedReason?: string;
|
||||
readonly warnings: readonly string[];
|
||||
},
|
||||
): RegexBenchmarkResult {
|
||||
return {
|
||||
status: values.status,
|
||||
...(values.identity ? { identity: values.identity } : {}),
|
||||
...(values.coldStartMs === undefined
|
||||
? {}
|
||||
: { coldStartMs: values.coldStartMs }),
|
||||
...(values.coldSample ? { coldSample: values.coldSample } : {}),
|
||||
warmSamples: values.warmSamples,
|
||||
warmStatistics: summarizeBenchmarkSamples(values.warmSamples),
|
||||
completedWarmups: values.completedWarmups,
|
||||
completedMeasuredIterations: values.warmSamples.length,
|
||||
requestedSettings: request.settings,
|
||||
wallTimeMs: Math.max(0, clock.now() - started),
|
||||
...(values.stoppedReason ? { stoppedReason: values.stoppedReason } : {}),
|
||||
warnings: values.warnings,
|
||||
};
|
||||
}
|
||||
|
||||
function progress(options: AnalysisRunOptions, value: AnalysisProgress): void {
|
||||
options.onProgress?.(value);
|
||||
}
|
||||
|
||||
export async function runRegexBenchmark(
|
||||
request: RegexBenchmarkRequest,
|
||||
client: AnalysisWorkerClient,
|
||||
options: AnalysisRunOptions = {},
|
||||
clock: Clock = SYSTEM_CLOCK,
|
||||
): Promise<RegexBenchmarkResult> {
|
||||
validateBenchmarkRequest(request);
|
||||
const started = clock.now();
|
||||
const deadline = started + request.settings.maximumWallTimeMs;
|
||||
let identity: RegexBenchmarkResult["identity"];
|
||||
let coldStartMs: number | undefined;
|
||||
let coldSample: BenchmarkWorkerSample | undefined;
|
||||
let completedWarmups = 0;
|
||||
const warmSamples: BenchmarkWorkerSample[] = [];
|
||||
let requestWasWallLimited = false;
|
||||
const nextRequestTimeout = () => {
|
||||
requestWasWallLimited =
|
||||
deadline - clock.now() < request.settings.sampleTimeoutMs;
|
||||
return timeoutWithinDeadline(
|
||||
request.settings.sampleTimeoutMs,
|
||||
deadline,
|
||||
clock,
|
||||
);
|
||||
};
|
||||
const warnings = [
|
||||
"One browser, pattern and subject are not a complete performance characterization.",
|
||||
"Native browser and WebAssembly engine timings are not directly comparable.",
|
||||
"Displayed precision does not imply nanosecond measurement accuracy.",
|
||||
];
|
||||
|
||||
const partial = (
|
||||
status: AnalysisRunStatus,
|
||||
stoppedReason: string,
|
||||
): RegexBenchmarkResult =>
|
||||
benchmarkResult(request, started, clock, {
|
||||
status,
|
||||
identity,
|
||||
coldStartMs,
|
||||
coldSample,
|
||||
warmSamples,
|
||||
completedWarmups,
|
||||
stoppedReason,
|
||||
warnings,
|
||||
});
|
||||
|
||||
try {
|
||||
if (aborted(options.signal)) {
|
||||
return partial("cancelled", "Benchmark cancelled before it started.");
|
||||
}
|
||||
progress(options, {
|
||||
phase: "starting-worker",
|
||||
completed: 0,
|
||||
total: 1,
|
||||
message: "Starting a fresh ECMAScript analysis worker…",
|
||||
});
|
||||
const coldStart = clock.now();
|
||||
identity = await client.identity(nextRequestTimeout());
|
||||
coldStartMs = Math.max(0, clock.now() - coldStart);
|
||||
|
||||
if (clock.now() >= deadline) {
|
||||
return partial(
|
||||
"wall-time-limit",
|
||||
"Benchmark stopped at its aggregate wall-time limit.",
|
||||
);
|
||||
}
|
||||
progress(options, {
|
||||
phase: "cold-sample",
|
||||
completed: 0,
|
||||
total: 1,
|
||||
message: "Measuring the first engine use separately…",
|
||||
});
|
||||
coldSample = await client.benchmarkSample(request, nextRequestTimeout());
|
||||
if (!coldSample.accepted) {
|
||||
return partial(
|
||||
"error",
|
||||
coldSample.compileError ??
|
||||
"The native ECMAScript engine rejected the benchmark pattern.",
|
||||
);
|
||||
}
|
||||
|
||||
for (let index = 0; index < request.settings.warmupIterations; index += 1) {
|
||||
if (aborted(options.signal)) {
|
||||
return partial("cancelled", "Benchmark cancelled during warm-up.");
|
||||
}
|
||||
if (clock.now() >= deadline) {
|
||||
return partial(
|
||||
"wall-time-limit",
|
||||
"Benchmark stopped at its aggregate wall-time limit.",
|
||||
);
|
||||
}
|
||||
progress(options, {
|
||||
phase: "warming-up",
|
||||
completed: index,
|
||||
total: request.settings.warmupIterations,
|
||||
message: `Warm-up ${index + 1} of ${request.settings.warmupIterations}…`,
|
||||
});
|
||||
const sample = await client.benchmarkSample(
|
||||
request,
|
||||
nextRequestTimeout(),
|
||||
);
|
||||
if (!sample.accepted) {
|
||||
return partial(
|
||||
"error",
|
||||
sample.compileError ??
|
||||
"The native ECMAScript engine rejected a warm-up sample.",
|
||||
);
|
||||
}
|
||||
completedWarmups += 1;
|
||||
}
|
||||
|
||||
for (
|
||||
let index = 0;
|
||||
index < request.settings.measuredIterations;
|
||||
index += 1
|
||||
) {
|
||||
if (aborted(options.signal)) {
|
||||
return partial("cancelled", "Benchmark cancelled while measuring.");
|
||||
}
|
||||
if (clock.now() >= deadline) {
|
||||
return partial(
|
||||
"wall-time-limit",
|
||||
"Benchmark stopped at its aggregate wall-time limit.",
|
||||
);
|
||||
}
|
||||
progress(options, {
|
||||
phase: "measuring",
|
||||
completed: index,
|
||||
total: request.settings.measuredIterations,
|
||||
message: `Measured sample ${index + 1} of ${request.settings.measuredIterations}…`,
|
||||
});
|
||||
const sample = await client.benchmarkSample(
|
||||
request,
|
||||
nextRequestTimeout(),
|
||||
);
|
||||
if (!sample.accepted) {
|
||||
return partial(
|
||||
"error",
|
||||
sample.compileError ??
|
||||
"The native ECMAScript engine rejected a measured sample.",
|
||||
);
|
||||
}
|
||||
warmSamples.push(sample);
|
||||
}
|
||||
} catch (error) {
|
||||
const kind = errorKind(error);
|
||||
if (kind === "timeout") {
|
||||
if (requestWasWallLimited || clock.now() >= deadline) {
|
||||
return partial(
|
||||
"wall-time-limit",
|
||||
"Benchmark stopped at its aggregate wall-time limit.",
|
||||
);
|
||||
}
|
||||
return partial(
|
||||
"timeout",
|
||||
"A benchmark sample timed out; its worker was terminated.",
|
||||
);
|
||||
}
|
||||
if (kind === "cancelled" || aborted(options.signal)) {
|
||||
return partial(
|
||||
"cancelled",
|
||||
"Benchmark cancelled; its worker was terminated.",
|
||||
);
|
||||
}
|
||||
return partial("error", `Benchmark worker failed: ${errorMessage(error)}`);
|
||||
}
|
||||
|
||||
for (const sample of [coldSample, ...warmSamples]) {
|
||||
if (sample?.matchCollectionTruncated) {
|
||||
warnings.push(
|
||||
`Match collection reached the ${request.maximumMatches.toLocaleString()}-match benchmark limit.`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
const skippedReplacement = [coldSample, ...warmSamples].find(
|
||||
(sample) => sample?.replacementSkippedReason,
|
||||
)?.replacementSkippedReason;
|
||||
if (skippedReplacement) {
|
||||
warnings.push(`Replacement timing skipped: ${skippedReplacement}`);
|
||||
}
|
||||
return benchmarkResult(request, started, clock, {
|
||||
status: "complete",
|
||||
identity,
|
||||
coldStartMs,
|
||||
coldSample,
|
||||
warmSamples,
|
||||
completedWarmups,
|
||||
warnings,
|
||||
});
|
||||
}
|
||||
|
||||
function generatedSubjectSize(
|
||||
request: GrowthAnalysisRequest,
|
||||
repetitions: number,
|
||||
): { readonly utf16: number; readonly bytes: number } {
|
||||
return {
|
||||
utf16:
|
||||
request.settings.prefix.length +
|
||||
request.settings.repeatedFragment.length * repetitions +
|
||||
request.settings.suffix.length,
|
||||
bytes:
|
||||
utf8ByteLength(request.settings.prefix) +
|
||||
utf8ByteLength(request.settings.repeatedFragment) * repetitions +
|
||||
utf8ByteLength(request.settings.suffix),
|
||||
};
|
||||
}
|
||||
|
||||
function dynamicFinding(
|
||||
request: GrowthAnalysisRequest,
|
||||
rule: "observed-growth" | "observed-timeout",
|
||||
details: {
|
||||
readonly confidence: "medium" | "high";
|
||||
readonly severity: "warning" | "high";
|
||||
readonly explanation: string;
|
||||
readonly exampleRisk: string;
|
||||
},
|
||||
): RegexRiskFinding {
|
||||
return {
|
||||
id: `risk-${rule}-0-${request.pattern.length}`,
|
||||
flavour: "ecmascript",
|
||||
range: { startUtf16: 0, endUtf16: request.pattern.length },
|
||||
rule,
|
||||
title:
|
||||
rule === "observed-timeout"
|
||||
? "Observed timeout under selected limits"
|
||||
: "Observed disproportionate growth",
|
||||
explanation: details.explanation,
|
||||
exampleRisk: details.exampleRisk,
|
||||
confidence: details.confidence,
|
||||
severity: details.severity,
|
||||
limitations:
|
||||
"This observation applies only to the generated subjects, browser runtime and limits shown in this result.",
|
||||
suggestedInvestigation:
|
||||
"Repeat with representative production boundaries, retain strict worker limits, and simplify ambiguous repetition where possible.",
|
||||
evidence: "dynamically-observed",
|
||||
};
|
||||
}
|
||||
|
||||
function growthResult(
|
||||
request: GrowthAnalysisRequest,
|
||||
started: number,
|
||||
clock: Clock,
|
||||
values: {
|
||||
readonly status: AnalysisRunStatus;
|
||||
readonly identity?: GrowthAnalysisResult["identity"];
|
||||
readonly samples: readonly GrowthSample[];
|
||||
readonly stopReason: GrowthStopReason;
|
||||
readonly stoppedReason: string;
|
||||
readonly dynamicFindings: readonly RegexRiskFinding[];
|
||||
},
|
||||
): GrowthAnalysisResult {
|
||||
return {
|
||||
status: values.status,
|
||||
...(values.identity ? { identity: values.identity } : {}),
|
||||
samples: values.samples,
|
||||
stopReason: values.stopReason,
|
||||
stoppedReason: values.stoppedReason,
|
||||
wallTimeMs: Math.max(0, clock.now() - started),
|
||||
dynamicFindings: values.dynamicFindings,
|
||||
settings: request.settings,
|
||||
};
|
||||
}
|
||||
|
||||
export async function runGrowthAnalysis(
|
||||
request: GrowthAnalysisRequest,
|
||||
client: AnalysisWorkerClient,
|
||||
options: AnalysisRunOptions = {},
|
||||
clock: Clock = SYSTEM_CLOCK,
|
||||
): Promise<GrowthAnalysisResult> {
|
||||
validateGrowthRequest(request);
|
||||
const started = clock.now();
|
||||
const deadline = started + request.settings.maximumWallTimeMs;
|
||||
const samples: GrowthSample[] = [];
|
||||
const dynamicFindings: RegexRiskFinding[] = [];
|
||||
let identity: GrowthAnalysisResult["identity"];
|
||||
let currentRepetitions = request.settings.startingRepetitions;
|
||||
let requestWasWallLimited = false;
|
||||
const nextRequestTimeout = () => {
|
||||
requestWasWallLimited =
|
||||
deadline - clock.now() < request.settings.sampleTimeoutMs;
|
||||
return timeoutWithinDeadline(
|
||||
request.settings.sampleTimeoutMs,
|
||||
deadline,
|
||||
clock,
|
||||
);
|
||||
};
|
||||
|
||||
const finish = (
|
||||
status: AnalysisRunStatus,
|
||||
stopReason: GrowthStopReason,
|
||||
stoppedReason: string,
|
||||
) =>
|
||||
growthResult(request, started, clock, {
|
||||
status,
|
||||
identity,
|
||||
samples,
|
||||
stopReason,
|
||||
stoppedReason,
|
||||
dynamicFindings,
|
||||
});
|
||||
|
||||
try {
|
||||
if (aborted(options.signal)) {
|
||||
return finish(
|
||||
"cancelled",
|
||||
"cancelled",
|
||||
"Growth analysis cancelled before it started.",
|
||||
);
|
||||
}
|
||||
identity = await client.identity(nextRequestTimeout());
|
||||
|
||||
while (samples.length < request.settings.maximumSteps) {
|
||||
if (aborted(options.signal)) {
|
||||
return finish(
|
||||
"cancelled",
|
||||
"cancelled",
|
||||
"Growth analysis cancelled; its worker was terminated.",
|
||||
);
|
||||
}
|
||||
if (clock.now() >= deadline) {
|
||||
return finish(
|
||||
"wall-time-limit",
|
||||
"wall-time-limit",
|
||||
"Growth analysis stopped at its aggregate wall-time limit.",
|
||||
);
|
||||
}
|
||||
if (currentRepetitions > request.settings.maximumRepetitions) {
|
||||
return finish(
|
||||
"complete",
|
||||
"maximum-repetitions",
|
||||
"Growth analysis reached the configured repetition bound.",
|
||||
);
|
||||
}
|
||||
const size = generatedSubjectSize(request, currentRepetitions);
|
||||
if (
|
||||
!Number.isSafeInteger(size.bytes) ||
|
||||
!Number.isSafeInteger(size.utf16) ||
|
||||
size.bytes > request.settings.maximumSubjectBytes
|
||||
) {
|
||||
return finish(
|
||||
"complete",
|
||||
"maximum-subject-size",
|
||||
"Growth analysis stopped before exceeding the generated-subject byte limit.",
|
||||
);
|
||||
}
|
||||
|
||||
progress(options, {
|
||||
phase: "growth-probe",
|
||||
completed: samples.length,
|
||||
total: request.settings.maximumSteps,
|
||||
message: `Probing ${currentRepetitions.toLocaleString()} fragment repetitions…`,
|
||||
});
|
||||
const subject =
|
||||
request.settings.prefix +
|
||||
request.settings.repeatedFragment.repeat(currentRepetitions) +
|
||||
request.settings.suffix;
|
||||
let workerSample;
|
||||
try {
|
||||
workerSample = await client.growthProbe(
|
||||
{
|
||||
flavour: "ecmascript",
|
||||
pattern: request.pattern,
|
||||
flags: request.flags,
|
||||
subject,
|
||||
scanAll: request.scanAll,
|
||||
maximumMatches: request.maximumMatches,
|
||||
},
|
||||
nextRequestTimeout(),
|
||||
);
|
||||
} catch (error) {
|
||||
const kind = errorKind(error);
|
||||
if (kind === "timeout") {
|
||||
if (requestWasWallLimited || clock.now() >= deadline) {
|
||||
return finish(
|
||||
"wall-time-limit",
|
||||
"wall-time-limit",
|
||||
"Growth analysis stopped at its aggregate wall-time limit.",
|
||||
);
|
||||
}
|
||||
samples.push({
|
||||
repetitions: currentRepetitions,
|
||||
inputUtf16: size.utf16,
|
||||
inputBytes: size.bytes,
|
||||
status: "timeout",
|
||||
message: "Worker timed out and was terminated.",
|
||||
});
|
||||
dynamicFindings.push(
|
||||
dynamicFinding(request, "observed-timeout", {
|
||||
confidence: "high",
|
||||
severity: "high",
|
||||
explanation: `Execution did not finish within ${request.settings.sampleTimeoutMs.toLocaleString()} ms at ${currentRepetitions.toLocaleString()} generated repetitions.`,
|
||||
exampleRisk:
|
||||
"The selected generated near-miss exhausted the configured per-sample wall time.",
|
||||
}),
|
||||
);
|
||||
return finish(
|
||||
"timeout",
|
||||
"timeout",
|
||||
"Observed timeout under selected limits; the worker was terminated.",
|
||||
);
|
||||
}
|
||||
if (kind === "cancelled" || aborted(options.signal)) {
|
||||
return finish(
|
||||
"cancelled",
|
||||
"cancelled",
|
||||
"Growth analysis cancelled; its worker was terminated.",
|
||||
);
|
||||
}
|
||||
samples.push({
|
||||
repetitions: currentRepetitions,
|
||||
inputUtf16: size.utf16,
|
||||
inputBytes: size.bytes,
|
||||
status: "crash",
|
||||
message: errorMessage(error),
|
||||
});
|
||||
return finish(
|
||||
"error",
|
||||
"crash",
|
||||
`Growth worker failed: ${errorMessage(error)}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!workerSample.accepted) {
|
||||
samples.push({
|
||||
repetitions: currentRepetitions,
|
||||
inputUtf16: size.utf16,
|
||||
inputBytes: size.bytes,
|
||||
status: "compile-error",
|
||||
message: workerSample.compileError,
|
||||
});
|
||||
return finish(
|
||||
"error",
|
||||
"compile-error",
|
||||
workerSample.compileError ??
|
||||
"The native ECMAScript engine rejected the pattern.",
|
||||
);
|
||||
}
|
||||
|
||||
const previous = samples.at(-1);
|
||||
const executionMs = workerSample.executionMs ?? 0;
|
||||
let normalizedGrowth: number | undefined;
|
||||
if (
|
||||
previous?.status === "complete" &&
|
||||
previous.executionMs !== undefined &&
|
||||
previous.executionMs >= 0.25 &&
|
||||
executionMs >= 1 &&
|
||||
previous.inputBytes > 0
|
||||
) {
|
||||
const inputRatio = size.bytes / previous.inputBytes;
|
||||
const timeRatio = executionMs / previous.executionMs;
|
||||
if (inputRatio > 0) normalizedGrowth = timeRatio / inputRatio;
|
||||
}
|
||||
samples.push({
|
||||
repetitions: currentRepetitions,
|
||||
inputUtf16: size.utf16,
|
||||
inputBytes: size.bytes,
|
||||
status: "complete",
|
||||
executionMs,
|
||||
matched: workerSample.matched,
|
||||
matchCount: workerSample.matchCount,
|
||||
matchCollectionTruncated: workerSample.matchCollectionTruncated,
|
||||
...(normalizedGrowth === undefined ? {} : { normalizedGrowth }),
|
||||
});
|
||||
|
||||
if (
|
||||
normalizedGrowth !== undefined &&
|
||||
normalizedGrowth >= request.settings.normalizedGrowthThreshold
|
||||
) {
|
||||
dynamicFindings.push(
|
||||
dynamicFinding(request, "observed-growth", {
|
||||
confidence: "medium",
|
||||
severity: "warning",
|
||||
explanation: `Observed time growth was ${normalizedGrowth.toFixed(2)}× the input growth between the last two generated samples.`,
|
||||
exampleRisk:
|
||||
"Execution time increased disproportionately for the selected generated subject family.",
|
||||
}),
|
||||
);
|
||||
return finish(
|
||||
"complete",
|
||||
"growth-threshold",
|
||||
"Growth analysis stopped at the configured normalized-growth threshold.",
|
||||
);
|
||||
}
|
||||
if (currentRepetitions >= request.settings.maximumRepetitions) {
|
||||
return finish(
|
||||
"complete",
|
||||
"maximum-repetitions",
|
||||
"Growth analysis reached the configured repetition bound.",
|
||||
);
|
||||
}
|
||||
const next = Math.min(
|
||||
request.settings.maximumRepetitions,
|
||||
Math.max(
|
||||
currentRepetitions + 1,
|
||||
Math.ceil(currentRepetitions * request.settings.multiplier),
|
||||
),
|
||||
);
|
||||
currentRepetitions = next;
|
||||
}
|
||||
} catch (error) {
|
||||
const kind = errorKind(error);
|
||||
if (kind === "timeout") {
|
||||
if (requestWasWallLimited || clock.now() >= deadline) {
|
||||
return finish(
|
||||
"wall-time-limit",
|
||||
"wall-time-limit",
|
||||
"Growth analysis stopped at its aggregate wall-time limit.",
|
||||
);
|
||||
}
|
||||
return finish(
|
||||
"timeout",
|
||||
"timeout",
|
||||
"The analysis worker timed out while starting and was terminated.",
|
||||
);
|
||||
}
|
||||
if (kind === "cancelled" || aborted(options.signal)) {
|
||||
return finish(
|
||||
"cancelled",
|
||||
"cancelled",
|
||||
"Growth analysis cancelled; its worker was terminated.",
|
||||
);
|
||||
}
|
||||
return finish(
|
||||
"error",
|
||||
"crash",
|
||||
`Growth worker failed: ${errorMessage(error)}`,
|
||||
);
|
||||
}
|
||||
|
||||
return finish(
|
||||
"complete",
|
||||
"maximum-steps",
|
||||
"Growth analysis reached the configured step limit.",
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user