bob/stats.rs
1/*
2 * Copyright (c) 2025 Jonathan Perkin <jonathan@perkin.org.uk>
3 *
4 * Permission to use, copy, modify, and distribute this software for any
5 * purpose with or without fee is hereby granted, provided that the above
6 * copyright notice and this permission notice appear in all copies.
7 *
8 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15 */
16
17//! Statistics collection for performance analysis.
18//!
19//! Writes events to a JSONL (JSON Lines) file for later analysis.
20//! Each line is a self-contained JSON object, making the file easy
21//! to process with tools like `jq`.
22//!
23//! # Example
24//!
25//! ```sh
26//! # Find slowest scans
27//! jq -s 'map(select(.event == "scan")) | sort_by(.duration_ms) | reverse | .[0:10]' stats.jsonl
28//!
29//! # Average build time
30//! jq -s '[.[] | select(.event == "build") | .duration_ms] | add / length' stats.jsonl
31//! ```
32
33use serde::Serialize;
34use std::fs::File;
35use std::io::{BufWriter, Write};
36use std::path::Path;
37use std::sync::Mutex;
38use std::time::Duration;
39
40/// A thread-safe JSONL stats writer.
41pub struct Stats {
42 writer: Mutex<BufWriter<File>>,
43}
44
45/// Events that can be recorded to the stats file.
46#[derive(Serialize)]
47#[serde(tag = "event")]
48pub enum Event<'a> {
49 #[serde(rename = "scan")]
50 Scan { pkgpath: &'a str, duration_ms: u64, success: bool },
51 #[serde(rename = "resolve")]
52 Resolve { buildable: usize, skipped: usize, duration_ms: u64 },
53 #[serde(rename = "build")]
54 Build {
55 pkgname: &'a str,
56 pkgpath: Option<&'a str>,
57 duration_ms: u64,
58 outcome: &'a str,
59 },
60}
61
62impl Stats {
63 /// Create a new stats writer that writes to the given path.
64 pub fn new(path: &Path) -> anyhow::Result<Self> {
65 let file = File::create(path)?;
66 Ok(Self { writer: Mutex::new(BufWriter::new(file)) })
67 }
68
69 /// Record an event to the stats file.
70 pub fn record(&self, event: Event) {
71 if let Ok(mut writer) = self.writer.lock() {
72 if let Ok(json) = serde_json::to_string(&event) {
73 let _ = writeln!(writer, "{}", json);
74 }
75 }
76 }
77
78 /// Record a package scan event.
79 pub fn scan(&self, pkgpath: &str, duration: Duration, success: bool) {
80 self.record(Event::Scan {
81 pkgpath,
82 duration_ms: duration.as_millis() as u64,
83 success,
84 });
85 }
86
87 /// Record a dependency resolution event.
88 pub fn resolve(
89 &self,
90 buildable: usize,
91 skipped: usize,
92 duration: Duration,
93 ) {
94 self.record(Event::Resolve {
95 buildable,
96 skipped,
97 duration_ms: duration.as_millis() as u64,
98 });
99 }
100
101 /// Record a package build event.
102 pub fn build(
103 &self,
104 pkgname: &str,
105 pkgpath: Option<&str>,
106 duration: Duration,
107 outcome: &str,
108 ) {
109 self.record(Event::Build {
110 pkgname,
111 pkgpath,
112 duration_ms: duration.as_millis() as u64,
113 outcome,
114 });
115 }
116
117 /// Flush any buffered data to disk.
118 pub fn flush(&self) {
119 if let Ok(mut writer) = self.writer.lock() {
120 let _ = writer.flush();
121 }
122 }
123}
124
125impl Drop for Stats {
126 fn drop(&mut self) {
127 self.flush();
128 }
129}