From 0dc283bc3c8af1f840f9e31957ae73beb6fe3b48 Mon Sep 17 00:00:00 2001 From: Jarvis Date: Wed, 24 Jun 2026 14:01:20 -0500 Subject: [PATCH] perf(fleet): scan persona dirs once per provision Fix #665 test timeout. --- packages/mosaic/src/commands/fleet-personas.ts | 15 +++++++++++++++ .../mosaic/src/commands/fleet-provision.ts | 18 ++++++++++++++++-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/packages/mosaic/src/commands/fleet-personas.ts b/packages/mosaic/src/commands/fleet-personas.ts index 9bb807c..a65d5fc 100644 --- a/packages/mosaic/src/commands/fleet-personas.ts +++ b/packages/mosaic/src/commands/fleet-personas.ts @@ -234,6 +234,21 @@ export async function resolvePersona( extractClassesFromDir(rolesDir), extractClassesFromDir(overrideDir), ]); + return resolvePersonaFrom(klass, { rolesDir, overrideDir, base, over }); +} + +/** + * Resolve a single class against ALREADY-EXTRACTED layer maps. Callers that + * resolve many classes against the same two directories (e.g. provisioning a + * full roster) should {@link extractClassesFromDir} each dir ONCE and reuse the + * result here, rather than paying a full directory re-scan per class. Precedence + * is identical to {@link resolvePersona}: override layer wins, then baseline. + */ +export async function resolvePersonaFrom( + klass: string, + layers: { rolesDir: string; overrideDir: string; base: DirClasses; over: DirClasses }, +): Promise { + const { rolesDir, overrideDir, base, over } = layers; const fromLayer = async ( dir: string, diff --git a/packages/mosaic/src/commands/fleet-provision.ts b/packages/mosaic/src/commands/fleet-provision.ts index 4c058e0..b2f64a1 100644 --- a/packages/mosaic/src/commands/fleet-provision.ts +++ b/packages/mosaic/src/commands/fleet-provision.ts @@ -33,7 +33,12 @@ import { defaultRolesDir, listPersonaClassesWithOverrides, } from './fleet-profiles.js'; -import { defaultOverrideDir, resolvePersona, type PersonaLayer } from './fleet-personas.js'; +import { + defaultOverrideDir, + extractClassesFromDir, + resolvePersonaFrom, + type PersonaLayer, +} from './fleet-personas.js'; function defaultMosaicHome(): string { return process.env['MOSAIC_HOME'] ?? join(homedir(), '.config', 'mosaic'); @@ -172,12 +177,21 @@ export async function generateRoster( ? profile.roster : profile.roster.filter((e) => floor.has(e.class)); + // Scan the persona directories ONCE, then resolve every roster entry against + // the in-memory maps. resolvePersona() would otherwise re-scan both dirs per + // entry — O(entries × files) redundant reads that push --full provisioning + // past the test timeout on slow/contended filesystems. + const [base, over] = await Promise.all([ + extractClassesFromDir(rolesDir), + extractClassesFromDir(overrideDir), + ]); + const seats: GeneratedSeat[] = []; for (const entry of selected) { const isFloor = floor.has(entry.class); const isLead = entry.class === lead; - const resolved = await resolvePersona(entry.class, { rolesDir, overrideDir }); + const resolved = await resolvePersonaFrom(entry.class, { rolesDir, overrideDir, base, over }); if (!resolved) { // Defensive: validateProfile already guards this, but a class can resolve // for membership yet have no readable file. Fail loudly rather than emit a