From e7d7b3014e43ce8c8043f7b835af0c667b0d309e Mon Sep 17 00:00:00 2001 From: 777genius Date: Sat, 30 May 2026 21:52:36 +0300 Subject: [PATCH] perf(main): reduce runtime launch metadata work --- .../services/team/TeamProvisioningService.ts | 85 +++++++++++++++---- .../team/TeamRuntimeLivenessResolver.ts | 23 ++++- .../team/TeamProvisioningService.test.ts | 47 ++++++++++ .../team/TeamRuntimeLivenessResolver.test.ts | 14 ++- 4 files changed, 150 insertions(+), 19 deletions(-) diff --git a/src/main/services/team/TeamProvisioningService.ts b/src/main/services/team/TeamProvisioningService.ts index 9655b0d5..96c019a1 100644 --- a/src/main/services/team/TeamProvisioningService.ts +++ b/src/main/services/team/TeamProvisioningService.ts @@ -599,6 +599,15 @@ interface PersistedRuntimeMemberLike { runtimeSessionId?: string; } +interface PersistedTeamConfigCacheEntry { + path: string; + size: number; + mtimeMs: number; + ctimeMs: number; + projectPath: string | null; + members: PersistedRuntimeMemberLike[]; +} + type RelayInboxMessage = InboxMessage & { messageId: string }; interface RelayInboxMessageView { @@ -3472,6 +3481,7 @@ export class TeamProvisioningService { stats: RuntimeProcessUsageStats | null; } >(); + private readonly persistedTeamConfigCache = new Map(); private readonly agentRuntimeSnapshotInFlightByTeam = new Map< string, { @@ -3764,6 +3774,7 @@ export class TeamProvisioningService { this.liveTeamAgentRuntimeMetadataCache.delete(teamName); this.liveTeamAgentRuntimeMetadataInFlightByTeam.delete(teamName); this.runtimeProcessRowsForUsageSnapshotByTeam.delete(teamName); + this.persistedTeamConfigCache.delete(teamName); // CPU/RSS samples are TTL-bound and do not decide liveness; keeping them // avoids repeated pidusage forks when launch-state churn invalidates snapshots. } @@ -31581,32 +31592,74 @@ export class TeamProvisioningService { } } - private readPersistedTeamProjectPath(teamName: string): string | null { + private clonePersistedRuntimeMember( + member: PersistedRuntimeMemberLike + ): PersistedRuntimeMemberLike { + return { ...member }; + } + + private isPersistedRuntimeMemberLike(member: unknown): member is PersistedRuntimeMemberLike { + return !!member && typeof member === 'object'; + } + + private readPersistedTeamConfig(teamName: string): PersistedTeamConfigCacheEntry | null { const configPath = path.join(getTeamsBasePath(), teamName, 'config.json'); + let stat: fs.Stats; + try { + stat = fs.statSync(configPath); + } catch { + this.persistedTeamConfigCache.delete(teamName); + return null; + } + + const cached = this.persistedTeamConfigCache.get(teamName); + if ( + cached && + cached.path === configPath && + cached.size === stat.size && + cached.mtimeMs === stat.mtimeMs && + cached.ctimeMs === stat.ctimeMs + ) { + return cached; + } + try { const raw = fs.readFileSync(configPath, 'utf8'); - const parsed = JSON.parse(raw) as { projectPath?: unknown }; + const parsed = JSON.parse(raw) as { projectPath?: unknown; members?: unknown }; const projectPath = typeof parsed.projectPath === 'string' ? parsed.projectPath.trim() : ''; - return projectPath || null; + const members = Array.isArray(parsed.members) + ? parsed.members + .filter((member): member is PersistedRuntimeMemberLike => + this.isPersistedRuntimeMemberLike(member) + ) + .map((member) => this.clonePersistedRuntimeMember(member)) + : []; + const entry: PersistedTeamConfigCacheEntry = { + path: configPath, + size: stat.size, + mtimeMs: stat.mtimeMs, + ctimeMs: stat.ctimeMs, + projectPath: projectPath || null, + members, + }; + this.persistedTeamConfigCache.set(teamName, entry); + return entry; } catch { + this.persistedTeamConfigCache.delete(teamName); return null; } } + private readPersistedTeamProjectPath(teamName: string): string | null { + return this.readPersistedTeamConfig(teamName)?.projectPath ?? null; + } + private readPersistedRuntimeMembers(teamName: string): PersistedRuntimeMemberLike[] { - const configPath = path.join(getTeamsBasePath(), teamName, 'config.json'); - try { - const raw = fs.readFileSync(configPath, 'utf8'); - const parsed = JSON.parse(raw) as { members?: unknown }; - if (!Array.isArray(parsed.members)) { - return []; - } - return parsed.members.filter((member): member is PersistedRuntimeMemberLike => { - return !!member && typeof member === 'object'; - }); - } catch { - return []; - } + return ( + this.readPersistedTeamConfig(teamName)?.members.map((member) => + this.clonePersistedRuntimeMember(member) + ) ?? [] + ); } private listPersistedTeamNames(): string[] { diff --git a/src/main/services/team/TeamRuntimeLivenessResolver.ts b/src/main/services/team/TeamRuntimeLivenessResolver.ts index 558ffb0c..0d47b74b 100644 --- a/src/main/services/team/TeamRuntimeLivenessResolver.ts +++ b/src/main/services/team/TeamRuntimeLivenessResolver.ts @@ -45,6 +45,8 @@ export interface ResolvedTeamMemberRuntimeLiveness { const SHELL_COMMAND_NAMES = new Set(['sh', 'bash', 'zsh', 'fish', 'dash', 'login', 'tmux']); const SECRET_FLAG_PATTERN = /(--(?:api-key|token|password|secret|authorization|auth-token)(?:=|\s+))("[^"]*"|'[^']*'|\S+)/gi; +const CLI_ARG_VALUES_CACHE_MAX_COMMANDS = 1_000; +const cliArgValuesCache = new Map>(); function basenameCommand(command: string | undefined): string { const firstToken = command?.trim().split(/\s+/, 1)[0] ?? ''; @@ -69,6 +71,16 @@ function escapeRegexLiteral(value: string): string { } export function extractCliArgValues(command: string, argName: string): string[] { + const cachedByArg = cliArgValuesCache.get(command); + const cachedValues = cachedByArg?.get(argName); + if (cachedValues) { + if (cachedByArg) { + cliArgValuesCache.delete(command); + cliArgValuesCache.set(command, cachedByArg); + } + return [...cachedValues]; + } + const escapedArg = escapeRegexLiteral(argName); const pattern = new RegExp( `(?:^|\\s)${escapedArg}(?:=|\\s+)("([^"]*)"|'([^']*)'|([^\\s]+))`, @@ -80,7 +92,16 @@ export function extractCliArgValues(command: string, argName: string): string[] const value = (match[2] ?? match[3] ?? match[4] ?? '').trim(); if (value) values.push(value); } - return values; + const nextByArg = cachedByArg ?? new Map(); + nextByArg.set(argName, values); + cliArgValuesCache.delete(command); + cliArgValuesCache.set(command, nextByArg); + while (cliArgValuesCache.size > CLI_ARG_VALUES_CACHE_MAX_COMMANDS) { + const oldestKey = cliArgValuesCache.keys().next().value; + if (oldestKey === undefined) break; + cliArgValuesCache.delete(oldestKey); + } + return [...values]; } export function commandArgEquals( diff --git a/test/main/services/team/TeamProvisioningService.test.ts b/test/main/services/team/TeamProvisioningService.test.ts index 4c0a750c..927952a2 100644 --- a/test/main/services/team/TeamProvisioningService.test.ts +++ b/test/main/services/team/TeamProvisioningService.test.ts @@ -664,6 +664,8 @@ type TeamProvisioningServicePrivateHarness = { teamName: string, options?: { allowAnonymousFailure?: boolean; contextMemberNames?: readonly string[] } ) => Promise<{ kind: string; observedAt: string; source?: string; reason?: string } | null>; + readPersistedRuntimeMembers: (teamName: string) => Array>; + readPersistedTeamProjectPath: (teamName: string) => string | null; }; function privateHarness(svc: TeamProvisioningService): TeamProvisioningServicePrivateHarness { @@ -881,6 +883,51 @@ describe('TeamProvisioningService', () => { }); }); + describe('persisted team config cache', () => { + it('returns defensive runtime member copies and refreshes when config changes', () => { + const teamName = 'persisted-config-cache-team'; + const teamDir = path.join(tempTeamsBase, teamName); + const configPath = path.join(teamDir, 'config.json'); + fs.mkdirSync(teamDir, { recursive: true }); + fs.writeFileSync( + configPath, + JSON.stringify({ + projectPath: '/repo-one', + members: [{ name: 'alice', agentId: 'agent-alice' }], + }), + 'utf8' + ); + + const svc = new TeamProvisioningService(); + const internals = privateHarness(svc); + const firstMembers = internals.readPersistedRuntimeMembers(teamName); + firstMembers[0]!.name = 'mutated'; + + expect(internals.readPersistedRuntimeMembers(teamName)[0]).toMatchObject({ + name: 'alice', + agentId: 'agent-alice', + }); + expect(internals.readPersistedTeamProjectPath(teamName)).toBe('/repo-one'); + + fs.writeFileSync( + configPath, + JSON.stringify({ + projectPath: '/repo-two', + members: [{ name: 'bob', agentId: 'agent-bob' }], + }), + 'utf8' + ); + const refreshedAt = new Date(Date.now() + 5_000); + fs.utimesSync(configPath, refreshedAt, refreshedAt); + + expect(internals.readPersistedRuntimeMembers(teamName)[0]).toMatchObject({ + name: 'bob', + agentId: 'agent-bob', + }); + expect(internals.readPersistedTeamProjectPath(teamName)).toBe('/repo-two'); + }); + }); + describe('live lead messages', () => { it('updates one live message for Codex synthetic text chunks', () => { const svc = new TeamProvisioningService(); diff --git a/test/main/services/team/TeamRuntimeLivenessResolver.test.ts b/test/main/services/team/TeamRuntimeLivenessResolver.test.ts index 9c60fea0..81cbc776 100644 --- a/test/main/services/team/TeamRuntimeLivenessResolver.test.ts +++ b/test/main/services/team/TeamRuntimeLivenessResolver.test.ts @@ -1,9 +1,9 @@ -import { describe, expect, it } from 'vitest'; - import { + extractCliArgValues, resolveTeamMemberRuntimeLiveness, sanitizeProcessCommandForDiagnostics, } from '@main/services/team/TeamRuntimeLivenessResolver'; +import { describe, expect, it } from 'vitest'; const NOW = '2026-04-24T12:00:00.000Z'; @@ -238,4 +238,14 @@ describe('resolveTeamMemberRuntimeLiveness', () => { sanitizeProcessCommandForDiagnostics('node runtime --api-key sk-123 --token=abc --safe ok') ).toBe('node runtime --api-key [redacted] --token=[redacted] --safe ok'); }); + + it('keeps cached CLI arg extraction immutable for callers', () => { + const command = + 'node runtime --team-name demo --agent-id "agent alice" --agent-id agent-bob'; + const first = extractCliArgValues(command, '--agent-id'); + first.push('mutated'); + + expect(extractCliArgValues(command, '--agent-id')).toEqual(['agent alice', 'agent-bob']); + expect(extractCliArgValues(command, '--team-name')).toEqual(['demo']); + }); });