1#![allow(clippy::print_stdout)]
2#[macro_export]
3#[doc(hidden)]
4macro_rules! profile_function {
5 () => {
6 #[cfg(feature = "profile")]
7 let _scope = $crate::profile::enabled::Scope::new(
8 $crate::profile::enabled::current_function_name!(),
9 );
10 };
11}
12
13#[macro_export]
14#[doc(hidden)]
15macro_rules! profile_scope {
16 ($label:expr) => {
17 #[cfg(feature = "profile")]
18 let _scope = $crate::profile::enabled::Scope::new($label);
19 };
20}
21
22pub fn run_profile() {
23 #[cfg(feature = "profile")]
24 enabled::run_profile();
25}
26
27#[cfg(feature = "profile")]
28pub(crate) mod enabled {
29 use std::{
30 collections::HashMap,
31 fs,
32 io::{Write, stdout},
33 sync::OnceLock,
34 time::Instant,
35 };
36
37 use crossbeam_channel::{Receiver, Sender};
38 use indexmap::IndexMap;
39 use serde::{Deserialize, Serialize};
40
41 use crate::Uiua;
42
43 const BENCHMARKS: &[(&str, &str)] = &[
44 ("PRIMES", "▽¬∊♭˙⊞×..+2⇡1000"),
45 (
46 "STRIPES",
47 "\
48[⊃⊃⊞+⊞↥⊞-].⇡300
49⍉ ÷2 +1.2 ∿ ÷10",
50 ),
51 (
52 "AUTOMATA",
53 "\
54Rule ← ˜⊏⊓⋯₈(°⋯⧈⇌3⊂⊂⊃⊣⟜⊢)
55=⌊⊃÷₂⇡ 500 # Init
56⍥⟜⊸Rule ⌊÷2◡⋅⧻ 30 # Run",
57 ),
58 (
59 "MANDELBROT",
60 "\
61×2 ⊞ℂ⤙-1/4 -1/2÷⟜⇡300 # Init
62>2⌵ ⍥⟜⊸(+⊙°√) 50 . # Run
63÷⧻⟜/+ # Normalize",
64 ),
65 (
66 "CHORD",
67 "\
68[0 4 7 10] # Notes
69×220 ˜ⁿ2÷12 # Freqs
70∿×τ ⊞× ÷⟜⇡&asr # Generate
71÷⧻⟜/+⍉ # Mix",
72 ),
73 (
74 "LOGO",
75 "\
76U ← /=⊞<0.2_0.7 /+×⟜ⁿ1_2
77I ← <⊙(⌵/ℂ) # Circle
78u ← +0.1↧¤ ⊃(I0.95|⊂⊙0.5⇌°√)
79A ← ×⊃U(I1) # Alpha
80⍜°⍉(⊂⊃u A) ⊞⊟.-1×2÷⟜⇡200",
81 ),
82 (
83 "LIFE",
84 "\
85Life ← ↥∩=₃⟜+⊸(/+↻⊂A₂C₂)
86⁅×0.6 gen⊙⚂ ⊟.30 # Init
87⍥⊸Life100 # Run
88≡(▽⟜≡▽) 4 # Upscale",
89 ),
90 (
91 "SPIRAL",
92 "\
93↯⟜(×20-⊸¬÷⟜⇡)200 # Xs
94-⊃∠(⌵ℂ)⊸⍉ # Spiral field
95-π◿τ⊞-×τ÷⟜⇡20 # Animate",
96 ),
97 ];
98
99 const RUNS: usize = 20;
100
101 pub fn run_profile() {
102 if cfg!(debug_assertions) {
103 eprintln!("Profiling must be done in release mode");
104 return;
105 }
106
107 const WARMUP_RUNS: usize = 3;
108 for i in 0..WARMUP_RUNS {
109 print!("\rProfiling... warmup {}/{}", i + 1, WARMUP_RUNS);
110 stdout().flush().unwrap();
111 for (_, bench) in BENCHMARKS {
112 Uiua::with_native_sys().run_str(bench).unwrap();
113 }
114 }
115
116 init_profiler();
117
118 for i in 0..RUNS {
119 profile_scope!("run");
120 print!("\rProfiling... run {}/{} ", i + 1, RUNS);
121 stdout().flush().unwrap();
122 for (name, bench) in BENCHMARKS {
123 profile_scope!(name);
124 Uiua::with_native_sys().run_str(bench).unwrap();
125 }
126 }
127
128 println!("\rProfiling complete ");
129 end_profiler();
130 }
131
132 #[inline(always)]
133 pub fn type_name_of<T>(_: T) -> &'static str {
134 std::any::type_name::<T>()
135 }
136 #[inline]
137 pub fn clean_function_name(name: &str) -> &str {
138 if let Some(colon) = name.rfind("::") {
139 if let Some(colon) = name[..colon].rfind("::") {
140 &name[colon + 2..]
142 } else {
143 name
145 }
146 } else {
147 name
148 }
149 }
150
151 #[macro_export]
153 macro_rules! current_function_name {
154 () => {{
155 fn f() {}
156 let name = $crate::profile::enabled::type_name_of(f);
157 let name = &name.get(..name.len() - 3).unwrap();
159 $crate::profile::enabled::clean_function_name(name)
160 }};
161 }
162 pub(crate) use current_function_name;
163 pub struct Scope {
164 name: &'static str,
165 start: Instant,
166 }
167
168 impl Scope {
169 pub fn new(name: &'static str) -> Self {
170 Scope {
171 name,
172 start: Instant::now(),
173 }
174 }
175 }
176
177 struct FinishedScope {
178 name: &'static str,
179 dur: f64,
180 }
181
182 impl Drop for Scope {
183 fn drop(&mut self) {
184 let end = Instant::now();
185 let finished = FinishedScope {
186 name: self.name,
187 dur: (end - self.start).as_secs_f64(),
188 };
189 if let Some(send) = SEND.get() {
190 send.send(finished).unwrap();
191 }
192 }
193 }
194
195 static SEND: OnceLock<Sender<FinishedScope>> = OnceLock::new();
196 static RECV: OnceLock<Receiver<FinishedScope>> = OnceLock::new();
197
198 fn init_profiler() {
199 let (send, recv) = crossbeam_channel::unbounded();
200 SEND.set(send).unwrap();
201 RECV.set(recv).unwrap();
202 }
203
204 fn end_profiler() {
205 let mut times = HashMap::new();
206 for scope in RECV.get().unwrap().try_iter() {
207 times
208 .entry(scope.name)
209 .or_insert(Vec::new())
210 .push(scope.dur);
211 }
212 if times.is_empty() {
213 return;
214 }
215 let max_total_dur = times
216 .values()
217 .map(|v| v.iter().sum::<f64>())
218 .max_by(|a, b| a.partial_cmp(b).unwrap())
219 .unwrap();
220
221 #[derive(Serialize, Deserialize)]
222 struct Profile {
223 avg_total_dur: f64,
224 entries: IndexMap<String, Entry>,
225 }
226
227 #[derive(Serialize, Deserialize)]
228 struct Entry {
229 share: f64,
230 median_dur: f64,
231 mean_dur: f64,
232 max_dur: f64,
233 min_dur: f64,
234 total_dur: f64,
235 count: usize,
236 }
237
238 let mut entries = IndexMap::new();
239 for (name, mut times) in times {
240 times.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap());
241 let total_dur = times.iter().sum::<f64>();
242 let count = times.len();
243 entries.insert(
244 name.to_string(),
245 Entry {
246 share: total_dur / max_total_dur,
247 total_dur,
248 max_dur: times[count - 1],
249 min_dur: times[0],
250 count,
251 mean_dur: total_dur / count as f64,
252 median_dur: times[count / 2],
253 },
254 );
255 }
256 entries.sort_by(|_, a, _, b| a.share.partial_cmp(&b.share).unwrap().reverse());
257
258 let profile = Profile {
259 avg_total_dur: max_total_dur / RUNS as f64,
260 entries,
261 };
262
263 if let Some(previous) = fs::read("profile.yaml")
264 .ok()
265 .and_then(|bytes| serde_yaml::from_slice::<Profile>(&bytes).ok())
266 {
267 let avg_total_dur_change =
269 percent_change(previous.avg_total_dur, profile.avg_total_dur) * 100.0;
270 println!("{:<26} | {:+.0}%", "Total duration", avg_total_dur_change);
271
272 println!("{}", "-".repeat(56));
274 println!(
275 "{:<26} | {:>7} {:>5} | {:>5} {:>5} |",
276 "scope", "median", "%Δ", "share", "%Δ"
277 );
278 println!("{}", "-".repeat(56));
279 for (name, new_entry) in profile.entries.iter().skip(1) {
280 let median_dur = format!("{:.4}", new_entry.median_dur);
281 let share = format!("{:.1}%", new_entry.share * 100.0);
282 let mut media_dur_change = String::new();
283 let mut share_change = String::new();
284 if let Some(previous_entry) = previous.entries.get(name) {
285 media_dur_change = format!(
286 "{:+.0}%",
287 percent_change(previous_entry.median_dur, new_entry.median_dur) * 100.0
288 );
289 share_change = format!(
290 "{:+.0}%",
291 percent_change(previous_entry.share, new_entry.share) * 100.0
292 );
293 if ["+0%", "-0%"].contains(&media_dur_change.as_str()) {
294 media_dur_change.clear();
295 }
296 if ["+0%", "-0%"].contains(&share_change.as_str()) {
297 share_change.clear();
298 }
299 }
300 println!(
301 "{name:<26} | {median_dur:>7} {media_dur_change:>5} | {share:>5} {share_change:>5} |"
302 );
303 }
304 }
305
306 fs::write("profile.yaml", serde_yaml::to_string(&profile).unwrap()).unwrap();
307 }
308
309 fn percent_change(a: f64, b: f64) -> f64 {
310 if b > a { b / a - 1.0 } else { 1.0 - a / b }
311 }
312}