perf: cache runtime process table to stop ps spawn storm

listRuntimeProcesses spawned a full `ps -ax` on every call with no caching.
It is invoked very frequently while a team runs: runtime liveness/telemetry
snapshots are rebuilt whenever a team file changes (invalidateRuntimeSnapshotCaches
fires from ~25 sites), so the main process ended up forking ps dozens of times
per second, which is expensive from the large Electron main process and pegged
its CPU.

Add a 1s TTL cache plus in-flight coalescing on the single ps spawn point.
Liveness/telemetry callers already tolerate ~2s staleness via their own snapshot
caches and the OS process table changes negligibly within a second, so this caps
ps to <=1/s without affecting liveness correctness. Measured posix_spawn dropped
from ~146/s to ~11/s with a team running.
This commit is contained in:
777genius 2026-05-29 23:46:22 +03:00
parent c0b9b4ec5d
commit d06ea7f265

View file

@ -32,6 +32,25 @@ export interface RuntimeProcessTableRow {
rssBytes?: number;
}
/**
* Short-lived cache window for the global process table.
*
* `listRuntimeProcesses` spawns a full `ps -ax`, which is expensive when forked
* from the large Electron main process. Runtime liveness/telemetry callers fire
* very frequently (every team file change invalidates their per-team snapshot
* caches), so without throttling here the main process spawns `ps` dozens of
* times per second while a team runs. Those callers already tolerate ~2s
* staleness via their own snapshot caches, and the OS process table changes
* negligibly within a second, so a 1s window collapses the spawn storm without
* affecting liveness correctness.
*/
const RUNTIME_PROCESS_TABLE_CACHE_TTL_MS = 1_000;
interface RuntimeProcessTableCacheEntry {
rows: RuntimeProcessTableRow[];
expiresAtMs: number;
}
export function parseRuntimeProcessTable(output: string): RuntimeProcessTableRow[] {
const rows: RuntimeProcessTableRow[] = [];
for (const line of output.split('\n')) {
@ -39,8 +58,13 @@ export function parseRuntimeProcessTable(output: string): RuntimeProcessTableRow
if (enrichedMatch) {
const pid = Number.parseInt(enrichedMatch[1], 10);
const ppid = Number.parseInt(enrichedMatch[2], 10);
const cpuPercent = Number(enrichedMatch[3]);
const rssKb = Number(enrichedMatch[4]);
// `ps` formats the %cpu column with the locale decimal separator (e.g. "12,5" on
// de_DE/fr_FR locales, which the runtime inherits via process.env). Normalize the
// comma to a dot so Number() does not return NaN — otherwise the enriched parse would
// fail its isFinite guard and fall back to the basic parser, leaking the pcpu/rss
// columns into `command`.
const cpuPercent = Number(enrichedMatch[3]?.replace(',', '.'));
const rssKb = Number(enrichedMatch[4]?.replace(',', '.'));
const command = enrichedMatch[5]?.trim() ?? '';
if (
Number.isFinite(pid) &&
@ -80,6 +104,8 @@ export function parseRuntimeProcessTable(output: string): RuntimeProcessTableRow
export class TmuxPlatformCommandExecutor {
readonly #wslService: TmuxWslService;
readonly #packageManagerResolver: TmuxPackageManagerResolver;
#runtimeProcessTableCache: RuntimeProcessTableCacheEntry | null = null;
#runtimeProcessTableInFlight: Promise<RuntimeProcessTableRow[]> | null = null;
constructor(
wslService = new TmuxWslService(),
@ -192,6 +218,31 @@ export class TmuxPlatformCommandExecutor {
}
async listRuntimeProcesses(): Promise<RuntimeProcessTableRow[]> {
const cached = this.#runtimeProcessTableCache;
if (cached && cached.expiresAtMs > Date.now()) {
return cached.rows;
}
if (this.#runtimeProcessTableInFlight) {
return this.#runtimeProcessTableInFlight;
}
const request = this.#readRuntimeProcessesUncached()
.then((rows) => {
this.#runtimeProcessTableCache = {
rows,
expiresAtMs: Date.now() + RUNTIME_PROCESS_TABLE_CACHE_TTL_MS,
};
return rows;
})
.finally(() => {
if (this.#runtimeProcessTableInFlight === request) {
this.#runtimeProcessTableInFlight = null;
}
});
this.#runtimeProcessTableInFlight = request;
return request;
}
async #readRuntimeProcessesUncached(): Promise<RuntimeProcessTableRow[]> {
const result =
process.platform === 'win32'
? await this.#wslService.execInPreferredDistro([