Files
dsh_ai1net_server/dsh-server-docs/07-scripts/analyze-turn.mjs
T

71 lines
3.3 KiB
JavaScript
Raw Normal View History

/**
* 会话每轮指标:TTFT(turn/start → 首个流式 chunk)、轮时长、工具耗时
* 只输出时间与类型,不输出任何正文。
* 用法:node analyze-turn.mjs <session.jsonl.zstd>
*/
import { readFileSync } from 'node:fs'
import { zstdDecompressSync } from 'node:zlib'
const raw = readFileSync(process.argv[2])
const MAGIC = Buffer.from([0x28, 0xb5, 0x2f, 0xfd])
const offs = []
for (let i = 0; i + 4 <= raw.length; i++) if (raw.compare(MAGIC, 0, 4, i, i + 4) === 0) offs.push(i)
let text = ''
const frames = offs.length > 0 ? offs : [0]
for (let f = 0; f < frames.length; f++) {
const s = frames[f], e = f + 1 < frames.length ? frames[f + 1] : raw.length
try { text += zstdDecompressSync(raw.subarray(s, e)).toString('utf8') } catch {}
}
if (text === '') { try { text = zstdDecompressSync(raw).toString('utf8') } catch (e) { console.log('fail', e.message); process.exit(1) } }
const pick = (o, keys, d = 0) => {
if (o === null || typeof o !== 'object' || d > 3) return undefined
for (const k of keys) if (typeof o[k] !== 'undefined') return o[k]
for (const v of Object.values(o)) { const h = pick(v, keys, d + 1); if (h !== undefined) return h }
return undefined
}
const ev = []
for (const line of text.split('\n')) {
if (line.trim() === '') continue
let o; try { o = JSON.parse(line) } catch { continue }
const kind = pick(o, ['kind', 'type', 'event'])
const ts = pick(o, ['ts', 'time', 'timestamp', 'at', 'createdAt'])
const t = typeof ts === 'number' ? (ts < 1e12 ? ts * 1000 : ts) : typeof ts === 'string' ? Date.parse(ts) : NaN
if (typeof kind === 'string') ev.push({ k: kind, t: Number.isNaN(t) ? undefined : t })
}
const fmt = (ms) => new Date(ms).toISOString().slice(11, 19) + 'Z'
let turns = 0
for (let i = 0; i < ev.length; i++) {
if (ev[i].k !== 'turn/start') continue
turns++
const t0 = ev[i].t
let end, firstChunk, firstText
for (let j = i + 1; j < ev.length; j++) {
if (ev[j].k === 'turn/end' && end === undefined) { end = ev[j].t; break }
if (firstChunk === undefined && (ev[j].k === 'assistant/chunk' || ev[j].k === 'reasoning-chunks') && ev[j].t !== undefined) firstChunk = ev[j].t
if (firstText === undefined && ev[j].k === 'text-chunks' && ev[j].t !== undefined) firstText = ev[j].t
}
const ttft = t0 !== undefined && firstChunk !== undefined ? ((firstChunk - t0) / 1000).toFixed(1) : '?'
const ttftText = t0 !== undefined && firstText !== undefined ? ((firstText - t0) / 1000).toFixed(1) : '?'
const dur = t0 !== undefined && end !== undefined ? ((end - t0) / 1000).toFixed(1) : '?'
console.log(`轮 ${String(turns).padStart(2)} ${t0 !== undefined ? fmt(t0) : '?'} TTFT=${ttft}s 首文本=${ttftText}s 轮时长=${dur}s`)
}
// 工具耗时
const tools = ev.map((e, i) => ({ e, i })).filter((x) => x.e.k === 'tool/call')
const dt = []
for (const { i } of tools) {
for (let j = i + 1; j < ev.length; j++) {
if (ev[j].k === 'tool/result') {
if (ev[i].t !== undefined && ev[j].t !== undefined) dt.push((ev[j].t - ev[i].t) / 1000)
break
}
}
}
if (dt.length > 0) {
dt.sort((a, b) => b - a)
const avg = dt.reduce((a, b) => a + b, 0) / dt.length
console.log(`\n工具调用 ${dt.length} 次;平均 ${avg.toFixed(1)}s;最慢 5 个: ${dt.slice(0, 5).map((x) => x.toFixed(1) + 's').join(', ')}`)
}
console.log(`\n合计轮数: ${turns}`)