@@ -54,6 +54,7 @@ fn parity_corpus() { |
| 54 | 54 | } |
| 55 | 55 | case_reports.push((case, report)); |
| 56 | 56 | } |
| 57 | + print_timing_summary(started.elapsed(), &case_reports); |
| 57 | 58 | |
| 58 | 59 | if let Some(dir) = artifact_dir.as_ref() { |
| 59 | 60 | write_index_artifact(dir, &case_reports).expect("write parity index"); |
@@ -80,24 +81,40 @@ fn parity_corpus() { |
| 80 | 81 | #[derive(Debug)] |
| 81 | 82 | struct CaseStep { |
| 82 | 83 | name: &'static str, |
| 84 | + duration: Duration, |
| 83 | 85 | error: Option<String>, |
| 84 | 86 | } |
| 85 | 87 | |
| 86 | 88 | #[derive(Debug, Default)] |
| 87 | 89 | struct CaseReport { |
| 88 | 90 | steps: Vec<CaseStep>, |
| 91 | + elapsed: Duration, |
| 89 | 92 | } |
| 90 | 93 | |
| 91 | 94 | impl CaseReport { |
| 92 | 95 | fn push(&mut self, name: &'static str, result: Result<(), String>) -> bool { |
| 96 | + self.push_timed(name, Duration::ZERO, result) |
| 97 | + } |
| 98 | + |
| 99 | + fn push_timed( |
| 100 | + &mut self, |
| 101 | + name: &'static str, |
| 102 | + duration: Duration, |
| 103 | + result: Result<(), String>, |
| 104 | + ) -> bool { |
| 93 | 105 | match result { |
| 94 | 106 | Ok(()) => { |
| 95 | | - self.steps.push(CaseStep { name, error: None }); |
| 107 | + self.steps.push(CaseStep { |
| 108 | + name, |
| 109 | + duration, |
| 110 | + error: None, |
| 111 | + }); |
| 96 | 112 | true |
| 97 | 113 | } |
| 98 | 114 | Err(error) => { |
| 99 | 115 | self.steps.push(CaseStep { |
| 100 | 116 | name, |
| 117 | + duration, |
| 101 | 118 | error: Some(error), |
| 102 | 119 | }); |
| 103 | 120 | false |
@@ -105,15 +122,32 @@ impl CaseReport { |
| 105 | 122 | } |
| 106 | 123 | } |
| 107 | 124 | |
| 125 | + fn measure<F>(&mut self, name: &'static str, action: F) -> bool |
| 126 | + where |
| 127 | + F: FnOnce() -> Result<(), String>, |
| 128 | + { |
| 129 | + let started = Instant::now(); |
| 130 | + let result = action(); |
| 131 | + self.push_timed(name, started.elapsed(), result) |
| 132 | + } |
| 133 | + |
| 134 | + fn finish(&mut self, elapsed: Duration) { |
| 135 | + self.elapsed = elapsed; |
| 136 | + } |
| 137 | + |
| 108 | 138 | fn passed(&self) -> bool { |
| 109 | 139 | self.steps.iter().all(|step| step.error.is_none()) |
| 110 | 140 | } |
| 111 | 141 | |
| 142 | + fn slowest_step(&self) -> Option<&CaseStep> { |
| 143 | + self.steps.iter().max_by_key(|step| step.duration) |
| 144 | + } |
| 145 | + |
| 112 | 146 | fn error_message(&self, case_name: &str) -> Option<String> { |
| 113 | 147 | self.steps.iter().find_map(|step| { |
| 114 | | - step.error.as_ref().map(|error| { |
| 115 | | - format!("[{case_name}] {} failed:\n{}", step.name, error) |
| 116 | | - }) |
| 148 | + step.error |
| 149 | + .as_ref() |
| 150 | + .map(|error| format!("[{case_name}] {} failed:\n{}", step.name, error)) |
| 117 | 151 | }) |
| 118 | 152 | } |
| 119 | 153 | } |
@@ -132,85 +166,84 @@ fn case_report_error_message_includes_case_name() { |
| 132 | 166 | } |
| 133 | 167 | |
| 134 | 168 | fn run_case(case: &LinkCase) -> CaseReport { |
| 169 | + let case_started = Instant::now(); |
| 135 | 170 | let mut report = CaseReport::default(); |
| 171 | + let link_started = Instant::now(); |
| 136 | 172 | let outputs = match link_both(case) { |
| 137 | 173 | Ok(outputs) => { |
| 138 | | - report.push("link", Ok(())); |
| 174 | + report.push_timed("link", link_started.elapsed(), Ok(())); |
| 139 | 175 | outputs |
| 140 | 176 | } |
| 141 | 177 | Err(error) => { |
| 142 | | - report.push( |
| 178 | + report.push_timed( |
| 143 | 179 | "link", |
| 180 | + link_started.elapsed(), |
| 144 | 181 | Err(format!( |
| 145 | 182 | "failed to link parity case from {}:\n{}", |
| 146 | 183 | case.dir.display(), |
| 147 | 184 | error |
| 148 | 185 | )), |
| 149 | 186 | ); |
| 150 | | - return report; |
| 187 | + return finish_case(report, case_started); |
| 151 | 188 | } |
| 152 | 189 | }; |
| 153 | 190 | |
| 154 | | - if !report.push( |
| 155 | | - "load-command ids", |
| 156 | | - compare_command_ids(&outputs.ours, &outputs.theirs, &case.ignored_load_commands), |
| 157 | | - ) { |
| 158 | | - return report; |
| 191 | + if !report.measure("load-command ids", || { |
| 192 | + compare_command_ids(&outputs.ours, &outputs.theirs, &case.ignored_load_commands) |
| 193 | + }) { |
| 194 | + return finish_case(report, case_started); |
| 159 | 195 | } |
| 160 | | - if !report.push( |
| 161 | | - "command details", |
| 162 | | - compare_command_details(&outputs.ours, &outputs.theirs, &case.command_checks), |
| 163 | | - ) { |
| 164 | | - return report; |
| 196 | + if !report.measure("command details", || { |
| 197 | + compare_command_details(&outputs.ours, &outputs.theirs, &case.command_checks) |
| 198 | + }) { |
| 199 | + return finish_case(report, case_started); |
| 165 | 200 | } |
| 166 | | - if !report.push( |
| 167 | | - "afs-ld absent commands", |
| 168 | | - ensure_absent_load_commands(&outputs.ours, &case.absent_load_commands, "afs-ld"), |
| 169 | | - ) { |
| 170 | | - return report; |
| 201 | + if !report.measure("afs-ld absent commands", || { |
| 202 | + ensure_absent_load_commands(&outputs.ours, &case.absent_load_commands, "afs-ld") |
| 203 | + }) { |
| 204 | + return finish_case(report, case_started); |
| 171 | 205 | } |
| 172 | | - if !report.push( |
| 173 | | - "Apple absent commands", |
| 174 | | - ensure_absent_load_commands(&outputs.theirs, &case.absent_load_commands, "Apple ld"), |
| 175 | | - ) { |
| 176 | | - return report; |
| 206 | + if !report.measure("Apple absent commands", || { |
| 207 | + ensure_absent_load_commands(&outputs.theirs, &case.absent_load_commands, "Apple ld") |
| 208 | + }) { |
| 209 | + return finish_case(report, case_started); |
| 177 | 210 | } |
| 178 | | - if !report.push( |
| 179 | | - "afs-ld absent sections", |
| 180 | | - ensure_absent_sections(&outputs.ours, &case.absent_sections, "afs-ld"), |
| 181 | | - ) { |
| 182 | | - return report; |
| 211 | + if !report.measure("afs-ld absent sections", || { |
| 212 | + ensure_absent_sections(&outputs.ours, &case.absent_sections, "afs-ld") |
| 213 | + }) { |
| 214 | + return finish_case(report, case_started); |
| 183 | 215 | } |
| 184 | | - if !report.push( |
| 185 | | - "Apple absent sections", |
| 186 | | - ensure_absent_sections(&outputs.theirs, &case.absent_sections, "Apple ld"), |
| 187 | | - ) { |
| 188 | | - return report; |
| 216 | + if !report.measure("Apple absent sections", || { |
| 217 | + ensure_absent_sections(&outputs.theirs, &case.absent_sections, "Apple ld") |
| 218 | + }) { |
| 219 | + return finish_case(report, case_started); |
| 189 | 220 | } |
| 190 | | - if !report.push( |
| 191 | | - "section parity", |
| 221 | + if !report.measure("section parity", || { |
| 192 | 222 | compare_sections( |
| 193 | 223 | &outputs.ours, |
| 194 | 224 | &outputs.theirs, |
| 195 | 225 | &case.section_checks, |
| 196 | 226 | &case.case_tolerances, |
| 197 | | - ), |
| 198 | | - ) { |
| 199 | | - return report; |
| 227 | + ) |
| 228 | + }) { |
| 229 | + return finish_case(report, case_started); |
| 200 | 230 | } |
| 201 | | - if !report.push( |
| 202 | | - "page-ref parity", |
| 203 | | - compare_page_refs(&outputs.ours, &outputs.theirs, &case.page_ref_checks), |
| 204 | | - ) { |
| 205 | | - return report; |
| 231 | + if !report.measure("page-ref parity", || { |
| 232 | + compare_page_refs(&outputs.ours, &outputs.theirs, &case.page_ref_checks) |
| 233 | + }) { |
| 234 | + return finish_case(report, case_started); |
| 206 | 235 | } |
| 207 | 236 | if !case.runtime_args.is_empty() || case.dir.join("runtime.txt").exists() { |
| 208 | | - report.push( |
| 209 | | - "runtime parity", |
| 210 | | - compare_runtime(&outputs.our_path, &outputs.their_path, &case.runtime_args), |
| 211 | | - ); |
| 237 | + report.measure("runtime parity", || { |
| 238 | + compare_runtime(&outputs.our_path, &outputs.their_path, &case.runtime_args) |
| 239 | + }); |
| 212 | 240 | } |
| 213 | 241 | |
| 242 | + finish_case(report, case_started) |
| 243 | +} |
| 244 | + |
| 245 | +fn finish_case(mut report: CaseReport, started: Instant) -> CaseReport { |
| 246 | + report.finish(started.elapsed()); |
| 214 | 247 | report |
| 215 | 248 | } |
| 216 | 249 | |
@@ -228,16 +261,22 @@ fn write_case_artifact(dir: &Path, case: &LinkCase, report: &CaseReport) -> Resu |
| 228 | 261 | if report.passed() { "ok" } else { "fail" }, |
| 229 | 262 | if report.passed() { "PASS" } else { "FAIL" } |
| 230 | 263 | )); |
| 264 | + html.push_str(&format!( |
| 265 | + "<p>Total: <strong>{}</strong></p>", |
| 266 | + format_duration(report.elapsed) |
| 267 | + )); |
| 231 | 268 | html.push_str("<h2>Steps</h2><ul>"); |
| 232 | 269 | for step in &report.steps { |
| 233 | 270 | match &step.error { |
| 234 | 271 | None => html.push_str(&format!( |
| 235 | | - "<li><span class=\"ok\">PASS</span> {}</li>", |
| 236 | | - escape_html(step.name) |
| 272 | + "<li><span class=\"ok\">PASS</span> {} <span class=\"time\">{}</span></li>", |
| 273 | + escape_html(step.name), |
| 274 | + format_duration(step.duration) |
| 237 | 275 | )), |
| 238 | 276 | Some(error) => html.push_str(&format!( |
| 239 | | - "<li><span class=\"fail\">FAIL</span> {}<pre>{}</pre></li>", |
| 277 | + "<li><span class=\"fail\">FAIL</span> {} <span class=\"time\">{}</span><pre>{}</pre></li>", |
| 240 | 278 | escape_html(step.name), |
| 279 | + format_duration(step.duration), |
| 241 | 280 | escape_html(error) |
| 242 | 281 | )), |
| 243 | 282 | } |
@@ -258,16 +297,32 @@ fn write_case_artifact(dir: &Path, case: &LinkCase, report: &CaseReport) -> Resu |
| 258 | 297 | fn write_index_artifact(dir: &Path, cases: &[(LinkCase, CaseReport)]) -> Result<(), String> { |
| 259 | 298 | let mut html = String::new(); |
| 260 | 299 | html.push_str("<!doctype html><html><head><meta charset=\"utf-8\">"); |
| 261 | | - html.push_str("<title>Parity Matrix</title><style>body{font-family:ui-monospace,Menlo,monospace;padding:2rem;} .ok{color:#0a0;} .fail{color:#a00;}</style></head><body>"); |
| 262 | | - html.push_str("<h1>Parity Matrix</h1><ul>"); |
| 300 | + html.push_str("<title>Parity Matrix</title><style>body{font-family:ui-monospace,Menlo,monospace;padding:2rem;} .ok{color:#0a0;} .fail{color:#a00;} .time{color:#57606a;} table{border-collapse:collapse;margin:1rem 0;} td,th{border:1px solid #d0d7de;padding:.35rem .6rem;text-align:left;}</style></head><body>"); |
| 301 | + html.push_str("<h1>Parity Matrix</h1>"); |
| 302 | + html.push_str("<h2>Slowest Cases</h2><table><thead><tr><th>Case</th><th>Total</th><th>Slowest Step</th></tr></thead><tbody>"); |
| 303 | + for (case, report) in slowest_cases(cases, 10) { |
| 304 | + let slowest = report |
| 305 | + .slowest_step() |
| 306 | + .map(|step| format!("{} {}", step.name, format_duration(step.duration))) |
| 307 | + .unwrap_or_else(|| "n/a".to_string()); |
| 308 | + html.push_str(&format!( |
| 309 | + "<tr><td><a href=\"{}.html\">{}</a></td><td>{}</td><td>{}</td></tr>", |
| 310 | + slug(&case.name), |
| 311 | + escape_html(&case.name), |
| 312 | + format_duration(report.elapsed), |
| 313 | + escape_html(&slowest) |
| 314 | + )); |
| 315 | + } |
| 316 | + html.push_str("</tbody></table><h2>Cases</h2><ul>"); |
| 263 | 317 | for (case, report) in cases { |
| 264 | 318 | let slug = slug(&case.name); |
| 265 | 319 | html.push_str(&format!( |
| 266 | | - "<li><a href=\"{}.html\">{}</a> <strong class=\"{}\">{}</strong></li>", |
| 320 | + "<li><a href=\"{}.html\">{}</a> <strong class=\"{}\">{}</strong> <span class=\"time\">{}</span></li>", |
| 267 | 321 | slug, |
| 268 | 322 | escape_html(&case.name), |
| 269 | 323 | if report.passed() { "ok" } else { "fail" }, |
| 270 | | - if report.passed() { "PASS" } else { "FAIL" } |
| 324 | + if report.passed() { "PASS" } else { "FAIL" }, |
| 325 | + format_duration(report.elapsed) |
| 271 | 326 | )); |
| 272 | 327 | } |
| 273 | 328 | html.push_str("</ul></body></html>"); |
@@ -275,6 +330,43 @@ fn write_index_artifact(dir: &Path, cases: &[(LinkCase, CaseReport)]) -> Result< |
| 275 | 330 | fs::write(&path, html).map_err(|e| format!("write {}: {e}", path.display())) |
| 276 | 331 | } |
| 277 | 332 | |
| 333 | +fn print_timing_summary(elapsed: Duration, cases: &[(LinkCase, CaseReport)]) { |
| 334 | + eprintln!( |
| 335 | + "parity matrix timing: {} case(s) in {}", |
| 336 | + cases.len(), |
| 337 | + format_duration(elapsed) |
| 338 | + ); |
| 339 | + for (case, report) in slowest_cases(cases, 10) { |
| 340 | + let slowest = report |
| 341 | + .slowest_step() |
| 342 | + .map(|step| { |
| 343 | + format!( |
| 344 | + "; slowest step: {} {}", |
| 345 | + step.name, |
| 346 | + format_duration(step.duration) |
| 347 | + ) |
| 348 | + }) |
| 349 | + .unwrap_or_default(); |
| 350 | + eprintln!( |
| 351 | + " {:>9} {}{}", |
| 352 | + format_duration(report.elapsed), |
| 353 | + case.name, |
| 354 | + slowest |
| 355 | + ); |
| 356 | + } |
| 357 | +} |
| 358 | + |
| 359 | +fn slowest_cases(cases: &[(LinkCase, CaseReport)], limit: usize) -> Vec<(&LinkCase, &CaseReport)> { |
| 360 | + let mut timed: Vec<_> = cases.iter().map(|(case, report)| (case, report)).collect(); |
| 361 | + timed.sort_by(|a, b| { |
| 362 | + b.1.elapsed |
| 363 | + .cmp(&a.1.elapsed) |
| 364 | + .then_with(|| a.0.name.cmp(&b.0.name)) |
| 365 | + }); |
| 366 | + timed.truncate(limit); |
| 367 | + timed |
| 368 | +} |
| 369 | + |
| 278 | 370 | fn slug(name: &str) -> String { |
| 279 | 371 | name.chars() |
| 280 | 372 | .map(|ch| { |
@@ -293,6 +385,15 @@ fn parity_matrix_time_limit() -> Option<Duration> { |
| 293 | 385 | Some(Duration::from_secs(seconds)) |
| 294 | 386 | } |
| 295 | 387 | |
| 388 | +fn format_duration(duration: Duration) -> String { |
| 389 | + let millis = duration.as_secs_f64() * 1000.0; |
| 390 | + if millis >= 1000.0 { |
| 391 | + format!("{:.2}s", duration.as_secs_f64()) |
| 392 | + } else { |
| 393 | + format!("{millis:.1}ms") |
| 394 | + } |
| 395 | +} |
| 396 | + |
| 296 | 397 | fn escape_html(text: &str) -> String { |
| 297 | 398 | text.replace('&', "&") |
| 298 | 399 | .replace('<', "<") |