1use std::{cell::RefCell, collections::BTreeMap, time::Duration};
10
11use serde::Serialize;
12
13const PROFILE_SCHEMA: &str = "heddle-cli-profile/v1";
14
15#[derive(Clone, Copy, Debug, Eq, PartialEq)]
16pub enum ProfileMode {
17 Off,
18 Human,
19 Jsonl,
20}
21
22#[derive(Clone, Copy, Debug)]
23pub struct ProfileField {
24 pub name: &'static str,
25 pub value: u128,
26 unit: ProfileMetricUnit,
27}
28
29impl ProfileField {
30 pub fn millis(name: &'static str, value_ms: u128) -> Self {
31 Self {
32 name,
33 value: value_ms,
34 unit: ProfileMetricUnit::Milliseconds,
35 }
36 }
37
38 pub fn duration(name: &'static str, value: Duration) -> Self {
39 Self {
40 name,
41 value: value.as_millis(),
42 unit: ProfileMetricUnit::Milliseconds,
43 }
44 }
45
46 pub fn count(name: &'static str, value: impl Into<u128>) -> Self {
47 Self {
48 name,
49 value: value.into(),
50 unit: ProfileMetricUnit::Count,
51 }
52 }
53}
54
55#[derive(Clone, Copy, Debug, Serialize)]
56#[serde(rename_all = "snake_case")]
57pub enum ProfileMetricUnit {
58 Milliseconds,
59 Count,
60}
61
62#[derive(Clone, Debug, Serialize)]
63pub struct ProfileMetricValue {
64 value: u128,
65 unit: ProfileMetricUnit,
66}
67
68#[derive(Clone, Debug, Serialize)]
69pub struct ProfilePhaseRecord {
70 name: String,
71 metrics: BTreeMap<String, ProfileMetricValue>,
72}
73
74#[derive(Clone, Debug, Serialize)]
75pub struct ProfileTraceRecord {
76 schema: &'static str,
77 command: String,
78 exit_status: &'static str,
79 phases: Vec<ProfilePhaseRecord>,
80 totals: BTreeMap<String, ProfileMetricValue>,
81}
82
83thread_local! {
84 static JSONL_PHASES: RefCell<Vec<ProfilePhaseRecord>> = const { RefCell::new(Vec::new()) };
85}
86
87pub fn profile_mode() -> ProfileMode {
88 std::env::var("HEDDLE_PROFILE")
89 .map(|value| {
90 let normalized = value.trim().to_ascii_lowercase();
91 match normalized.as_str() {
92 "" | "0" | "false" | "no" | "off" => ProfileMode::Off,
93 "jsonl" => ProfileMode::Jsonl,
94 _ => ProfileMode::Human,
95 }
96 })
97 .unwrap_or(ProfileMode::Off)
98}
99
100pub fn profile_enabled() -> bool {
101 profile_mode() != ProfileMode::Off
102}
103
104pub fn emit_profile(command: &str, fields: &[ProfileField]) {
105 match profile_mode() {
106 ProfileMode::Off => {}
107 ProfileMode::Human => emit_human_profile(command, fields),
108 ProfileMode::Jsonl => record_phase(command, fields),
109 }
110}
111
112pub fn emit_command_profile(command: &str, exit_status: i32, totals: &[ProfileField]) {
113 match profile_mode() {
114 ProfileMode::Off => {}
115 ProfileMode::Human => emit_human_profile(command, totals),
116 ProfileMode::Jsonl => emit_jsonl_trace(command, exit_status, totals),
117 }
118}
119
120fn emit_human_profile(command: &str, fields: &[ProfileField]) {
121 eprintln!("heddle profile:");
122 eprintln!(" command: {command}");
123 for field in fields {
124 eprintln!(" {}: {}", field.name, field.value);
125 }
126}
127
128fn record_phase(command: &str, fields: &[ProfileField]) {
129 let phase = ProfilePhaseRecord {
130 name: command.to_string(),
131 metrics: fields_to_metrics(fields),
132 };
133 JSONL_PHASES.with(|phases| phases.borrow_mut().push(phase));
134}
135
136fn emit_jsonl_trace(command: &str, exit_status: i32, totals: &[ProfileField]) {
137 let phases = JSONL_PHASES.with(|records| std::mem::take(&mut *records.borrow_mut()));
138 let trace = ProfileTraceRecord {
139 schema: PROFILE_SCHEMA,
140 command: command.to_string(),
141 exit_status: if exit_status == 0 { "ok" } else { "error" },
142 phases,
143 totals: fields_to_metrics(totals),
144 };
145 match serde_json::to_string(&trace) {
146 Ok(line) => eprintln!("{line}"),
147 Err(err) => eprintln!("heddle profile jsonl error: {err}"),
148 }
149}
150
151fn fields_to_metrics(fields: &[ProfileField]) -> BTreeMap<String, ProfileMetricValue> {
152 fields
153 .iter()
154 .map(|field| {
155 (
156 field.name.to_string(),
157 ProfileMetricValue {
158 value: field.value,
159 unit: field.unit,
160 },
161 )
162 })
163 .collect()
164}