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) <noreply@anthropic.com>
This commit is contained in:
parent
a43836ffbb
commit
bc4d4fcf48
2 changed files with 28 additions and 13 deletions
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue