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}