Skip to main content

soak/
soak.rs

1//! Memory soak test: does repeating what a front-end does every few seconds make the
2//! process grow? Each round re-reads the logs, rebuilds the report, the charts and
3//! the drill-down (with and without a selection) — exactly the refresh path of the
4//! TUI, GUI and web — and prints the resident memory every few rounds.
5//!
6//!     cargo run --release -p tokenburn-core --example soak -- [rounds] [month|all]
7//!
8//! A leak shows as a steady climb; a healthy run flattens out after the first rounds.
9
10use std::process::Command;
11
12use chrono::Local;
13use tokenburn_core::{
14    build_report, by_project, collect, drill, pie, time_series, Query, Selection, ToolFilter,
15    Window,
16};
17
18fn rss_mb() -> f64 {
19    let out = Command::new("ps")
20        .args(["-o", "rss=", "-p", &std::process::id().to_string()])
21        .output()
22        .expect("ps");
23    String::from_utf8_lossy(&out.stdout)
24        .trim()
25        .parse::<f64>()
26        .unwrap_or(0.0)
27        / 1024.0
28}
29
30fn main() {
31    let mut args = std::env::args().skip(1);
32    let rounds: usize = args.next().and_then(|n| n.parse().ok()).unwrap_or(100);
33    let window = match args.next().as_deref() {
34        Some("all") => Window::All,
35        _ => Window::Month,
36    };
37    let query = Query {
38        bucket: tokenburn_core::Bucket::Day,
39        window,
40        ..Query::default()
41    };
42
43    println!("round   rows   rss MB");
44    let mut first = 0.0;
45    for i in 0..rounds {
46        let collected = collect(ToolFilter::All, window.start(Local::now()));
47        let rows = collected.rows;
48        let _ = build_report(&rows, &query);
49        let _ = time_series(&rows, &query);
50        let _ = pie(&by_project(&rows), 8);
51        let all = drill(&rows, &query, &Selection::default());
52        // Click a bar and a slice, as a user would.
53        let mut sel = Selection::default();
54        if let Some(l) = all.series.labels.get(3) {
55            sel.toggle_bucket(l);
56        }
57        if let Some(k) = all.pie.slices.first().and_then(|s| s.key.clone()) {
58            sel.toggle_project(&k);
59        }
60        let _ = drill(&rows, &query, &sel);
61        if i == 0 || i % (rounds / 10).max(1) == 0 || i == rounds - 1 {
62            let rss = rss_mb();
63            if i == 0 {
64                first = rss;
65            }
66            println!("{i:>5}  {:>6}  {rss:>7.1}", rows.len());
67        }
68    }
69    println!("first round {first:.1} MB → last round {:.1} MB", rss_mb());
70}