1use std::path::PathBuf;
15use std::time::Duration;
16
17use reqwest::StatusCode;
18
19#[derive(Debug, Clone)]
23pub struct PprofBundle {
24 pub dir: PathBuf,
25 pub profile_bytes: u64,
26 pub trace_bytes: u64,
27 pub seconds: u32,
28}
29
30impl PprofBundle {
31 pub fn summary(&self) -> String {
33 format!(
34 "pprof bundle ready · {}s sampling · profile {} · trace {} → {}",
35 self.seconds,
36 human_bytes(self.profile_bytes),
37 human_bytes(self.trace_bytes),
38 self.dir.display(),
39 )
40 }
41}
42
43pub async fn fetch_and_write(
47 base_url: &str,
48 auth_token: Option<String>,
49 seconds: u32,
50 dir: PathBuf,
51) -> Result<PprofBundle, String> {
52 std::fs::create_dir_all(&dir).map_err(|e| format!("mkdir {}: {e}", dir.display()))?;
53 let client = reqwest::Client::builder()
56 .timeout(Duration::from_secs(u64::from(seconds) + 30))
57 .build()
58 .map_err(|e| format!("client build: {e}"))?;
59
60 let base = base_url.trim_end_matches('/').to_string();
61 let profile_url = format!("{base}/debug/pprof/profile?seconds={seconds}");
62 let trace_url = format!("{base}/debug/pprof/trace?seconds={seconds}");
63
64 let (profile, trace) = tokio::join!(
65 fetch_one(&client, &profile_url, auth_token.as_deref()),
66 fetch_one(&client, &trace_url, auth_token.as_deref()),
67 );
68
69 let profile_bytes = profile.map_err(|e| format!("profile: {e}"))?;
70 let trace_bytes = trace.map_err(|e| format!("trace: {e}"))?;
71
72 let profile_path = dir.join("profile.pprof");
73 let trace_path = dir.join("trace.pprof");
74 std::fs::write(&profile_path, &profile_bytes).map_err(|e| format!("write profile: {e}"))?;
75 std::fs::write(&trace_path, &trace_bytes).map_err(|e| format!("write trace: {e}"))?;
76
77 Ok(PprofBundle {
78 dir,
79 profile_bytes: profile_bytes.len() as u64,
80 trace_bytes: trace_bytes.len() as u64,
81 seconds,
82 })
83}
84
85async fn fetch_one(
86 client: &reqwest::Client,
87 url: &str,
88 auth: Option<&str>,
89) -> Result<Vec<u8>, String> {
90 let mut req = client.get(url);
91 if let Some(token) = auth {
92 req = req.bearer_auth(token);
93 }
94 let resp = req.send().await.map_err(|e| format!("GET {url}: {e}"))?;
95 let status = resp.status();
96 if status == StatusCode::NOT_FOUND {
97 return Err("404 — Bee's debug API isn't enabled. Add `--debug-api-enable=true` to your Bee start args (or set `debug-api-enable: true` in bee.yaml) and restart.".to_string());
98 }
99 if !status.is_success() {
100 return Err(format!("{url} returned HTTP {status}"));
101 }
102 let bytes = resp
103 .bytes()
104 .await
105 .map_err(|e| format!("read body of {url}: {e}"))?;
106 Ok(bytes.to_vec())
107}
108
109fn human_bytes(n: u64) -> String {
110 const KIB: u64 = 1 << 10;
111 const MIB: u64 = 1 << 20;
112 if n >= MIB {
113 format!("{:.1} MiB", n as f64 / MIB as f64)
114 } else if n >= KIB {
115 format!("{:.1} KiB", n as f64 / KIB as f64)
116 } else {
117 format!("{n} B")
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 #[test]
126 fn human_bytes_picks_unit() {
127 assert_eq!(human_bytes(0), "0 B");
128 assert_eq!(human_bytes(1023), "1023 B");
129 assert_eq!(human_bytes(2048), "2.0 KiB");
130 assert_eq!(human_bytes(2 * 1024 * 1024), "2.0 MiB");
131 }
132
133 #[test]
134 fn summary_line_includes_dir_and_byte_counts() {
135 let b = PprofBundle {
136 dir: PathBuf::from("/tmp/bee-tui-diagnostic-1234"),
137 profile_bytes: 12_345,
138 trace_bytes: 6_789,
139 seconds: 60,
140 };
141 let s = b.summary();
142 assert!(s.contains("60s sampling"));
143 assert!(s.contains("/tmp/bee-tui-diagnostic-1234"));
144 assert!(s.contains("KiB"));
145 }
146}