Make Git slowdowns visible in daemon metrics (#2366)

* feat(server): expose daemon Git pressure metrics

Separate limiter queue wait from Git execution time and report subscription ownership so accumulating work is visible in the existing runtime log.

* fix(server): collect subscriptions in agent metrics

Reuse the existing agent snapshot during runtime flushes so WebSocket shutdown does not require an additional AgentManager method call.

* test(server): retry transient hub cleanup
This commit is contained in:
Mohamed Boudra
2026-07-23 21:57:08 +02:00
committed by GitHub
parent a290f74705
commit 12612f6646
13 changed files with 440 additions and 18 deletions
+35 -11
View File
@@ -2,6 +2,10 @@ import { existsSync } from "node:fs";
import pLimit from "p-limit";
import type { Logger } from "pino";
import type { ProcessEnvRecord } from "../server/paseo-env.js";
import {
GitCommandRuntimeMetricsWindow,
type GitCommandRuntimeMetricsSnapshot,
} from "./git-command-runtime-metrics.js";
import { spawnProcess } from "./spawn.js";
const DEFAULT_TIMEOUT_MS = 30_000;
@@ -10,6 +14,7 @@ const DEFAULT_STDERR_LIMIT = 2048;
const gitConcurrency = parseInt(process.env.PASEO_GIT_CONCURRENCY ?? "8", 10) || 8;
const gitLimit = pLimit(gitConcurrency);
const gitRuntimeMetrics = new GitCommandRuntimeMetricsWindow(gitConcurrency);
export interface GitCommandOptions {
cwd: string;
@@ -81,6 +86,13 @@ export function stopGitCommandMetrics(): GitCommandMetricsSnapshot {
};
}
export function snapshotGitCommandRuntimeMetrics(): GitCommandRuntimeMetricsSnapshot {
return gitRuntimeMetrics.snapshotAndReset({
active: gitLimit.activeCount,
pending: gitLimit.pendingCount,
});
}
function beginGitCommandMetric(): GitCommandMetricsState | null {
const state = gitCommandMetricsState;
if (!state) {
@@ -123,9 +135,11 @@ export function runGitCommand(
args: string[],
options: GitCommandOptions,
): Promise<GitCommandResult> {
return gitLimit(
const runtimeMetric = gitRuntimeMetrics.submit(getGitOperation(args));
const promise = gitLimit(
() =>
new Promise<GitCommandResult>((resolve, reject) => {
gitRuntimeMetrics.start(runtimeMetric);
const timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;
const maxOutputBytes = options.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES;
const acceptExitCodes = options.acceptExitCodes ?? [0];
@@ -175,24 +189,28 @@ export function runGitCommand(
callback();
};
const finishMetricOnce = (metric: GitCommandMetric) => {
const finishMetricOnce = (metric: GitCommandMetric, timedOut = false) => {
if (metricFinished) return;
metricFinished = true;
finishGitCommandMetric(metricsState, metric);
gitRuntimeMetrics.finish(runtimeMetric, { success: metric.success, timedOut });
};
const timer = setTimeout(() => {
const error = new Error(`Git command timed out after ${timeout}ms: ${command}`);
child.kill("SIGKILL");
finishMetricOnce({
args,
cwd: options.cwd,
startedAtMs: startedAt,
durationMs: Date.now() - startedAt,
exitCode: null,
signal: "SIGKILL",
success: false,
});
finishMetricOnce(
{
args,
cwd: options.cwd,
startedAtMs: startedAt,
durationMs: Date.now() - startedAt,
exitCode: null,
signal: "SIGKILL",
success: false,
},
true,
);
settle(() => reject(error));
}, timeout);
@@ -318,8 +336,14 @@ export function runGitCommand(
});
}),
);
gitRuntimeMetrics.observeLimiter(gitLimit.activeCount, gitLimit.pendingCount);
return promise;
}
function formatGitCommand(args: string[]): string {
return ["git", ...args].join(" ");
}
function getGitOperation(args: string[]): string {
return args[0] === "-c" ? (args[2] ?? "unknown") : (args[0] ?? "unknown");
}