clone

a personal forge on plain git: static pages, stacked-diff reviews, signed local ci, repo sites and releases

main
forge / lib / metrics.ts
metrics.ts 2.9K
1// Build metrics: where a build spends its time, so changes to the forge (and2// to a repo) can be compared. Each build appends one JSON line to3// $FORGE_STATE/metrics.jsonl; `forge metrics` summarizes them.4import fs from 'node:fs';5import path from 'node:path';6import { createHash } from 'node:crypto';7import { fileURLToPath } from 'node:url';89const round = (n: number): number => Math.round(n * 10) / 10;1011export class Spans {12 ms: Record<string, number> = {};13 counts: Record<string, number> = {};1415 time<T>(name: string, fn: () => T): T {16 const t = performance.now();17 try {18 return fn();19 } finally {20 this.ms[name] = round((this.ms[name] ?? 0) + performance.now() - t);21 }22 }2324 count(name: string, n = 1): void {25 this.counts[name] = (this.counts[name] ?? 0) + n;26 }27}2829let version: string | null = null;3031// A short hash of the generator's own code (lib/ and site/), so metrics and32// page fingerprints from different versions of the forge can be told apart33// without a git checkout.34export function codeVersion(): string {35 if (version) return version;36 const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');37 const h = createHash('sha256');38 const walk = (dir: string): void => {39 for (const e of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {40 const p = path.join(dir, e.name);41 if (e.isDirectory()) walk(p);42 else h.update(path.relative(root, p) + '\0').update(fs.readFileSync(p));43 }44 };45 for (const d of ['lib', 'site']) walk(path.join(root, d));46 h.update(fs.readFileSync(path.join(root, 'eleventy.config.ts')));47 version = h.digest('hex').slice(0, 10);48 return version;49}5051export interface RepoMetrics {52 slug: string;53 ms: Record<string, number>;54 counts: Record<string, number>;55 pages: Record<string, number>;56}5758export const build = {59 started: Number(process.env.FORGE_BUILD_STARTED) || Date.now(),60 reason: process.env.FORGE_BUILD_REASON || 'manual',61 dataStart: 0,62 dataEnd: 0,63 repos: [] as RepoMetrics[],64};6566export interface MetricsLine {67 at: string;68 code: string;69 reason: string;70 only: string | null;71 ms: { total: number; data: number; render: number };72 rss_mb: number;73 written?: number;74 linked?: number;75 repos: RepoMetrics[];76}7778export function record(file: string, extra: Partial<MetricsLine>): MetricsLine {79 const now = Date.now();80 const line: MetricsLine = {81 at: new Date(now).toISOString(),82 code: codeVersion(),83 reason: build.reason,84 only: process.env.FORGE_ONLY || null,85 ms: {86 total: now - build.started,87 data: round(build.dataEnd - build.dataStart),88 render: round(performance.now() - build.dataEnd),89 },90 rss_mb: Math.round(process.memoryUsage().rss / (1 << 20)),91 ...extra,92 repos: build.repos,93 };94 fs.mkdirSync(path.dirname(file), { recursive: true });95 fs.appendFileSync(file, JSON.stringify(line) + '\n');96 return line;97}