perf(state): cache buildStateFrontmatter disk scan per process (#1967)
buildStateFrontmatter performs N+1 readdirSync calls (phases dir + each phase subdirectory) every time it's called. Multiple state writes within a single gsd-tools invocation repeat the same scan unnecessarily. Add a module-level Map cache keyed by cwd that stores the disk scan results. The cache auto-clears when the process exits since each gsd-tools CLI invocation is a short-lived process running one command. Closes #1967 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,11 @@ const path = require('path');
|
|||||||
const { escapeRegex, loadConfig, getMilestoneInfo, getMilestonePhaseFilter, normalizeMd, planningDir, planningPaths, output, error, atomicWriteFileSync } = require('./core.cjs');
|
const { escapeRegex, loadConfig, getMilestoneInfo, getMilestonePhaseFilter, normalizeMd, planningDir, planningPaths, output, error, atomicWriteFileSync } = require('./core.cjs');
|
||||||
const { extractFrontmatter, reconstructFrontmatter } = require('./frontmatter.cjs');
|
const { extractFrontmatter, reconstructFrontmatter } = require('./frontmatter.cjs');
|
||||||
|
|
||||||
|
// Cache disk scan results from buildStateFrontmatter per cwd per process (#1967).
|
||||||
|
// Avoids re-reading N+1 directories on every state write when the phase structure
|
||||||
|
// hasn't changed within the same gsd-tools invocation.
|
||||||
|
const _diskScanCache = new Map();
|
||||||
|
|
||||||
/** Shorthand — every state command needs this path */
|
/** Shorthand — every state command needs this path */
|
||||||
function getStatePath(cwd) {
|
function getStatePath(cwd) {
|
||||||
return planningPaths(cwd).state;
|
return planningPaths(cwd).state;
|
||||||
@@ -737,28 +742,40 @@ function buildStateFrontmatter(bodyContent, cwd) {
|
|||||||
try {
|
try {
|
||||||
const phasesDir = planningPaths(cwd).phases;
|
const phasesDir = planningPaths(cwd).phases;
|
||||||
if (fs.existsSync(phasesDir)) {
|
if (fs.existsSync(phasesDir)) {
|
||||||
const isDirInMilestone = getMilestonePhaseFilter(cwd);
|
// Use cached disk scan when available — avoids N+1 readdirSync calls
|
||||||
const phaseDirs = fs.readdirSync(phasesDir, { withFileTypes: true })
|
// on repeated buildStateFrontmatter invocations within the same process (#1967)
|
||||||
.filter(e => e.isDirectory()).map(e => e.name)
|
let cached = _diskScanCache.get(cwd);
|
||||||
.filter(isDirInMilestone);
|
if (!cached) {
|
||||||
let diskTotalPlans = 0;
|
const isDirInMilestone = getMilestonePhaseFilter(cwd);
|
||||||
let diskTotalSummaries = 0;
|
const phaseDirs = fs.readdirSync(phasesDir, { withFileTypes: true })
|
||||||
let diskCompletedPhases = 0;
|
.filter(e => e.isDirectory()).map(e => e.name)
|
||||||
|
.filter(isDirInMilestone);
|
||||||
|
let diskTotalPlans = 0;
|
||||||
|
let diskTotalSummaries = 0;
|
||||||
|
let diskCompletedPhases = 0;
|
||||||
|
|
||||||
for (const dir of phaseDirs) {
|
for (const dir of phaseDirs) {
|
||||||
const files = fs.readdirSync(path.join(phasesDir, dir));
|
const files = fs.readdirSync(path.join(phasesDir, dir));
|
||||||
const plans = files.filter(f => f.match(/-PLAN\.md$/i)).length;
|
const plans = files.filter(f => f.match(/-PLAN\.md$/i)).length;
|
||||||
const summaries = files.filter(f => f.match(/-SUMMARY\.md$/i)).length;
|
const summaries = files.filter(f => f.match(/-SUMMARY\.md$/i)).length;
|
||||||
diskTotalPlans += plans;
|
diskTotalPlans += plans;
|
||||||
diskTotalSummaries += summaries;
|
diskTotalSummaries += summaries;
|
||||||
if (plans > 0 && summaries >= plans) diskCompletedPhases++;
|
if (plans > 0 && summaries >= plans) diskCompletedPhases++;
|
||||||
|
}
|
||||||
|
cached = {
|
||||||
|
totalPhases: isDirInMilestone.phaseCount > 0
|
||||||
|
? Math.max(phaseDirs.length, isDirInMilestone.phaseCount)
|
||||||
|
: phaseDirs.length,
|
||||||
|
completedPhases: diskCompletedPhases,
|
||||||
|
totalPlans: diskTotalPlans,
|
||||||
|
completedPlans: diskTotalSummaries,
|
||||||
|
};
|
||||||
|
_diskScanCache.set(cwd, cached);
|
||||||
}
|
}
|
||||||
totalPhases = isDirInMilestone.phaseCount > 0
|
totalPhases = cached.totalPhases;
|
||||||
? Math.max(phaseDirs.length, isDirInMilestone.phaseCount)
|
completedPhases = cached.completedPhases;
|
||||||
: phaseDirs.length;
|
totalPlans = cached.totalPlans;
|
||||||
completedPhases = diskCompletedPhases;
|
completedPlans = cached.completedPlans;
|
||||||
totalPlans = diskTotalPlans;
|
|
||||||
completedPlans = diskTotalSummaries;
|
|
||||||
}
|
}
|
||||||
} catch { /* intentionally empty */ }
|
} catch { /* intentionally empty */ }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user