1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
use std::collections::HashMap;
use std::fs::{File, OpenOptions};
use std::io::{self, BufRead, BufReader};
use std::path::Path;
use std::str::FromStr;

use log::warn;

use crate::flamegraph::color::Color;

/// Mapping of the association between a function name and the color used when drawing information
/// from this function.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct PaletteMap(HashMap<String, Color>);

impl PaletteMap {
    /// Returns the color value corresponding to the given function name.
    pub fn get(&self, func: &str) -> Option<Color> {
        self.0.get(func).cloned()
    }

    /// Inserts a function name/color pair in the map.
    pub fn insert<S: ToString>(&mut self, func: S, color: Color) -> Option<Color> {
        self.0.insert(func.to_string(), color)
    }

    /// Provides an iterator over the elements of the map.
    pub fn iter(&self) -> impl Iterator<Item = (&str, Color)> {
        self.0.iter().map(|(func, color)| (func.as_str(), *color))
    }

    /// Builds a mapping based on the inputs given by the reader.
    ///
    /// The reader should provide name/color pairs as text input, each pair separated by a line
    /// separator.
    ///
    /// Each line should follow the format: NAME->rgb(RED, GREEN, BLUE)
    /// where NAME is the function name, and RED, GREEN, BLUE integer values between 0 and 255
    /// included.
    /// Any line which does not follow the previous format will be ignored.
    ///
    /// This function will propagate any [`std::io::Error`] returned by the given reader.
    pub fn from_reader(reader: &mut dyn io::BufRead) -> io::Result<Self> {
        let mut map = HashMap::default();
        let mut ignored = 0;

        for line in reader.lines() {
            let line = line?;
            if let Ok((name, color)) = parse_line(&line) {
                map.insert(name.to_string(), color);
            } else {
                ignored += 1;
            }
        }

        if ignored != 0 {
            warn!("Ignored {} lines with invalid format", ignored);
        }

        Ok(PaletteMap(map))
    }

    /// Writes the palette map using the given writer.
    /// The output content will follow the same format described in [from_stream()]
    /// The name/color pairs will be sorted by name in lexicographic order.
    pub fn to_writer(&self, writer: &mut dyn io::Write) -> io::Result<()> {
        let mut entries = self.0.iter().collect::<Vec<_>>();
        // We sort the palette because the Perl implementation does.
        entries.sort_unstable();

        for (name, color) in entries {
            writer.write_all(
                format!("{}->rgb({},{},{})\n", name, color.r, color.g, color.b).as_bytes(),
            )?
        }

        Ok(())
    }

    /// Utility function to load a palette map from a file.
    ///
    /// The file content should follow the format described in [from_stream()].
    ///
    /// If the file does not exist, an empty palette map is returned.
    pub fn load_from_file_or_empty(path: &dyn AsRef<Path>) -> io::Result<Self> {
        // If the file does not exist, it is probably the first call to flamegraph with a consistent
        // palette: there is nothing to load.
        if path.as_ref().exists() {
            let file = File::open(path)?;
            let mut reader = BufReader::new(file);
            PaletteMap::from_reader(&mut reader)
        } else {
            Ok(PaletteMap::default())
        }
    }

    /// Utility function to save a palette map to a file.
    ///
    /// The file content will follow the format described in [from_stream()].
    pub fn save_to_file(&self, path: &dyn AsRef<Path>) -> io::Result<()> {
        let mut file = OpenOptions::new().write(true).create(true).open(path)?;
        self.to_writer(&mut file)
    }

    /// Returns the color value corresponding to the given function name if it is present.
    /// Otherwise compute the color, and insert the new function name/color in the map.
    pub(crate) fn find_color_for<F: FnMut(&str) -> Color>(
        &mut self,
        name: &str,
        mut compute_color: F,
    ) -> Color {
        match self.get(name) {
            Some(color) => color,
            None => {
                let color = compute_color(name);
                self.insert(name, color);
                color
            }
        }
    }
}

fn parse_line(line: &str) -> io::Result<(&str, Color)> {
    // A line is formatted like this: NAME -> rbg(RED, GREEN, BLUE)
    let mut words = line.split("->");

    let name = match words.next() {
        Some(name) => name,
        None => return Err(io::Error::from(io::ErrorKind::InvalidInput)),
    };

    let color = match words.next() {
        Some(name) => name,
        None => return Err(io::Error::from(io::ErrorKind::InvalidInput)),
    };

    if words.next().is_some() {
        return Err(io::Error::from(io::ErrorKind::InvalidInput));
    }

    let rgb_color =
        parse_rgb_string(color).ok_or_else(|| io::Error::from(io::ErrorKind::InvalidData))?;

    Ok((name, rgb_color))
}

fn parse_rgb_string(s: &str) -> Option<Color> {
    let s = s.trim();

    if !s.starts_with("rgb(") || !s.ends_with(')') {
        return None;
    }

    let s = &s["rgb(".len()..s.len() - 1];
    let r_end_index = s.find(',')?;
    let r_str = s[..r_end_index].trim();
    let r = u8::from_str(r_str).ok()?;

    let s = &s[r_end_index + 1..];
    let g_end_index = s.find(',')?;
    let g_str = s[..g_end_index].trim();
    let g = u8::from_str(g_str).ok()?;

    let b_str = &s[g_end_index + 1..].trim();
    let b = u8::from_str(b_str).ok()?;

    Some(Color { r, g, b })
}

#[cfg(test)]
mod tests {
    use crate::flamegraph::color::palette_map::{parse_line, PaletteMap};
    use crate::flamegraph::color::Color;
    use pretty_assertions::assert_eq;
    use std::io::Cursor;

    macro_rules! color {
        ($r:expr, $g:expr, $b:expr) => {
            Color {
                r: $r,
                g: $g,
                b: $b,
            }
        };
    }

    #[test]
    fn palette_map_test() {
        let mut palette = PaletteMap::default();

        assert_eq!(palette.insert("foo", color!(0, 50, 255)), None);
        assert_eq!(palette.insert("bar", color!(50, 0, 60)), None);
        assert_eq!(
            palette.insert("foo", color!(80, 20, 63)),
            Some(color!(0, 50, 255))
        );
        assert_eq!(
            palette.insert("foo", color!(128, 128, 128)),
            Some(color!(80, 20, 63))
        );
        assert_eq!(palette.insert("baz", color!(255, 0, 255)), None);

        assert_eq!(palette.get("func"), None);
        assert_eq!(palette.get("bar"), Some(color!(50, 0, 60)));
        assert_eq!(palette.get("foo"), Some(color!(128, 128, 128)));
        assert_eq!(palette.get("baz"), Some(color!(255, 0, 255)));

        let mut vec = palette.iter().collect::<Vec<_>>();
        vec.sort_unstable();
        let mut iter = vec.iter();

        assert_eq!(iter.next(), Some(&("bar", color!(50, 0, 60))));
        assert_eq!(iter.next(), Some(&("baz", color!(255, 0, 255))));
        assert_eq!(iter.next(), Some(&("foo", color!(128, 128, 128))));
        assert_eq!(iter.next(), None);

        let mut buf = Cursor::new(Vec::new());

        palette.to_writer(&mut buf).unwrap();
        buf.set_position(0);
        let palette = PaletteMap::from_reader(&mut buf).unwrap();

        let mut vec = palette.iter().collect::<Vec<_>>();
        vec.sort_unstable();
        let mut iter = vec.iter();

        assert_eq!(iter.next(), Some(&("bar", color!(50, 0, 60))));
        assert_eq!(iter.next(), Some(&("baz", color!(255, 0, 255))));
        assert_eq!(iter.next(), Some(&("foo", color!(128, 128, 128))));
        assert_eq!(iter.next(), None);
    }

    #[test]
    fn parse_line_test() {
        assert_eq!(
            parse_line("func->rgb(0, 0, 0)").unwrap(),
            ("func", color!(0, 0, 0))
        );
        assert_eq!(
            parse_line("->rgb(255, 255, 255)").unwrap(),
            ("", color!(255, 255, 255))
        );

        assert!(parse_line("").is_err());
        assert!(parse_line("func->(0, 0, 0)").is_err());
        assert!(parse_line("func->").is_err());
        assert!(parse_line("func->foo->rgb(0, 0, 0)").is_err());
        assert!(parse_line("func->rgb(0, 0, 0)->foo").is_err());
        assert!(parse_line("func->rgb(255, 255, 256)").is_err());
        assert!(parse_line("func->rgb(-1, 255, 255)").is_err());
    }

    #[test]
    fn load_from_non_existing_file() {
        let palette_map = PaletteMap::load_from_file_or_empty(&"non-existing-palette.map").unwrap();
        assert_eq!(palette_map, PaletteMap::default());
    }
}