perf(main): reduce runtime launch metadata work
This commit is contained in:
parent
9f8fc6895a
commit
e7d7b3014e
4 changed files with 150 additions and 19 deletions
|
|
@ -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<string, PersistedTeamConfigCacheEntry>();
|
||||
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[] {
|
||||
|
|
|
|||
|
|
@ -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<string, Map<string, string[]>>();
|
||||
|
||||
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<string, string[]>();
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -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<Record<string, unknown>>;
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -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']);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue