From bc4d4fcf48df374525c0d02d977713d40404feee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=82CHES?= Date: Sun, 15 Mar 2026 16:57:22 -0600 Subject: [PATCH] perf: fix synchronous I/O in hot paths (#540) Replace existsSync collision loop with atomic O_CREAT|O_EXCL file creation, hoist regex to module-level constant, and memoize getPackageDir() to avoid repeated directory walks. Co-authored-by: Claude Opus 4.6 (1M context) --- packages/pi-coding-agent/src/config.ts | 16 ++++++++----- src/resources/extensions/gsd/activity-log.ts | 25 ++++++++++++++------ 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/packages/pi-coding-agent/src/config.ts b/packages/pi-coding-agent/src/config.ts index 2c971aaa3..70297cc16 100644 --- a/packages/pi-coding-agent/src/config.ts +++ b/packages/pi-coding-agent/src/config.ts @@ -77,29 +77,33 @@ export function getUpdateInstruction(packageName: string): string { * - For Node.js (dist/): returns __dirname (the dist/ directory) * - For tsx (src/): returns parent directory (the package root) */ +let _cachedPackageDir: string | undefined; + export function getPackageDir(): string { + if (_cachedPackageDir !== undefined) return _cachedPackageDir; + // Allow override via environment variable (useful for Nix/Guix where store paths tokenize poorly) const envDir = process.env.PI_PACKAGE_DIR; if (envDir) { - if (envDir === "~") return homedir(); - if (envDir.startsWith("~/")) return homedir() + envDir.slice(1); - return envDir; + if (envDir === "~") return (_cachedPackageDir = homedir()); + if (envDir.startsWith("~/")) return (_cachedPackageDir = homedir() + envDir.slice(1)); + return (_cachedPackageDir = envDir); } if (isBunBinary) { // Bun binary: process.execPath points to the compiled executable - return dirname(process.execPath); + return (_cachedPackageDir = dirname(process.execPath)); } // Node.js: walk up from __dirname until we find package.json let dir = __dirname; while (dir !== dirname(dir)) { if (existsSync(join(dir, "package.json"))) { - return dir; + return (_cachedPackageDir = dir); } dir = dirname(dir); } // Fallback (shouldn't happen) - return __dirname; + return (_cachedPackageDir = __dirname); } /** diff --git a/src/resources/extensions/gsd/activity-log.ts b/src/resources/extensions/gsd/activity-log.ts index 7aef8fc47..3e58543ec 100644 --- a/src/resources/extensions/gsd/activity-log.ts +++ b/src/resources/extensions/gsd/activity-log.ts @@ -8,10 +8,11 @@ * Diagnostic extraction is handled by session-forensics.ts. */ -import { writeFileSync, mkdirSync, readdirSync, unlinkSync, statSync } from "node:fs"; -import { existsSync } from "node:fs"; +import { writeFileSync, mkdirSync, readdirSync, unlinkSync, statSync, openSync, closeSync, constants } from "node:fs"; import { createHash } from "node:crypto"; import { join } from "node:path"; + +const SEQ_PREFIX_RE = /^(\d+)-/; import type { ExtensionContext } from "@gsd/pi-coding-agent"; import { gsdRoot } from "./paths.js"; @@ -26,7 +27,7 @@ function scanNextSequence(activityDir: string): number { let maxSeq = 0; try { for (const f of readdirSync(activityDir)) { - const match = f.match(/^(\d+)-/); + const match = f.match(SEQ_PREFIX_RE); if (match) maxSeq = Math.max(maxSeq, parseInt(match[1], 10)); } } catch { @@ -55,14 +56,24 @@ function nextActivityFilePath( unitType: string, safeUnitId: string, ): string { - while (true) { + // Use O_CREAT | O_EXCL for atomic "create if absent" — no directory scan needed. + for (let attempts = 0; attempts < 1000; attempts++) { const seq = String(state.nextSeq).padStart(3, "0"); const filePath = join(activityDir, `${seq}-${unitType}-${safeUnitId}.jsonl`); - if (!existsSync(filePath)) { + try { + const fd = openSync(filePath, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY); + closeSync(fd); return filePath; + } catch (err: any) { + if (err?.code === "EEXIST") { + state.nextSeq++; + continue; + } + throw err; } - state.nextSeq = scanNextSequence(activityDir); } + // Fallback: should never reach here in practice + throw new Error(`Failed to find available activity log sequence in ${activityDir}`); } export function saveActivityLog( @@ -99,7 +110,7 @@ export function pruneActivityLogs(activityDir: string, retentionDays: number): v const files = readdirSync(activityDir); const entries: { seq: number; filePath: string }[] = []; for (const f of files) { - const match = f.match(/^(\d+)-/); + const match = f.match(SEQ_PREFIX_RE); if (match) entries.push({ seq: parseInt(match[1], 10), filePath: join(activityDir, f) }); } if (entries.length === 0) return;