criterion_table/formatter/
gfm.rs

1use crate::{ColumnInfo, Comparison, Formatter, TimeUnit};
2use flexstr::{flex_fmt, FlexStr, IntoFlex, ToCase, ToFlexStr};
3use indexmap::IndexMap;
4
5const CT_URL: &str = "https://github.com/nu11ptr/criterion-table";
6
7// *** NOTE: These are in _bytes_, not _chars_ - since ASCII right now this is ok ***
8// Width of making a single item bold
9const FIRST_COL_EXTRA_WIDTH: usize = "**``**".len();
10// Width of a single item in bold (italics is less) + one item in back ticks + one item in parens + one space
11// NOTE: Added one more "X" because we added unicode check, x, and rocket (uses only 1 per cell) that won't be 1 byte each
12const USED_EXTRA_WIDTH: usize = "() ``****XX".len();
13
14// *** GFM Formatter ***
15
16/// This formatter outputs Github Flavored Markdown
17pub struct GFMFormatter;
18
19impl GFMFormatter {
20    fn pad(buffer: &mut String, ch: char, max_width: usize, written: usize) {
21        // Pad the rest of the column (inclusive to handle trailing space)
22        let remaining = max_width - written;
23
24        for _ in 0..=remaining {
25            buffer.push(ch);
26        }
27    }
28
29    #[inline]
30    fn encode_link(s: &str) -> FlexStr {
31        s.replace(' ', "-").into_flex().to_lower()
32    }
33
34    fn write_toc_entry(buffer: &mut String, entry: &str, indent: bool) {
35        if indent {
36            buffer.push_str("    ");
37        }
38        buffer.push_str("- [");
39        buffer.push_str(entry);
40        buffer.push_str("](#");
41        buffer.push_str(&Self::encode_link(entry));
42        buffer.push_str(")\n");
43    }
44}
45
46impl Formatter for GFMFormatter {
47    fn start(
48        &mut self,
49        buffer: &mut String,
50        top_comments: &IndexMap<FlexStr, FlexStr>,
51        tables: &[&FlexStr],
52    ) {
53        buffer.push_str("# Benchmarks\n\n");
54        buffer.push_str("## Table of Contents\n\n");
55
56        // Write each ToC entry in comments
57        for section_entry in top_comments.keys() {
58            Self::write_toc_entry(buffer, section_entry, false);
59        }
60
61        Self::write_toc_entry(buffer, "Benchmark Results", false);
62
63        // Write each Benchmark ToC entry
64        for &table_entry in tables {
65            Self::write_toc_entry(buffer, table_entry, true);
66        }
67
68        buffer.push('\n');
69
70        // Write out all the comment sections and comments
71        for (header, comment) in top_comments {
72            buffer.push_str("## ");
73            buffer.push_str(header);
74            buffer.push_str("\n\n");
75            buffer.push_str(comment);
76            buffer.push('\n');
77        }
78
79        buffer.push_str("## Benchmark Results\n\n");
80    }
81
82    fn end(&mut self, buffer: &mut String) {
83        buffer.push_str("---\n");
84        buffer.push_str("Made with [criterion-table](");
85        buffer.push_str(CT_URL);
86        buffer.push_str(")\n");
87    }
88
89    fn start_table(
90        &mut self,
91        buffer: &mut String,
92        name: &FlexStr,
93        comment: Option<&FlexStr>,
94        columns: &[ColumnInfo],
95    ) {
96        // *** Title ***
97
98        buffer.push_str("### ");
99        buffer.push_str(name);
100        buffer.push_str("\n\n");
101
102        if let Some(comments) = comment {
103            buffer.push_str(comments);
104            buffer.push('\n');
105        }
106
107        // *** Header Row ***
108
109        buffer.push_str("| ");
110        // Safety: Any slicing up to index 1 is always safe - guaranteed to have at least one column
111        let first_col_max_width = columns[0].max_width + FIRST_COL_EXTRA_WIDTH;
112        Self::pad(buffer, ' ', first_col_max_width, 0);
113
114        // Safety: Any slicing up to index 1 is always safe - guaranteed to have at least one column
115        for column in &columns[1..] {
116            let max_width = column.max_width + USED_EXTRA_WIDTH;
117
118            buffer.push_str("| `");
119            buffer.push_str(&column.name);
120            buffer.push('`');
121            Self::pad(buffer, ' ', max_width, column.name.chars().count() + 2);
122        }
123
124        buffer.push_str(" |\n");
125
126        // *** Deliminator Row ***
127
128        // Right now, everything is left justified
129        buffer.push_str("|:");
130        Self::pad(buffer, '-', first_col_max_width, 0);
131
132        // Safety: Any slicing up to index 1 is always safe - guaranteed to have at least one column
133        for column in &columns[1..] {
134            let max_width = column.max_width + USED_EXTRA_WIDTH;
135
136            buffer.push_str("|:");
137            Self::pad(buffer, '-', max_width, 0);
138        }
139
140        buffer.push_str(" |\n");
141    }
142
143    fn end_table(&mut self, buffer: &mut String) {
144        buffer.push('\n');
145    }
146
147    fn start_row(&mut self, buffer: &mut String, name: &FlexStr, max_width: usize) {
148        // Regular row name
149        let written = if !name.is_empty() {
150            buffer.push_str("| **`");
151            buffer.push_str(name);
152            buffer.push_str("`**");
153            name.chars().count() + FIRST_COL_EXTRA_WIDTH
154            // Empty row name
155        } else {
156            buffer.push_str("| ");
157            0
158        };
159
160        Self::pad(buffer, ' ', max_width + FIRST_COL_EXTRA_WIDTH, written);
161    }
162
163    fn end_row(&mut self, buffer: &mut String) {
164        buffer.push_str(" |\n");
165    }
166
167    fn used_column(
168        &mut self,
169        buffer: &mut String,
170        time: TimeUnit,
171        compare: Comparison,
172        max_width: usize,
173    ) {
174        let (time_str, speedup_str) = (time.to_flex_str(), compare.to_flex_str());
175
176        // Allow 10% wiggle room to qualify
177        let data = if compare >= 1.8 {
178            // Positive = bold
179            flex_fmt!("`{time_str}` (🚀 **{speedup_str}**)")
180        // Allow 10% wiggle room to qualify
181        } else if compare > 0.9 {
182            // Positive = bold
183            flex_fmt!("`{time_str}` (✅ **{speedup_str}**)")
184        // Allow 10% wiggle room
185        } else if compare < 0.9 {
186            // Negative = italics
187            flex_fmt!("`{time_str}` (❌ *{speedup_str}*)")
188        } else {
189            // Even = no special formatting
190            flex_fmt!("`{time_str}` ({speedup_str})")
191        };
192
193        buffer.push_str("| ");
194        buffer.push_str(&data);
195
196        let max_width = max_width + USED_EXTRA_WIDTH;
197        Self::pad(buffer, ' ', max_width, data.chars().count());
198    }
199
200    fn unused_column(&mut self, buffer: &mut String, max_width: usize) {
201        buffer.push_str("| ");
202        let data = "`N/A`";
203        buffer.push_str(data);
204
205        Self::pad(
206            buffer,
207            ' ',
208            max_width + USED_EXTRA_WIDTH,
209            data.chars().count(),
210        );
211    }
212}