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;
|
runtimeSessionId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface PersistedTeamConfigCacheEntry {
|
||||||
|
path: string;
|
||||||
|
size: number;
|
||||||
|
mtimeMs: number;
|
||||||
|
ctimeMs: number;
|
||||||
|
projectPath: string | null;
|
||||||
|
members: PersistedRuntimeMemberLike[];
|
||||||
|
}
|
||||||
|
|
||||||
type RelayInboxMessage = InboxMessage & { messageId: string };
|
type RelayInboxMessage = InboxMessage & { messageId: string };
|
||||||
|
|
||||||
interface RelayInboxMessageView {
|
interface RelayInboxMessageView {
|
||||||
|
|
@ -3472,6 +3481,7 @@ export class TeamProvisioningService {
|
||||||
stats: RuntimeProcessUsageStats | null;
|
stats: RuntimeProcessUsageStats | null;
|
||||||
}
|
}
|
||||||
>();
|
>();
|
||||||
|
private readonly persistedTeamConfigCache = new Map<string, PersistedTeamConfigCacheEntry>();
|
||||||
private readonly agentRuntimeSnapshotInFlightByTeam = new Map<
|
private readonly agentRuntimeSnapshotInFlightByTeam = new Map<
|
||||||
string,
|
string,
|
||||||
{
|
{
|
||||||
|
|
@ -3764,6 +3774,7 @@ export class TeamProvisioningService {
|
||||||
this.liveTeamAgentRuntimeMetadataCache.delete(teamName);
|
this.liveTeamAgentRuntimeMetadataCache.delete(teamName);
|
||||||
this.liveTeamAgentRuntimeMetadataInFlightByTeam.delete(teamName);
|
this.liveTeamAgentRuntimeMetadataInFlightByTeam.delete(teamName);
|
||||||
this.runtimeProcessRowsForUsageSnapshotByTeam.delete(teamName);
|
this.runtimeProcessRowsForUsageSnapshotByTeam.delete(teamName);
|
||||||
|
this.persistedTeamConfigCache.delete(teamName);
|
||||||
// CPU/RSS samples are TTL-bound and do not decide liveness; keeping them
|
// CPU/RSS samples are TTL-bound and do not decide liveness; keeping them
|
||||||
// avoids repeated pidusage forks when launch-state churn invalidates snapshots.
|
// 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');
|
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 {
|
try {
|
||||||
const raw = fs.readFileSync(configPath, 'utf8');
|
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() : '';
|
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 {
|
} catch {
|
||||||
|
this.persistedTeamConfigCache.delete(teamName);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private readPersistedTeamProjectPath(teamName: string): string | null {
|
||||||
|
return this.readPersistedTeamConfig(teamName)?.projectPath ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
private readPersistedRuntimeMembers(teamName: string): PersistedRuntimeMemberLike[] {
|
private readPersistedRuntimeMembers(teamName: string): PersistedRuntimeMemberLike[] {
|
||||||
const configPath = path.join(getTeamsBasePath(), teamName, 'config.json');
|
return (
|
||||||
try {
|
this.readPersistedTeamConfig(teamName)?.members.map((member) =>
|
||||||
const raw = fs.readFileSync(configPath, 'utf8');
|
this.clonePersistedRuntimeMember(member)
|
||||||
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 [];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private listPersistedTeamNames(): string[] {
|
private listPersistedTeamNames(): string[] {
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,8 @@ export interface ResolvedTeamMemberRuntimeLiveness {
|
||||||
const SHELL_COMMAND_NAMES = new Set(['sh', 'bash', 'zsh', 'fish', 'dash', 'login', 'tmux']);
|
const SHELL_COMMAND_NAMES = new Set(['sh', 'bash', 'zsh', 'fish', 'dash', 'login', 'tmux']);
|
||||||
const SECRET_FLAG_PATTERN =
|
const SECRET_FLAG_PATTERN =
|
||||||
/(--(?:api-key|token|password|secret|authorization|auth-token)(?:=|\s+))("[^"]*"|'[^']*'|\S+)/gi;
|
/(--(?: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 {
|
function basenameCommand(command: string | undefined): string {
|
||||||
const firstToken = command?.trim().split(/\s+/, 1)[0] ?? '';
|
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[] {
|
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 escapedArg = escapeRegexLiteral(argName);
|
||||||
const pattern = new RegExp(
|
const pattern = new RegExp(
|
||||||
`(?:^|\\s)${escapedArg}(?:=|\\s+)("([^"]*)"|'([^']*)'|([^\\s]+))`,
|
`(?:^|\\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();
|
const value = (match[2] ?? match[3] ?? match[4] ?? '').trim();
|
||||||
if (value) values.push(value);
|
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(
|
export function commandArgEquals(
|
||||||
|
|
|
||||||
|
|
@ -664,6 +664,8 @@ type TeamProvisioningServicePrivateHarness = {
|
||||||
teamName: string,
|
teamName: string,
|
||||||
options?: { allowAnonymousFailure?: boolean; contextMemberNames?: readonly string[] }
|
options?: { allowAnonymousFailure?: boolean; contextMemberNames?: readonly string[] }
|
||||||
) => Promise<{ kind: string; observedAt: string; source?: string; reason?: string } | null>;
|
) => 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 {
|
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', () => {
|
describe('live lead messages', () => {
|
||||||
it('updates one live message for Codex synthetic text chunks', () => {
|
it('updates one live message for Codex synthetic text chunks', () => {
|
||||||
const svc = new TeamProvisioningService();
|
const svc = new TeamProvisioningService();
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { describe, expect, it } from 'vitest';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
extractCliArgValues,
|
||||||
resolveTeamMemberRuntimeLiveness,
|
resolveTeamMemberRuntimeLiveness,
|
||||||
sanitizeProcessCommandForDiagnostics,
|
sanitizeProcessCommandForDiagnostics,
|
||||||
} from '@main/services/team/TeamRuntimeLivenessResolver';
|
} from '@main/services/team/TeamRuntimeLivenessResolver';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
const NOW = '2026-04-24T12:00:00.000Z';
|
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')
|
sanitizeProcessCommandForDiagnostics('node runtime --api-key sk-123 --token=abc --safe ok')
|
||||||
).toBe('node runtime --api-key [redacted] --token=[redacted] --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