fix: resolve perf regression in transcript loading and session search
Two performance regressions introduced in recent PRs: 1. readAgentConfigs blocked transcript rendering (PR #50) The agent config IPC call was awaited on the critical path of fetchSessionDetail, preventing any transcript data from rendering until the filesystem read completed. On macOS this was especially noticeable due to security checks on first directory access. Fixed by making the call fire-and-forget: the transcript renders immediately and subagent color badges update asynchronously. Also set the project cache key optimistically before the async call to prevent duplicate in-flight requests on rapid navigation. 2. SessionSearcher stat()-called every session file on each search (PR #53) LocalFileSystemProvider.readdir() did not populate the optional mtimeMs field on FsDirent entries. The new SearchTextCache-based SessionSearcher fell back to an individual fsProvider.stat() call per session file when mtimeMs was missing, adding N extra filesystem round-trips on every search in local mode. Fixed by statting all entries concurrently inside readdir(), so mtimeMs is always populated and the stat fallback is never triggered. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
39d88e22a4
commit
9e7d3b58a1
2 changed files with 31 additions and 14 deletions
|
|
@ -43,11 +43,25 @@ export class LocalFileSystemProvider implements FileSystemProvider {
|
|||
|
||||
async readdir(dirPath: string): Promise<FsDirent[]> {
|
||||
const entries = await fs.promises.readdir(dirPath, { withFileTypes: true });
|
||||
return entries.map((entry) => ({
|
||||
name: entry.name,
|
||||
isFile: () => entry.isFile(),
|
||||
isDirectory: () => entry.isDirectory(),
|
||||
}));
|
||||
// Stat all entries concurrently to populate mtimeMs, used by SessionSearcher's
|
||||
// mtime-based cache invalidation. Failures are silently ignored (mtimeMs stays undefined).
|
||||
return Promise.all(
|
||||
entries.map(async (entry) => {
|
||||
let mtimeMs: number | undefined;
|
||||
try {
|
||||
const stat = await fs.promises.stat(`${dirPath}/${entry.name}`);
|
||||
mtimeMs = stat.mtimeMs;
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return {
|
||||
name: entry.name,
|
||||
mtimeMs,
|
||||
isFile: () => entry.isFile(),
|
||||
isDirectory: () => entry.isDirectory(),
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
createReadStream(filePath: string, opts?: ReadStreamOptions): fs.ReadStream {
|
||||
|
|
|
|||
|
|
@ -197,16 +197,19 @@ export const createSessionDetailSlice: StateCreator<AppState, [], [], SessionDet
|
|||
let claudeMdStats: Map<string, ClaudeMdStats> | null = null;
|
||||
let contextStats: Map<string, ContextStats> | null = null;
|
||||
let phaseInfo: ContextPhaseInfo | null = null;
|
||||
// Fetch agent configs from .claude/agents/ (only when project changes)
|
||||
// Fetch agent configs from .claude/agents/ (only when project changes).
|
||||
// Fire-and-forget: don't block transcript rendering — color badges update async.
|
||||
if (connectionMode !== 'ssh' && projectRoot && projectRoot !== agentConfigsCachedForProject) {
|
||||
try {
|
||||
const configs = await api.readAgentConfigs(projectRoot);
|
||||
if (requestGeneration !== sessionDetailFetchGeneration) return;
|
||||
agentConfigsCachedForProject = projectRoot;
|
||||
set({ agentConfigs: configs });
|
||||
} catch (err) {
|
||||
logger.error('Failed to read agent configs:', err);
|
||||
}
|
||||
agentConfigsCachedForProject = projectRoot; // Optimistic set to prevent duplicate fetches
|
||||
api
|
||||
.readAgentConfigs(projectRoot)
|
||||
.then((configs) => {
|
||||
set({ agentConfigs: configs });
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.error('Failed to read agent configs:', err);
|
||||
agentConfigsCachedForProject = ''; // Reset so it retries next time
|
||||
});
|
||||
}
|
||||
|
||||
if (connectionMode !== 'ssh' && conversation?.items) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue