Skip to main content

uiua/
profile.rs

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                // "foo::bar::baz::function_name" -> "baz::function_name"
141                &name[colon + 2..]
142            } else {
143                // "foo::function_name" -> "foo::function_name"
144                name
145            }
146        } else {
147            name
148        }
149    }
150
151    /// Returns the name of the calling function without a long module path prefix.
152    #[macro_export]
153    macro_rules! current_function_name {
154        () => {{
155            fn f() {}
156            let name = $crate::profile::enabled::type_name_of(f);
157            // Remove "::f" from the name:
158            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            // Average total duration
268            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            // Scopes
273            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}