traceVersions
uses tcmts.cli.main.versionsOf, which uses tcmts.share.NameTrees, which uses tcmts.store.unpacked, which uses node:zlib
One definition (or a page or statement, by name) since since: each version it has had side by side, newest first
as history has them (the one its name points at now marked), each with how it ran and the history entry that made
it; each version beside the one before it (what changed, and how it runs for it, if that's news); its slowest
calls and newest errors; and where its calls came from in the browser, as the page waited for it. Undefined if
nothing's recorded for it.
import type { Codebase } from "#codebase/tcmts/cli/codebase"; import { changesBetween, versionsOf, versionVerdict } from "#codebase/tcmts/cli/main"; import { percentileOf, spanOf, statsOf } from "#codebase/tcmts/cli/traced"; import type { Of, Version } from "#codebase/tcmts/cli/traced"; import { shortHash } from "#codebase/tcmts/core/hash"; import type { DatabaseSync } from "node:sqlite"; export function traceVersions( db: DatabaseSync, cb: Codebase, name: string, since: number, ): | { byHash: boolean; versions: { hash?: string; now: boolean; n: number; errors: number; p50: number; p95: number; max: number; first: number; last: number; since?: Version; }[]; changes: { newer: string; older: string; changed: string[]; verdict?: { worse: boolean; said: string }; }[]; slowest: { trace: string; start: number; dur: number; hash: string | null }[]; errors: { trace: string; start: number; error: string; hash: string | null }[]; callers: { caller?: Of; n: number; errors: number; p50: number; p95: number; max: number }[]; } | undefined { let now: string | undefined; try { now = cb.lookupOne(name, "value"); } catch { // no longer a name of code, or never one: what was recorded under it is all there is } const recorded = [ ...new Set([ ...(now ? [now] : []), ...( db .prepare("SELECT DISTINCT hash FROM spans WHERE name = ? AND hash IS NOT NULL") .all(name) as { hash: string }[] ).map((h) => h.hash), ]), ]; const history = recorded.length ? versionsOf(cb, name) : []; const order = (h: string) => { const i = history.findIndex((v) => v.hash === h); return i < 0 ? history.length : i; }; const hashes = recorded.sort((a, b) => order(a) - order(b)); const groups = hashes.length ? hashes.map((h) => ({ hash: h as string | undefined, where: "hash = ? AND kind != 'client'", args: [h], })) : [{ hash: undefined, where: "hash IS NULL AND name = ?", args: [name] }]; const versions = []; const hit: typeof groups = []; const stats = new Map<string, ReturnType<typeof statsOf>>(); for (const g of groups) { const where = `${g.where} AND start >= ?`; const args = [...g.args, since]; stats.set(g.args[0]!, statsOf(db, where, args)); const s = db .prepare( `SELECT count(*) AS n, max(dur) AS max, sum(error IS NOT NULL) AS errors, min(start) AS first, max(start) AS last FROM spans WHERE ${where}`, ) .get(...args) as { n: number; max: number; errors: number; first: number; last: number }; if (!s.n) continue; hit.push(g); const v = g.hash ? history.find((x) => x.hash === g.hash) : undefined; versions.push({ ...(g.hash ? { hash: g.hash } : {}), now: !!g.hash && g.hash === now, n: s.n, errors: s.errors, p50: percentileOf(db, where, args, s.n, 0.5), p95: percentileOf(db, where, args, s.n, 0.95), max: s.max, first: s.first, last: s.last, ...(v ? { since: v } : {}), }); } if (!versions.length) return undefined; const changes = []; const seen = hashes.length ? hit.map((g) => g.args[0]!) : []; for (let i = 0; i + 1 < seen.length; i++) { const [newer, older] = [seen[i]!, seen[i + 1]!]; const verdict = versionVerdict(stats.get(older)!, stats.get(newer)!); changes.push({ newer, older, changed: changesBetween(cb, older, newer), ...(verdict ? { verdict } : {}), }); } const any = `(${hit.map((g) => g.where).join(" OR ")}) AND start >= ?`; const anyArgs = [...hit.flatMap((g) => g.args), since]; const slowest = db .prepare(`SELECT trace, start, dur, hash FROM spans WHERE ${any} ORDER BY dur DESC LIMIT 5`) .all(...anyArgs) as { trace: string; start: number; dur: number; hash: string | null }[]; const errors = db .prepare( `SELECT trace, start, error, hash FROM spans WHERE ${any} AND error IS NOT NULL ORDER BY start DESC LIMIT 5`, ) .all(...anyArgs) as { trace: string; start: number; error: string; hash: string | null }[]; const callers: { caller?: Of; n: number; errors: number; p50: number; p95: number; max: number; }[] = []; if (hashes.length) { const browser = `hash IN (${hashes.map(() => "?").join(", ")}) AND kind = 'client' AND start >= ?`; const fromOf = `json_extract(attrs, '$."tcm.from"')`; const rows = db .prepare( `SELECT ${fromOf} AS caller, count(*) AS n, max(dur) AS max, sum(error IS NOT NULL) AS errors FROM spans WHERE ${browser} GROUP BY caller ORDER BY n DESC LIMIT 10`, ) .all(...hashes, since) as { caller: string | null; n: number; max: number; errors: number }[]; for (const c of rows) { const where = `${browser} AND ${c.caller === null ? `${fromOf} IS NULL` : `${fromOf} = ?`}`; const args = [...hashes, since, ...(c.caller === null ? [] : [c.caller])]; callers.push({ ...(c.caller ? { caller: spanOf(cb, { hash: c.caller, name: shortHash(c.caller) }) } : {}), n: c.n, errors: c.errors, p50: percentileOf(db, where, args, c.n, 0.5), p95: percentileOf(db, where, args, c.n, 0.95), max: c.max, }); } } return { byHash: hashes.length > 0, versions, changes, slowest, errors, callers }; }
uses
- tcmts.cli.codebase.Codebase
- tcmts.cli.main.changesBetween
- tcmts.cli.main.versionVerdict
- tcmts.cli.main.versionsOf
- tcmts.cli.traced.Of
- tcmts.cli.traced.Version
- tcmts.cli.traced.percentileOf
- tcmts.cli.traced.spanOf
- tcmts.cli.traced.statsOf
- tcmts.core.hash.shortHash