Files
paseo/packages/server/scripts/measure-git-observation.ts
Mohamed BoudraandGitHub 48e65b407f Replace Parcel Watcher with Custom Watcher (#3107)
* fix(server): replace wedging file observation

Native watcher teardown could block the daemon main thread during workspace archive. Own recursive observation behind an awaited lifecycle boundary, retain an isolated comparison harness, and gate correctness, resources, and sustained CPU across platforms.

* test(server): accept root watcher evidence

Native recursive watchers may coalesce ignored descendant changes into a root event. Assert that ignored paths stay excluded while a nonignored sentinel proves delivery remains live.

* test(server): preserve watcher comparison evidence

Record Parcel delivery misses without weakening the production observer gate, and allow the Windows stress test to use its declared polling window.

* fix(server): reconcile native watcher omissions

Back macOS and Windows recursive watching with a bounded namespace inventory so coalesced create and delete events converge without unbounded rescans.

* test(server): compare worktree filesystem identity

Windows can return the same worktree through 8.3 and long path spellings. Preserve cardinality and branch checks while comparing the real path.

* perf(server): bound native inventory audits

Reduce full native inventory cadence while preserving a six-second convergence bound, and gate setup readiness and event-loop p99 against platform-appropriate budgets.

* build: refresh Nix npm dependency hash

* perf(server): scope native file reconciliation

* perf(server): defer native safety audits until quiet

* fix(server): move native safety deadline during audits

* perf(server): audit native inventories after settled churn

* fix(server): reconcile native root evidence promptly

* perf(server): deduplicate native audit scheduling

* perf(server): coalesce native event clusters

* perf(server): suppress fresh native change scans

* test(server): separate burst recovery from sustained churn

* test(server): stabilize observer performance gates

* perf(server): skip fresh native change scans

* fix(server): preserve nested native change audits

* perf(server): coalesce native repair scans

* fix(server): reconcile native directory moves promptly

* test(server): bound observer burst repair cost

* refactor(server): own file observer lifecycle

* build: refresh Nix npm dependency hash

* ci: fold observer coverage into server suite

* fix(server): await file observer shutdown
2026-08-10 13:43:48 +02:00

425 lines
14 KiB
TypeScript

import { execFileSync } from "node:child_process";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
import type pino from "pino";
import {
getCheckoutDiff,
getCheckoutSnapshotFacts,
getCheckoutWorktreeState,
} from "../src/utils/checkout-git.js";
import {
startGitCommandMetrics,
stopGitCommandMetrics,
type GitCommandMetricsSnapshot,
} from "../src/utils/run-git-command.js";
import { CheckoutDiffManager } from "../src/server/checkout-diff-manager.js";
import { createFileObserver } from "../src/server/file-observer/index.js";
import {
WorkspaceGitServiceImpl,
type WorkspaceGitRuntimeSnapshot,
} from "../src/server/workspace-git-service.js";
import type {
FileObserverSubscription,
SubscribeToFileChanges,
} from "../src/server/file-observer/index.js";
interface ProducerCounts {
structural: number;
worktree: number;
detailedDiff: number;
}
interface PhaseResult {
elapsedMs: number;
gitCommands: number;
failedGitCommands: number;
maxConcurrentGitCommands: number;
byCommand: Record<string, number>;
producers: ProducerCounts;
queueAfter: {
inFlight: number;
queued: number;
};
}
function createLogger(): pino.Logger {
const logger = {
child: () => logger,
debug: () => {},
warn: () => {},
};
return logger as unknown as pino.Logger;
}
function runGit(cwd: string, args: string[]): void {
execFileSync("git", args, { cwd, stdio: "pipe" });
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function waitFor(predicate: () => boolean, label: string, timeoutMs = 8_000): Promise<void> {
const deadline = Date.now() + timeoutMs;
while (!predicate()) {
if (Date.now() >= deadline) {
throw new Error(`Timed out waiting for ${label}`);
}
await sleep(25);
}
}
function summarizeCommands(metrics: GitCommandMetricsSnapshot): Record<string, number> {
const counts = new Map<string, number>();
for (const command of metrics.commands) {
const key = command.args.slice(0, 3).join(" ");
counts.set(key, (counts.get(key) ?? 0) + 1);
}
return Object.fromEntries([...counts].sort(([left], [right]) => left.localeCompare(right)));
}
function subtractCounts(after: ProducerCounts, before: ProducerCounts): ProducerCounts {
return {
structural: after.structural - before.structural,
worktree: after.worktree - before.worktree,
detailedDiff: after.detailedDiff - before.detailedDiff,
};
}
function snapshotCounts(counts: ProducerCounts): ProducerCounts {
return { ...counts };
}
function createTrackedSubscriber(failWorktreeRoot: string | null) {
const observer = createFileObserver();
const subscriptions = new Set<FileObserverSubscription>();
const subscribe: SubscribeToFileChanges = async (...args) => {
if (failWorktreeRoot && path.resolve(args[0]) === failWorktreeRoot) {
throw new Error("measurement watcher setup failure");
}
const subscription = await observer.subscribe(...args);
subscriptions.add(subscription);
return {
updateIgnore: (paths) => subscription.updateIgnore(paths),
unsubscribe: async () => {
await subscription.unsubscribe();
subscriptions.delete(subscription);
},
};
};
return { observer, subscribe, subscriptions };
}
function createMeasuredService(input: {
repoDir: string;
paseoHome: string;
failWorktreeWatch?: boolean;
}) {
const counts: ProducerCounts = { structural: 0, worktree: 0, detailedDiff: 0 };
const watcher = createTrackedSubscriber(input.failWorktreeWatch ? input.repoDir : null);
const service = new WorkspaceGitServiceImpl({
logger: createLogger(),
paseoHome: input.paseoHome,
deps: {
subscribe: watcher.subscribe,
getWorkspaceGitSelfHealPhaseMs: () => 60_000,
getCheckoutSnapshotFacts: async (...args) => {
counts.structural += 1;
return getCheckoutSnapshotFacts(...args);
},
getCheckoutWorktreeState: async (...args) => {
counts.worktree += 1;
return getCheckoutWorktreeState(...args);
},
getCheckoutDiff: async (...args) => {
counts.detailedDiff += 1;
return getCheckoutDiff(...args);
},
},
});
return { counts, service, watcher };
}
async function measurePhase(input: {
service: WorkspaceGitServiceImpl;
counts: ProducerCounts;
action: () => Promise<void>;
}): Promise<PhaseResult> {
const countsBefore = snapshotCounts(input.counts);
const startedAtMs = Date.now();
startGitCommandMetrics();
let metrics: GitCommandMetricsSnapshot;
try {
await input.action();
await waitFor(() => {
const serviceMetrics = input.service.getMetrics();
return (
serviceMetrics.workspaceRefreshInFlightCount === 0 &&
serviceMetrics.workspaceRefreshQueuedCount === 0
);
}, "observation queue to drain");
} finally {
metrics = stopGitCommandMetrics();
}
const serviceMetrics = input.service.getMetrics();
return {
elapsedMs: Date.now() - startedAtMs,
gitCommands: metrics.total,
failedGitCommands: metrics.failed,
maxConcurrentGitCommands: metrics.maxConcurrent,
byCommand: summarizeCommands(metrics),
producers: subtractCounts(input.counts, countsBefore),
queueAfter: {
inFlight: serviceMetrics.workspaceRefreshInFlightCount,
queued: serviceMetrics.workspaceRefreshQueuedCount,
},
};
}
async function closeMeasuredService(input: {
service: WorkspaceGitServiceImpl;
observer: ReturnType<typeof createFileObserver>;
subscriptions: Set<FileObserverSubscription>;
}): Promise<void> {
await input.service.dispose();
await waitFor(() => input.subscriptions.size === 0, "watchers to close");
await input.observer.close();
}
async function main(): Promise<void> {
const tempDir = mkdtempSync(path.join(tmpdir(), "paseo-git-observation-measurement-"));
const repoDir = path.join(tempDir, "repo");
const paseoHome = path.join(tempDir, "paseo-home");
const trackedPath = path.join(repoDir, "tracked.txt");
const ignoredDir = path.join(repoDir, "build");
mkdirSync(repoDir, { recursive: true });
mkdirSync(ignoredDir, { recursive: true });
writeFileSync(path.join(ignoredDir, "probe.txt"), "ignored\n");
writeFileSync(path.join(repoDir, ".gitignore"), "build/\n");
writeFileSync(trackedPath, "base\n");
runGit(repoDir, ["init", "-b", "main"]);
runGit(repoDir, ["config", "user.email", "measurement@paseo.local"]);
runGit(repoDir, ["config", "user.name", "Paseo Measurement"]);
runGit(repoDir, ["add", ".gitignore", "tracked.txt"]);
runGit(repoDir, ["commit", "-m", "fixture"]);
runGit(repoDir, ["checkout", "-b", "feature"]);
const phases: Record<string, PhaseResult> = {};
let healthy: ReturnType<typeof createMeasuredService> | null = null;
let degraded: ReturnType<typeof createMeasuredService> | null = null;
let summarySubscription: { unsubscribe: () => void } | null = null;
let diffSubscription: { unsubscribe: () => void } | null = null;
let diffManager: CheckoutDiffManager | null = null;
try {
healthy = createMeasuredService({ repoDir, paseoHome });
let latestSummary: WorkspaceGitRuntimeSnapshot | null = null;
let latestDiffAdditions = 0;
startGitCommandMetrics();
const bootstrapStartedAtMs = Date.now();
const bootstrapCounts = snapshotCounts(healthy.counts);
summarySubscription = healthy.service.registerWorkspace({ cwd: repoDir }, (snapshot) => {
latestSummary = snapshot;
});
diffManager = new CheckoutDiffManager({
logger: createLogger(),
paseoHome,
workspaceGitService: healthy.service,
});
const openedDiff = await diffManager.subscribe(
{ cwd: repoDir, compare: { mode: "uncommitted" } },
(payload) => {
latestDiffAdditions = payload.files[0]?.additions ?? 0;
},
);
diffSubscription = openedDiff;
await waitFor(
() =>
healthy?.service.peekSnapshot(repoDir) !== null &&
healthy.service.getMetrics().workspaceObservationSetupInFlightCount === 0 &&
healthy.watcher.subscriptions.size === 2,
"healthy bootstrap",
);
await sleep(250);
const bootstrapMetrics = stopGitCommandMetrics();
const bootstrapServiceMetrics = healthy.service.getMetrics();
phases.bootstrap = {
elapsedMs: Date.now() - bootstrapStartedAtMs,
gitCommands: bootstrapMetrics.total,
failedGitCommands: bootstrapMetrics.failed,
maxConcurrentGitCommands: bootstrapMetrics.maxConcurrent,
byCommand: summarizeCommands(bootstrapMetrics),
producers: subtractCounts(healthy.counts, bootstrapCounts),
queueAfter: {
inFlight: bootstrapServiceMetrics.workspaceRefreshInFlightCount,
queued: bootstrapServiceMetrics.workspaceRefreshQueuedCount,
},
};
phases.legacyFullRefreshReference = await measurePhase({
service: healthy.service,
counts: healthy.counts,
action: async () => {
await Promise.all([
healthy?.service.getSnapshot(repoDir, {
force: true,
includeForge: false,
reason: "measurement-legacy-full-refresh-reference",
}),
healthy?.service.getCheckoutDiff(
repoDir,
{ mode: "uncommitted", includeStructured: true },
{ force: true, reason: "measurement-legacy-full-refresh-reference" },
),
]);
},
});
phases.idle = await measurePhase({
service: healthy.service,
counts: healthy.counts,
action: () => sleep(750),
});
phases.activeDiff = await measurePhase({
service: healthy.service,
counts: healthy.counts,
action: async () => {
writeFileSync(trackedPath, "first\nsecond\n");
await waitFor(
() => (latestSummary?.git.diffStat?.additions ?? 0) >= 2 && latestDiffAdditions >= 2,
"active diff payload",
);
await sleep(1_200);
},
});
phases.editBurst = await measurePhase({
service: healthy.service,
counts: healthy.counts,
action: async () => {
for (let index = 0; index < 100; index += 1) {
writeFileSync(
trackedPath,
Array.from({ length: index + 2 }, (_, line) => `burst-${index}-${line}`).join("\n") +
"\n",
);
}
await waitFor(
() => (latestSummary?.git.diffStat?.additions ?? 0) >= 100 && latestDiffAdditions >= 100,
"edit burst final payload",
);
await sleep(1_200);
},
});
phases.ignoredStorm = await measurePhase({
service: healthy.service,
counts: healthy.counts,
action: async () => {
for (let index = 0; index < 100; index += 1) {
writeFileSync(path.join(ignoredDir, `artifact-${index}.txt`), `${index}\n`);
}
await sleep(1_200);
},
});
if (phases.idle.gitCommands !== 0 || phases.ignoredStorm.gitCommands !== 0) {
throw new Error("Idle and ignored-storm phases must perform zero Git commands");
}
if (phases.editBurst.producers.worktree > 2 || phases.editBurst.producers.detailedDiff > 2) {
throw new Error("One hundred edits exceeded two final-state refresh generations");
}
diffSubscription.unsubscribe();
diffSubscription = null;
summarySubscription.unsubscribe();
summarySubscription = null;
diffManager.dispose();
diffManager = null;
await closeMeasuredService({
service: healthy.service,
subscriptions: healthy.watcher.subscriptions,
});
healthy = null;
degraded = createMeasuredService({ repoDir, paseoHome, failWorktreeWatch: true });
startGitCommandMetrics();
const degradedBootstrapStartedAtMs = Date.now();
const degradedBootstrapCounts = snapshotCounts(degraded.counts);
summarySubscription = degraded.service.registerWorkspace({ cwd: repoDir }, () => {});
await waitFor(
() =>
degraded?.service.peekSnapshot(repoDir) !== null &&
degraded.service.getMetrics().workspaceObservationSetupInFlightCount === 0,
"degraded bootstrap",
);
await sleep(250);
const degradedBootstrapMetrics = stopGitCommandMetrics();
const degradedBootstrapServiceMetrics = degraded.service.getMetrics();
phases.degradedBootstrap = {
elapsedMs: Date.now() - degradedBootstrapStartedAtMs,
gitCommands: degradedBootstrapMetrics.total,
failedGitCommands: degradedBootstrapMetrics.failed,
maxConcurrentGitCommands: degradedBootstrapMetrics.maxConcurrent,
byCommand: summarizeCommands(degradedBootstrapMetrics),
producers: subtractCounts(degraded.counts, degradedBootstrapCounts),
queueAfter: {
inFlight: degradedBootstrapServiceMetrics.workspaceRefreshInFlightCount,
queued: degradedBootstrapServiceMetrics.workspaceRefreshQueuedCount,
},
};
phases.degradedWatcher = await measurePhase({
service: degraded.service,
counts: degraded.counts,
action: async () => {
const refreshCount = degraded?.counts.worktree ?? 0;
await waitFor(
() => (degraded?.counts.worktree ?? 0) > refreshCount,
"degraded watcher poll",
8_000,
);
},
});
const report = {
generatedAt: new Date().toISOString(),
phases,
};
const outputPath = process.env.PASEO_GIT_OBSERVATION_REPORT?.trim();
if (outputPath) {
await writeFile(path.resolve(outputPath), `${JSON.stringify(report, null, 2)}\n`, "utf8");
}
process.stdout.write(`GIT_OBSERVATION_MEASUREMENT ${JSON.stringify(report)}\n`);
} finally {
stopGitCommandMetrics();
diffSubscription?.unsubscribe();
summarySubscription?.unsubscribe();
diffManager?.dispose();
if (healthy) {
await closeMeasuredService({
service: healthy.service,
observer: healthy.watcher.observer,
subscriptions: healthy.watcher.subscriptions,
}).catch(() => {});
}
if (degraded) {
await closeMeasuredService({
service: degraded.service,
observer: degraded.watcher.observer,
subscriptions: degraded.watcher.subscriptions,
}).catch(() => {});
}
rmSync(tempDir, { recursive: true, force: true });
}
}
main().catch((error) => {
process.stderr.write(`${error instanceof Error ? error.stack : String(error)}\n`);
process.exitCode = 1;
});