feat: record first token latency in runtime router
This commit is contained in:
@@ -155,6 +155,10 @@ function summarizeRuntime(runtimeJson) {
|
|||||||
streamOpenCount: worker.streamOpenCount,
|
streamOpenCount: worker.streamOpenCount,
|
||||||
streamAbortCount: worker.streamAbortCount,
|
streamAbortCount: worker.streamAbortCount,
|
||||||
streamErrorCount: worker.streamErrorCount,
|
streamErrorCount: worker.streamErrorCount,
|
||||||
|
ewmaFirstTokenMs: worker.ewmaFirstTokenMs,
|
||||||
|
lastFirstTokenMs: worker.lastFirstTokenMs,
|
||||||
|
lastFirstTokenAt: worker.lastFirstTokenAt,
|
||||||
|
firstTokenCount: worker.firstTokenCount,
|
||||||
cpuLoad: worker.cpuLoad,
|
cpuLoad: worker.cpuLoad,
|
||||||
memoryPressure: worker.memoryPressure,
|
memoryPressure: worker.memoryPressure,
|
||||||
fdPressure: worker.fdPressure,
|
fdPressure: worker.fdPressure,
|
||||||
@@ -213,6 +217,7 @@ for (const worker of runtimeSummary?.workers ?? []) {
|
|||||||
if (worker.containerHealth && worker.containerHealth !== 'healthy') failures.push(`${worker.id}_container_${worker.containerHealth}`);
|
if (worker.containerHealth && worker.containerHealth !== 'healthy') failures.push(`${worker.id}_container_${worker.containerHealth}`);
|
||||||
if (!worker.metricsFresh) failures.push(`${worker.id}_metrics_stale`);
|
if (!worker.metricsFresh) failures.push(`${worker.id}_metrics_stale`);
|
||||||
if (worker.streamErrorCount > 0) failures.push(`${worker.id}_stream_errors_${worker.streamErrorCount}`);
|
if (worker.streamErrorCount > 0) failures.push(`${worker.id}_stream_errors_${worker.streamErrorCount}`);
|
||||||
|
if (worker.firstTokenCount > 0 && worker.ewmaFirstTokenMs <= 0) failures.push(`${worker.id}_first_token_ewma_missing`);
|
||||||
}
|
}
|
||||||
if (runtimeSummary?.toolRuntime?.chatInjectsCodeTools !== false) failures.push('chat_injects_code_tools');
|
if (runtimeSummary?.toolRuntime?.chatInjectsCodeTools !== false) failures.push('chat_injects_code_tools');
|
||||||
|
|
||||||
|
|||||||
@@ -8768,6 +8768,17 @@ function createRuntimeRouter({
|
|||||||
if (streamKey) multi.set(streamKey, status, { EX: 600 });
|
if (streamKey) multi.set(streamKey, status, { EX: 600 });
|
||||||
await multi.exec().catch(() => null);
|
await multi.exec().catch(() => null);
|
||||||
},
|
},
|
||||||
|
async firstTokenObserved(target, latencyMs) {
|
||||||
|
const client = await getClient();
|
||||||
|
if (!client || !target) return;
|
||||||
|
const workerId = workerIdForTarget(target);
|
||||||
|
const sample = Math.max(0, Math.round(Number(latencyMs) || 0));
|
||||||
|
const ewmaKey = key("worker", workerId, "ewma_first_token_ms");
|
||||||
|
const prev = readNumber(await client.get(ewmaKey).catch(() => null));
|
||||||
|
const next = prev > 0 ? Math.round(prev * 0.8 + sample * 0.2) : sample;
|
||||||
|
const now = String(Date.now());
|
||||||
|
await client.multi().set(ewmaKey, String(next)).set(key("worker", workerId, "last_first_token_ms"), String(sample)).set(key("worker", workerId, "last_first_token_at"), now).incr(key("worker", workerId, "first_token_count")).set(key("worker", workerId, "heartbeat"), now, { EX: 30 }).exec().catch(() => null);
|
||||||
|
},
|
||||||
async getStatus() {
|
async getStatus() {
|
||||||
const client = await getClient();
|
const client = await getClient();
|
||||||
const workers = [];
|
const workers = [];
|
||||||
@@ -8791,7 +8802,10 @@ function createRuntimeRouter({
|
|||||||
key("worker", workerId, "fd_count"),
|
key("worker", workerId, "fd_count"),
|
||||||
key("worker", workerId, "container_pids"),
|
key("worker", workerId, "container_pids"),
|
||||||
key("worker", workerId, "container_health"),
|
key("worker", workerId, "container_health"),
|
||||||
key("worker", workerId, "metrics_sampled_at")
|
key("worker", workerId, "metrics_sampled_at"),
|
||||||
|
key("worker", workerId, "last_first_token_ms"),
|
||||||
|
key("worker", workerId, "last_first_token_at"),
|
||||||
|
key("worker", workerId, "first_token_count")
|
||||||
]).catch(() => []) : [];
|
]).catch(() => []) : [];
|
||||||
workers.push({
|
workers.push({
|
||||||
id: workerId,
|
id: workerId,
|
||||||
@@ -8814,6 +8828,9 @@ function createRuntimeRouter({
|
|||||||
containerPids: readNumber(values?.[15]),
|
containerPids: readNumber(values?.[15]),
|
||||||
containerHealth: values?.[16] ?? null,
|
containerHealth: values?.[16] ?? null,
|
||||||
metricsSampledAt: values?.[17] ? Number(values[17]) : null,
|
metricsSampledAt: values?.[17] ? Number(values[17]) : null,
|
||||||
|
lastFirstTokenMs: readNumber(values?.[18]),
|
||||||
|
lastFirstTokenAt: values?.[19] ? Number(values[19]) : null,
|
||||||
|
firstTokenCount: readNumber(values?.[20]),
|
||||||
score: workerScoreFromValues(values)
|
score: workerScoreFromValues(values)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -9344,6 +9361,10 @@ function createTkmindProxy({
|
|||||||
if (!runtimeRouter || !sessionId || !target) return;
|
if (!runtimeRouter || !sessionId || !target) return;
|
||||||
await runtimeRouter.streamEnded(sessionId, target, options).catch(() => null);
|
await runtimeRouter.streamEnded(sessionId, target, options).catch(() => null);
|
||||||
}
|
}
|
||||||
|
function markFirstTokenObserved(target, latencyMs) {
|
||||||
|
if (!runtimeRouter || !target) return;
|
||||||
|
void runtimeRouter.firstTokenObserved(target, latencyMs).catch(() => null);
|
||||||
|
}
|
||||||
async function getRuntimeStatus() {
|
async function getRuntimeStatus() {
|
||||||
const targetStatuses = [];
|
const targetStatuses = [];
|
||||||
for (const target of targets) {
|
for (const target of targets) {
|
||||||
@@ -9859,6 +9880,7 @@ function createTkmindProxy({
|
|||||||
];
|
];
|
||||||
const proxySessionEvents = async (req, res, sessionId, { onAfterFinish, onEvent } = {}) => {
|
const proxySessionEvents = async (req, res, sessionId, { onAfterFinish, onEvent } = {}) => {
|
||||||
const upstreamAbort = new AbortController();
|
const upstreamAbort = new AbortController();
|
||||||
|
const streamRequestedAt = Date.now();
|
||||||
let clientClosed = false;
|
let clientClosed = false;
|
||||||
const abortUpstream = () => {
|
const abortUpstream = () => {
|
||||||
clientClosed = true;
|
clientClosed = true;
|
||||||
@@ -9916,6 +9938,16 @@ function createTkmindProxy({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
const source = Readable.fromWeb(upstream.body);
|
const source = Readable.fromWeb(upstream.body);
|
||||||
|
let firstChunkSeen = false;
|
||||||
|
const firstTokenProbe = new Transform2({
|
||||||
|
transform(chunk, _encoding, callback) {
|
||||||
|
if (!firstChunkSeen && chunk?.length > 0) {
|
||||||
|
firstChunkSeen = true;
|
||||||
|
markFirstTokenObserved(sessionTarget, Date.now() - streamRequestedAt);
|
||||||
|
}
|
||||||
|
callback(null, chunk);
|
||||||
|
}
|
||||||
|
});
|
||||||
const linkSanitizer = createSessionEventSanitizer(req.currentUser, { onEvent });
|
const linkSanitizer = createSessionEventSanitizer(req.currentUser, { onEvent });
|
||||||
const waitForDrain = () => new Promise((resolve) => res.once("drain", resolve));
|
const waitForDrain = () => new Promise((resolve) => res.once("drain", resolve));
|
||||||
const writeClientChunk = async (chunk) => {
|
const writeClientChunk = async (chunk) => {
|
||||||
@@ -9945,7 +9977,7 @@ function createTkmindProxy({
|
|||||||
let streamCloseStatus = "closed";
|
let streamCloseStatus = "closed";
|
||||||
try {
|
try {
|
||||||
try {
|
try {
|
||||||
await pipeline(source, linkSanitizer, billingTransform, clientSink);
|
await pipeline(source, firstTokenProbe, linkSanitizer, billingTransform, clientSink);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
streamCloseStatus = clientClosed || upstreamAbort.signal.aborted ? "aborted" : "error";
|
streamCloseStatus = clientClosed || upstreamAbort.signal.aborted ? "aborted" : "error";
|
||||||
throw err;
|
throw err;
|
||||||
|
|||||||
@@ -155,6 +155,10 @@ function summarizeRuntime(runtimeJson) {
|
|||||||
streamOpenCount: worker.streamOpenCount,
|
streamOpenCount: worker.streamOpenCount,
|
||||||
streamAbortCount: worker.streamAbortCount,
|
streamAbortCount: worker.streamAbortCount,
|
||||||
streamErrorCount: worker.streamErrorCount,
|
streamErrorCount: worker.streamErrorCount,
|
||||||
|
ewmaFirstTokenMs: worker.ewmaFirstTokenMs,
|
||||||
|
lastFirstTokenMs: worker.lastFirstTokenMs,
|
||||||
|
lastFirstTokenAt: worker.lastFirstTokenAt,
|
||||||
|
firstTokenCount: worker.firstTokenCount,
|
||||||
cpuLoad: worker.cpuLoad,
|
cpuLoad: worker.cpuLoad,
|
||||||
memoryPressure: worker.memoryPressure,
|
memoryPressure: worker.memoryPressure,
|
||||||
fdPressure: worker.fdPressure,
|
fdPressure: worker.fdPressure,
|
||||||
@@ -213,6 +217,7 @@ for (const worker of runtimeSummary?.workers ?? []) {
|
|||||||
if (worker.containerHealth && worker.containerHealth !== 'healthy') failures.push(`${worker.id}_container_${worker.containerHealth}`);
|
if (worker.containerHealth && worker.containerHealth !== 'healthy') failures.push(`${worker.id}_container_${worker.containerHealth}`);
|
||||||
if (!worker.metricsFresh) failures.push(`${worker.id}_metrics_stale`);
|
if (!worker.metricsFresh) failures.push(`${worker.id}_metrics_stale`);
|
||||||
if (worker.streamErrorCount > 0) failures.push(`${worker.id}_stream_errors_${worker.streamErrorCount}`);
|
if (worker.streamErrorCount > 0) failures.push(`${worker.id}_stream_errors_${worker.streamErrorCount}`);
|
||||||
|
if (worker.firstTokenCount > 0 && worker.ewmaFirstTokenMs <= 0) failures.push(`${worker.id}_first_token_ewma_missing`);
|
||||||
}
|
}
|
||||||
if (runtimeSummary?.toolRuntime?.chatInjectsCodeTools !== false) failures.push('chat_injects_code_tools');
|
if (runtimeSummary?.toolRuntime?.chatInjectsCodeTools !== false) failures.push('chat_injects_code_tools');
|
||||||
|
|
||||||
|
|||||||
+42
-1
@@ -219,6 +219,25 @@ function createRuntimeRouter({
|
|||||||
if (streamKey) multi.set(streamKey, status, { EX: 600 });
|
if (streamKey) multi.set(streamKey, status, { EX: 600 });
|
||||||
await multi.exec().catch(() => null);
|
await multi.exec().catch(() => null);
|
||||||
},
|
},
|
||||||
|
async firstTokenObserved(target, latencyMs) {
|
||||||
|
const client = await getClient();
|
||||||
|
if (!client || !target) return;
|
||||||
|
const workerId = workerIdForTarget(target);
|
||||||
|
const sample = Math.max(0, Math.round(Number(latencyMs) || 0));
|
||||||
|
const ewmaKey = key('worker', workerId, 'ewma_first_token_ms');
|
||||||
|
const prev = readNumber(await client.get(ewmaKey).catch(() => null));
|
||||||
|
const next = prev > 0 ? Math.round(prev * 0.8 + sample * 0.2) : sample;
|
||||||
|
const now = String(Date.now());
|
||||||
|
await client
|
||||||
|
.multi()
|
||||||
|
.set(ewmaKey, String(next))
|
||||||
|
.set(key('worker', workerId, 'last_first_token_ms'), String(sample))
|
||||||
|
.set(key('worker', workerId, 'last_first_token_at'), now)
|
||||||
|
.incr(key('worker', workerId, 'first_token_count'))
|
||||||
|
.set(key('worker', workerId, 'heartbeat'), now, { EX: 30 })
|
||||||
|
.exec()
|
||||||
|
.catch(() => null);
|
||||||
|
},
|
||||||
async getStatus() {
|
async getStatus() {
|
||||||
const client = await getClient();
|
const client = await getClient();
|
||||||
const workers = [];
|
const workers = [];
|
||||||
@@ -245,6 +264,9 @@ function createRuntimeRouter({
|
|||||||
key('worker', workerId, 'container_pids'),
|
key('worker', workerId, 'container_pids'),
|
||||||
key('worker', workerId, 'container_health'),
|
key('worker', workerId, 'container_health'),
|
||||||
key('worker', workerId, 'metrics_sampled_at'),
|
key('worker', workerId, 'metrics_sampled_at'),
|
||||||
|
key('worker', workerId, 'last_first_token_ms'),
|
||||||
|
key('worker', workerId, 'last_first_token_at'),
|
||||||
|
key('worker', workerId, 'first_token_count'),
|
||||||
])
|
])
|
||||||
.catch(() => [])
|
.catch(() => [])
|
||||||
: [];
|
: [];
|
||||||
@@ -269,6 +291,9 @@ function createRuntimeRouter({
|
|||||||
containerPids: readNumber(values?.[15]),
|
containerPids: readNumber(values?.[15]),
|
||||||
containerHealth: values?.[16] ?? null,
|
containerHealth: values?.[16] ?? null,
|
||||||
metricsSampledAt: values?.[17] ? Number(values[17]) : null,
|
metricsSampledAt: values?.[17] ? Number(values[17]) : null,
|
||||||
|
lastFirstTokenMs: readNumber(values?.[18]),
|
||||||
|
lastFirstTokenAt: values?.[19] ? Number(values[19]) : null,
|
||||||
|
firstTokenCount: readNumber(values?.[20]),
|
||||||
score: workerScoreFromValues(values),
|
score: workerScoreFromValues(values),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -909,6 +934,11 @@ export function createTkmindProxy({
|
|||||||
await runtimeRouter.streamEnded(sessionId, target, options).catch(() => null);
|
await runtimeRouter.streamEnded(sessionId, target, options).catch(() => null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function markFirstTokenObserved(target, latencyMs) {
|
||||||
|
if (!runtimeRouter || !target) return;
|
||||||
|
void runtimeRouter.firstTokenObserved(target, latencyMs).catch(() => null);
|
||||||
|
}
|
||||||
|
|
||||||
async function getRuntimeStatus() {
|
async function getRuntimeStatus() {
|
||||||
const targetStatuses = [];
|
const targetStatuses = [];
|
||||||
for (const target of targets) {
|
for (const target of targets) {
|
||||||
@@ -1503,6 +1533,7 @@ export function createTkmindProxy({
|
|||||||
|
|
||||||
const proxySessionEvents = async (req, res, sessionId, { onAfterFinish, onEvent } = {}) => {
|
const proxySessionEvents = async (req, res, sessionId, { onAfterFinish, onEvent } = {}) => {
|
||||||
const upstreamAbort = new AbortController();
|
const upstreamAbort = new AbortController();
|
||||||
|
const streamRequestedAt = Date.now();
|
||||||
let clientClosed = false;
|
let clientClosed = false;
|
||||||
const abortUpstream = () => {
|
const abortUpstream = () => {
|
||||||
clientClosed = true;
|
clientClosed = true;
|
||||||
@@ -1564,6 +1595,16 @@ export function createTkmindProxy({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const source = Readable.fromWeb(upstream.body);
|
const source = Readable.fromWeb(upstream.body);
|
||||||
|
let firstChunkSeen = false;
|
||||||
|
const firstTokenProbe = new Transform({
|
||||||
|
transform(chunk, _encoding, callback) {
|
||||||
|
if (!firstChunkSeen && chunk?.length > 0) {
|
||||||
|
firstChunkSeen = true;
|
||||||
|
markFirstTokenObserved(sessionTarget, Date.now() - streamRequestedAt);
|
||||||
|
}
|
||||||
|
callback(null, chunk);
|
||||||
|
},
|
||||||
|
});
|
||||||
const linkSanitizer = createSessionEventSanitizer(req.currentUser, { onEvent });
|
const linkSanitizer = createSessionEventSanitizer(req.currentUser, { onEvent });
|
||||||
const waitForDrain = () => new Promise((resolve) => res.once('drain', resolve));
|
const waitForDrain = () => new Promise((resolve) => res.once('drain', resolve));
|
||||||
const writeClientChunk = async (chunk) => {
|
const writeClientChunk = async (chunk) => {
|
||||||
@@ -1595,7 +1636,7 @@ export function createTkmindProxy({
|
|||||||
let streamCloseStatus = 'closed';
|
let streamCloseStatus = 'closed';
|
||||||
try {
|
try {
|
||||||
try {
|
try {
|
||||||
await pipeline(source, linkSanitizer, billingTransform, clientSink);
|
await pipeline(source, firstTokenProbe, linkSanitizer, billingTransform, clientSink);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
streamCloseStatus = clientClosed || upstreamAbort.signal.aborted ? 'aborted' : 'error';
|
streamCloseStatus = clientClosed || upstreamAbort.signal.aborted ? 'aborted' : 'error';
|
||||||
throw err;
|
throw err;
|
||||||
|
|||||||
Reference in New Issue
Block a user