inferno/flamegraph/color/
mod.rs

1//! Color palettes and options for flame graph generation.
2
3mod palette_map;
4mod palettes;
5
6use std::borrow::Cow;
7use std::cmp::Ordering;
8use std::fmt;
9use std::str::FromStr;
10
11use rgb::RGB8;
12
13pub use self::palette_map::PaletteMap;
14
15/// A re-export of `RGB8` from the [`rgb` crate](https://docs.rs/rgb).
16pub type Color = RGB8;
17
18pub(super) const VDGREY: Color = Color {
19    r: 160,
20    g: 160,
21    b: 160,
22};
23pub(super) const DGREY: Color = Color {
24    r: 200,
25    g: 200,
26    b: 200,
27};
28
29const YELLOW_GRADIENT: (&str, &str) = ("#eeeeee", "#eeeeb0");
30const BLUE_GRADIENT: (&str, &str) = ("#eeeeee", "#e0e0ff");
31const GREEN_GRADIENT: (&str, &str) = ("#eef2ee", "#e0ffe0");
32const GRAY_GRADIENT: (&str, &str) = ("#f8f8f8", "#e8e8e8");
33
34/// A flame graph background color.
35///
36/// The default background color usually depends on the color scheme:
37///
38///  - [`BasicPalette::Mem`] defaults to [`BackgroundColor::Green`].
39///  - [`BasicPalette::Io`] and [`MultiPalette::Wakeup`] default to [`BackgroundColor::Blue`].
40///  - [`BasicPalette::Hot`] defaults to [`BackgroundColor::Yellow`].
41///  - All other [`BasicPalette`] variants default to [`BackgroundColor::Grey`].
42///  - All other [`MultiPalette`] variants default to [`BackgroundColor::Yellow`].
43///
44/// `BackgroundColor::default()` is `Yellow`.
45#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
46pub enum BackgroundColor {
47    /// A yellow gradient from `#EEEEEE` to `#EEEEB0`.
48    #[default]
49    Yellow,
50    /// A blue gradient from `#EEEEEE` to `#E0E0FF`.
51    Blue,
52    /// A green gradient from `#EEF2EE` to `#E0FFE0`.
53    Green,
54    /// A grey gradient from `#F8F8F8` to `#E8E8E8`.
55    Grey,
56    /// A flag background color with the given RGB components.
57    ///
58    /// Expressed in string form as `#RRGGBB` where each component is written in hexadecimal.
59    Flat(Color),
60}
61
62/// A flame graph color palette.
63///
64/// Defaults to [`BasicPalette::Hot`].
65#[derive(Clone, Copy, Debug, PartialEq, Eq)]
66pub enum Palette {
67    /// A plain color palette in which the color is not chosen based on function semantics.
68    ///
69    /// See [`BasicPalette`] for details.
70    Basic(BasicPalette),
71    /// A semantic color palette in which different hues are used to signify semantic aspects of
72    /// different function names (kernel functions, JIT functions, etc.).
73    Multi(MultiPalette),
74}
75
76impl Palette {
77    /// The valid set of palettes (via `FromStr`).
78    pub const VARIANTS: &'static [&'static str] = &[
79        "aqua", "blue", "green", "hot", "io", "java", "js", "mem", "orange", "perl", "python",
80        "purple", "red", "rust", "wakeup", "yellow",
81    ];
82}
83
84impl Default for Palette {
85    fn default() -> Self {
86        Palette::Basic(BasicPalette::Hot)
87    }
88}
89
90/// A plain color palette in which the color is not chosen based on function semantics.
91///
92/// Exactly how the color is chosen depends on a number of other configuration options like
93/// [`super::Options.consistent_palette`] and [`super::Options.hash`]. In the absence of options
94/// like that, these palettes all choose colors randomly from the indicated spectrum, and does not
95/// consider the name of the frame's function when doing so.
96#[derive(Clone, Copy, Debug, PartialEq, Eq)]
97pub enum BasicPalette {
98    /// A palette in which colors are chosen from a red-yellow spectrum.
99    Hot,
100    /// A palette in which colors are chosen from a green-blue spectrum.
101    Mem,
102    /// A palette in which colors are chosen from a wide blue spectrum.
103    Io,
104    /// A palette in which colors are chosen from a red spectrum.
105    Red,
106    /// A palette in which colors are chosen from a green spectrum.
107    Green,
108    /// A palette in which colors are chosen from a blue spectrum.
109    Blue,
110    /// A palette in which colors are chosen from an aqua-tinted spectrum.
111    Aqua,
112    /// A palette in which colors are chosen from a yellow spectrum.
113    Yellow,
114    /// A palette in which colors are chosen from a purple spectrum.
115    Purple,
116    /// A palette in which colors are chosen from a orange spectrum.
117    Orange,
118}
119
120/// A semantic color palette in which different hues are used to signify semantic aspects of
121/// different function names (kernel functions, JIT functions, etc.).
122#[derive(Clone, Copy, Debug, PartialEq, Eq)]
123pub enum MultiPalette {
124    /// Use Java semantics to color frames.
125    Java,
126    /// Use JavaScript semantics to color frames.
127    Js,
128    /// Use Perl semantics to color frames.
129    Perl,
130    /// Use Python semantics to color frames.
131    Python,
132    /// Use Rust semantics to color frames.
133    Rust,
134    /// Equivalent to [`BasicPalette::Aqua`] with [`BackgroundColor::Blue`].
135    Wakeup,
136}
137
138impl FromStr for BackgroundColor {
139    type Err = String;
140
141    fn from_str(s: &str) -> Result<Self, Self::Err> {
142        match s {
143            "yellow" => Ok(BackgroundColor::Yellow),
144            "blue" => Ok(BackgroundColor::Blue),
145            "green" => Ok(BackgroundColor::Green),
146            "grey" => Ok(BackgroundColor::Grey),
147            flat => parse_hex_color(flat)
148                .map(BackgroundColor::Flat)
149                .ok_or_else(|| format!("unknown background color: {}", flat)),
150        }
151    }
152}
153
154macro_rules! u8_from_hex_iter {
155    ($slice:expr) => {
156        (($slice.next()?.to_digit(16)? as u8) << 4) | ($slice.next()?.to_digit(16)? as u8)
157    };
158}
159
160/// Parses a string as a hex color, returning None if it is an invalid color
161pub fn parse_hex_color(s: &str) -> Option<Color> {
162    if !s.starts_with('#') || (s.len() != 7) {
163        None
164    } else {
165        let mut s = s[1..].chars();
166
167        let r = u8_from_hex_iter!(s);
168        let g = u8_from_hex_iter!(s);
169        let b = u8_from_hex_iter!(s);
170
171        Some(Color { r, g, b })
172    }
173}
174
175/// `SearchColor::default()` is `rgb(230,0,230)`.
176#[derive(Clone, Copy, Debug, PartialEq, Eq)]
177pub struct SearchColor(Color);
178
179impl FromStr for SearchColor {
180    type Err = String;
181
182    fn from_str(s: &str) -> Result<Self, Self::Err> {
183        parse_hex_color(s)
184            .map(SearchColor)
185            .ok_or_else(|| format!("unknown color: {}", s))
186    }
187}
188
189impl fmt::Display for SearchColor {
190    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
191        write!(f, "rgb({},{},{})", self.0.r, self.0.g, self.0.b)
192    }
193}
194
195/// `StrokeColor::default()` is `None`.
196#[derive(Clone, Copy, Debug, PartialEq, Eq)]
197pub enum StrokeColor {
198    /// Color of the stroke
199    Color(Color),
200    /// No color for the stroke
201    None,
202}
203
204impl FromStr for StrokeColor {
205    type Err = String;
206
207    fn from_str(s: &str) -> Result<Self, Self::Err> {
208        if s == "none" {
209            return Ok(StrokeColor::None);
210        }
211        parse_hex_color(s)
212            .map(StrokeColor::Color)
213            .ok_or_else(|| format!("unknown color: {}", s))
214    }
215}
216
217impl FromStr for Palette {
218    type Err = String;
219
220    fn from_str(s: &str) -> Result<Self, Self::Err> {
221        match s {
222            "hot" => Ok(Palette::Basic(BasicPalette::Hot)),
223            "mem" => Ok(Palette::Basic(BasicPalette::Mem)),
224            "io" => Ok(Palette::Basic(BasicPalette::Io)),
225            "wakeup" => Ok(Palette::Multi(MultiPalette::Wakeup)),
226            "java" => Ok(Palette::Multi(MultiPalette::Java)),
227            "js" => Ok(Palette::Multi(MultiPalette::Js)),
228            "perl" => Ok(Palette::Multi(MultiPalette::Perl)),
229            "python" => Ok(Palette::Multi(MultiPalette::Python)),
230            "rust" => Ok(Palette::Multi(MultiPalette::Rust)),
231            "red" => Ok(Palette::Basic(BasicPalette::Red)),
232            "green" => Ok(Palette::Basic(BasicPalette::Green)),
233            "blue" => Ok(Palette::Basic(BasicPalette::Blue)),
234            "aqua" => Ok(Palette::Basic(BasicPalette::Aqua)),
235            "yellow" => Ok(Palette::Basic(BasicPalette::Yellow)),
236            "purple" => Ok(Palette::Basic(BasicPalette::Purple)),
237            "orange" => Ok(Palette::Basic(BasicPalette::Orange)),
238            unknown => Err(format!("unknown color palette: {}", unknown)),
239        }
240    }
241}
242
243struct NamehashVariables {
244    vector: f32,
245    weight: f32,
246    max: f32,
247    modulo: u8,
248}
249
250impl NamehashVariables {
251    fn init() -> Self {
252        NamehashVariables {
253            vector: 0.0,
254            weight: 1.0,
255            max: 1.0,
256            modulo: 10,
257        }
258    }
259
260    fn update(&mut self, character: u8) {
261        let i = f32::from(character % self.modulo);
262        self.vector += (i / f32::from(self.modulo - 1)) * self.weight;
263        self.modulo += 1;
264        self.max += self.weight;
265        self.weight *= 0.70;
266    }
267
268    fn result(&self) -> f32 {
269        1.0 - self.vector / self.max
270    }
271}
272
273/// Generate a vector hash for the name string, weighting early over
274/// later characters. We want to pick the same colors for function
275/// names across different flame graphs.
276fn namehash<I: Iterator<Item = u8>>(mut name: I) -> f32 {
277    let mut namehash_variables = NamehashVariables::init();
278    let mut module_name_found = false;
279
280    // The original Perl regex is: $name =~ s/.(.*?)`//;
281    // Ie. we want to remove everything before the first '`'. If '`' is the first character,
282    // we remove everything before the second '`'. If there is no '`', we keep everything.
283    // This becomes tricky because we want to compute the hash and do the potential deletion
284    // ine one pass only.
285    // So, we start computing the hash and we check for '`' after the first character.
286    // If we find '`' before the end of the computation (3 characters), we stop the computation.
287    // If the computation finishes normally, we search for the first next '`'.
288    // After that, either we have found a '`' (end of prefix), and we need to compute the hash from there,
289    // or there is no '`' in the iterator and we have the hash computed!
290    // In the Perl implementation, the hash was computed while `modulo > 12`, which means 3 iterations
291    // maximum because modulo is initialized at 10.
292
293    match name.next() {
294        None => return namehash_variables.result(),
295        Some(first_char) => namehash_variables.update(first_char),
296    }
297
298    for character in name.by_ref().take(2) {
299        if character == b'`' {
300            module_name_found = true;
301            break;
302        }
303
304        namehash_variables.update(character);
305    }
306
307    module_name_found = module_name_found || name.any(|c| c == b'`');
308
309    if module_name_found {
310        namehash_variables = NamehashVariables::init();
311
312        for character in name.take(3) {
313            namehash_variables.update(character)
314        }
315    }
316
317    namehash_variables.result()
318}
319
320macro_rules! t {
321    ($b:expr, $a:expr, $x:expr) => {
322        $b + ($a * $x) as u8
323    };
324}
325
326macro_rules! color {
327    ($r:expr, $g:expr, $b:expr) => {
328        Color {
329            r: $r,
330            g: $g,
331            b: $b,
332        }
333    };
334}
335
336fn rgb_components_for_palette(palette: Palette, name: &str, v1: f32, v2: f32, v3: f32) -> Color {
337    let basic_palette = match palette {
338        Palette::Basic(basic) => basic,
339        Palette::Multi(MultiPalette::Java) => palettes::java::resolve(name),
340        Palette::Multi(MultiPalette::Perl) => palettes::perl::resolve(name),
341        Palette::Multi(MultiPalette::Python) => palettes::python::resolve(name),
342        Palette::Multi(MultiPalette::Js) => palettes::js::resolve(name),
343        Palette::Multi(MultiPalette::Wakeup) => palettes::wakeup::resolve(name),
344        Palette::Multi(MultiPalette::Rust) => palettes::rust::resolve(name),
345    };
346
347    match basic_palette {
348        BasicPalette::Hot => color!(t!(205, 50_f32, v3), t!(0, 230_f32, v1), t!(0, 55_f32, v2)),
349        BasicPalette::Mem => color!(t!(0, 0_f32, v3), t!(190, 50_f32, v2), t!(0, 210_f32, v1)),
350        BasicPalette::Io => color!(t!(80, 60_f32, v1), t!(80, 60_f32, v1), t!(190, 55_f32, v2)),
351        BasicPalette::Red => color!(t!(200, 55_f32, v1), t!(50, 80_f32, v1), t!(50, 80_f32, v1)),
352        BasicPalette::Green => color!(t!(50, 60_f32, v1), t!(200, 55_f32, v1), t!(50, 60_f32, v1)),
353        BasicPalette::Blue => color!(t!(80, 60_f32, v1), t!(80, 60_f32, v1), t!(205, 50_f32, v1)),
354        BasicPalette::Yellow => {
355            color!(t!(175, 55_f32, v1), t!(175, 55_f32, v1), t!(50, 20_f32, v1))
356        }
357        BasicPalette::Purple => {
358            color!(t!(190, 65_f32, v1), t!(80, 60_f32, v1), t!(190, 65_f32, v1))
359        }
360        BasicPalette::Aqua => color!(t!(50, 60_f32, v1), t!(165, 55_f32, v1), t!(165, 55_f32, v1)),
361        BasicPalette::Orange => color!(t!(190, 65_f32, v1), t!(90, 65_f32, v1), t!(0, 0_f32, v1)),
362    }
363}
364
365pub(super) fn color(
366    palette: Palette,
367    hash: bool,
368    deterministic: bool,
369    name: &str,
370    mut rng: impl FnMut() -> f32,
371) -> Color {
372    let (v1, v2, v3) = if hash {
373        let name_hash = namehash(name.bytes());
374        let reverse_name_hash = namehash(name.bytes().rev());
375
376        (name_hash, reverse_name_hash, reverse_name_hash)
377    } else if deterministic {
378        // Do not use ahash, since it does not have stable output across computers
379        // Instead, just inline the implementation of FNV:
380        // https://github.com/servo/rust-fnv/blob/4b4784ebfd3332dc61f0640764d6f1140e03a9ab/lib.rs#L95
381        let mut hash: u64 = 0xcbf29ce484222325;
382        // https://github.com/servo/rust-fnv/blob/4b4784ebfd3332dc61f0640764d6f1140e03a9ab/lib.rs#L118-L121
383        for byte in name.as_bytes() {
384            hash ^= *byte as u64;
385            hash = hash.wrapping_mul(0x100000001b3);
386        }
387        let hash1 = (hash as f64 / u64::MAX as f64) as f32;
388
389        // Rotate hash so we get two more distinct numbers
390        hash ^= 0;
391        hash = hash.wrapping_mul(0x100000001b3);
392        let hash2 = (hash as f64 / u64::MAX as f64) as f32;
393        hash ^= 0;
394        hash = hash.wrapping_mul(0x100000001b3);
395        let hash3 = (hash as f64 / u64::MAX as f64) as f32;
396
397        (hash1, hash2, hash3)
398    } else {
399        (rng(), rng(), rng())
400    };
401
402    rgb_components_for_palette(palette, name, v1, v2, v3)
403}
404
405pub(super) fn color_scale(value: isize, max: usize) -> Color {
406    match value.cmp(&0) {
407        Ordering::Equal => Color {
408            r: 250,
409            g: 250,
410            b: 250,
411        },
412        Ordering::Greater => {
413            // A positive value indicates _more_ samples,
414            // and hence more time spent, so we give it a red hue.
415            let c = 100 + (150 * (max as isize - value) / max as isize) as u8;
416            Color { r: 255, g: c, b: c }
417        }
418        Ordering::Less => {
419            // A negative value indicates _fewer_ samples,
420            // or a speed-up, so we give it a blue hue.
421            let c = 100 + (150 * (max as isize + value) / max as isize) as u8;
422            Color { r: c, g: c, b: 255 }
423        }
424    }
425}
426
427fn default_bg_color_for(palette: Palette) -> BackgroundColor {
428    match palette {
429        Palette::Basic(BasicPalette::Mem) => BackgroundColor::Green,
430        Palette::Basic(BasicPalette::Io) | Palette::Multi(MultiPalette::Wakeup) => {
431            BackgroundColor::Blue
432        }
433        Palette::Basic(BasicPalette::Red)
434        | Palette::Basic(BasicPalette::Green)
435        | Palette::Basic(BasicPalette::Blue)
436        | Palette::Basic(BasicPalette::Aqua)
437        | Palette::Basic(BasicPalette::Yellow)
438        | Palette::Basic(BasicPalette::Purple)
439        | Palette::Basic(BasicPalette::Orange) => BackgroundColor::Grey,
440        _ => BackgroundColor::Yellow,
441    }
442}
443
444macro_rules! cow {
445    ($gradient:expr) => {
446        (Cow::from($gradient.0), Cow::from($gradient.1))
447    };
448}
449
450pub(super) fn bgcolor_for<'a>(
451    bgcolor: Option<BackgroundColor>,
452    palette: Palette,
453) -> (Cow<'a, str>, Cow<'a, str>) {
454    let bgcolor = bgcolor.unwrap_or_else(|| default_bg_color_for(palette));
455
456    match bgcolor {
457        BackgroundColor::Yellow => cow!(YELLOW_GRADIENT),
458        BackgroundColor::Blue => cow!(BLUE_GRADIENT),
459        BackgroundColor::Green => cow!(GREEN_GRADIENT),
460        BackgroundColor::Grey => cow!(GRAY_GRADIENT),
461        BackgroundColor::Flat(color) => {
462            let color = format!("#{:02x}{:02x}{:02x}", color.r, color.g, color.b);
463            let first = Cow::from(color);
464            let second = first.clone();
465            (first, second)
466        }
467    }
468}
469
470#[cfg(test)]
471mod tests {
472    use super::namehash;
473    use super::parse_hex_color;
474    use super::Color;
475    use pretty_assertions::assert_eq;
476
477    #[test]
478    fn bgcolor_parse_test() {
479        assert_eq!(parse_hex_color("#ffffff"), Some(color!(0xff, 0xff, 0xff)));
480        assert_eq!(parse_hex_color("#000000"), Some(color!(0x00, 0x00, 0x00)));
481        assert_eq!(parse_hex_color("#abcdef"), Some(color!(0xab, 0xcd, 0xef)));
482        assert_eq!(parse_hex_color("#123456"), Some(color!(0x12, 0x34, 0x56)));
483        assert_eq!(parse_hex_color("#789000"), Some(color!(0x78, 0x90, 0x00)));
484        assert_eq!(parse_hex_color("ffffff"), None);
485        assert_eq!(parse_hex_color("#fffffff"), None);
486        assert_eq!(parse_hex_color("#xfffff"), None);
487        assert_eq!(parse_hex_color("# fffff"), None);
488    }
489
490    macro_rules! test_hash {
491        ($name:expr, $expected:expr) => {
492            assert!((dbg!(namehash($name.bytes())) - $expected).abs() < f32::EPSILON);
493        };
494    }
495
496    #[test]
497    fn namehash_test() {
498        test_hash!(
499            "org/mozilla/javascript/NativeFunction:.initScriptFunction_[j]",
500            0.779_646_04
501        );
502        test_hash!(
503            "]j[_noitcnuFtpircStini.:noitcnuFevitaN/tpircsavaj/allizom/gro",
504            0.644_153_1
505        );
506        test_hash!("genunix`kmem_cache_free", 0.466_926_34);
507        test_hash!("eerf_ehcac_memk`xinuneg", 0.840_410_3);
508        test_hash!("unix`0xfffffffffb8001d6", 0.418_131_17);
509        test_hash!("6d1008bfffffffffx0`xinu", 0.840_410_3);
510        test_hash!("un`0xfffffffffb8001d6", 0.418_131_17);
511        test_hash!("``0xfffffffffb8001d6", 0.418_131_17);
512        test_hash!("", 1.0);
513    }
514}