@@ -54,6 +54,7 @@ fn parity_corpus() { |
| 54 | } | 54 | } |
| 55 | case_reports.push((case, report)); | 55 | case_reports.push((case, report)); |
| 56 | } | 56 | } |
| | 57 | + print_timing_summary(started.elapsed(), &case_reports); |
| 57 | | 58 | |
| 58 | if let Some(dir) = artifact_dir.as_ref() { | 59 | if let Some(dir) = artifact_dir.as_ref() { |
| 59 | write_index_artifact(dir, &case_reports).expect("write parity index"); | 60 | write_index_artifact(dir, &case_reports).expect("write parity index"); |
@@ -80,24 +81,40 @@ fn parity_corpus() { |
| 80 | #[derive(Debug)] | 81 | #[derive(Debug)] |
| 81 | struct CaseStep { | 82 | struct CaseStep { |
| 82 | name: &'static str, | 83 | name: &'static str, |
| | 84 | + duration: Duration, |
| 83 | error: Option<String>, | 85 | error: Option<String>, |
| 84 | } | 86 | } |
| 85 | | 87 | |
| 86 | #[derive(Debug, Default)] | 88 | #[derive(Debug, Default)] |
| 87 | struct CaseReport { | 89 | struct CaseReport { |
| 88 | steps: Vec<CaseStep>, | 90 | steps: Vec<CaseStep>, |
| | 91 | + elapsed: Duration, |
| 89 | } | 92 | } |
| 90 | | 93 | |
| 91 | impl CaseReport { | 94 | impl CaseReport { |
| 92 | fn push(&mut self, name: &'static str, result: Result<(), String>) -> bool { | 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 | match result { | 105 | match result { |
| 94 | Ok(()) => { | 106 | Ok(()) => { |
| 95 | - self.steps.push(CaseStep { name, error: None }); | 107 | + self.steps.push(CaseStep { |
| | 108 | + name, |
| | 109 | + duration, |
| | 110 | + error: None, |
| | 111 | + }); |
| 96 | true | 112 | true |
| 97 | } | 113 | } |
| 98 | Err(error) => { | 114 | Err(error) => { |
| 99 | self.steps.push(CaseStep { | 115 | self.steps.push(CaseStep { |
| 100 | name, | 116 | name, |
| | 117 | + duration, |
| 101 | error: Some(error), | 118 | error: Some(error), |
| 102 | }); | 119 | }); |
| 103 | false | 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 | fn passed(&self) -> bool { | 138 | fn passed(&self) -> bool { |
| 109 | self.steps.iter().all(|step| step.error.is_none()) | 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 | fn error_message(&self, case_name: &str) -> Option<String> { | 146 | fn error_message(&self, case_name: &str) -> Option<String> { |
| 113 | self.steps.iter().find_map(|step| { | 147 | self.steps.iter().find_map(|step| { |
| 114 | - step.error.as_ref().map(|error| { | 148 | + step.error |
| 115 | - format!("[{case_name}] {} failed:\n{}", step.name, error) | 149 | + .as_ref() |
| 116 | - }) | 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 | fn run_case(case: &LinkCase) -> CaseReport { | 168 | fn run_case(case: &LinkCase) -> CaseReport { |
| | 169 | + let case_started = Instant::now(); |
| 135 | let mut report = CaseReport::default(); | 170 | let mut report = CaseReport::default(); |
| | 171 | + let link_started = Instant::now(); |
| 136 | let outputs = match link_both(case) { | 172 | let outputs = match link_both(case) { |
| 137 | Ok(outputs) => { | 173 | Ok(outputs) => { |
| 138 | - report.push("link", Ok(())); | 174 | + report.push_timed("link", link_started.elapsed(), Ok(())); |
| 139 | outputs | 175 | outputs |
| 140 | } | 176 | } |
| 141 | Err(error) => { | 177 | Err(error) => { |
| 142 | - report.push( | 178 | + report.push_timed( |
| 143 | "link", | 179 | "link", |
| | 180 | + link_started.elapsed(), |
| 144 | Err(format!( | 181 | Err(format!( |
| 145 | "failed to link parity case from {}:\n{}", | 182 | "failed to link parity case from {}:\n{}", |
| 146 | case.dir.display(), | 183 | case.dir.display(), |
| 147 | error | 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( | 191 | + if !report.measure("load-command ids", || { |
| 155 | - "load-command ids", | 192 | + compare_command_ids(&outputs.ours, &outputs.theirs, &case.ignored_load_commands) |
| 156 | - compare_command_ids(&outputs.ours, &outputs.theirs, &case.ignored_load_commands), | 193 | + }) { |
| 157 | - ) { | 194 | + return finish_case(report, case_started); |
| 158 | - return report; | | |
| 159 | } | 195 | } |
| 160 | - if !report.push( | 196 | + if !report.measure("command details", || { |
| 161 | - "command details", | 197 | + compare_command_details(&outputs.ours, &outputs.theirs, &case.command_checks) |
| 162 | - compare_command_details(&outputs.ours, &outputs.theirs, &case.command_checks), | 198 | + }) { |
| 163 | - ) { | 199 | + return finish_case(report, case_started); |
| 164 | - return report; | | |
| 165 | } | 200 | } |
| 166 | - if !report.push( | 201 | + if !report.measure("afs-ld absent commands", || { |
| 167 | - "afs-ld absent commands", | 202 | + ensure_absent_load_commands(&outputs.ours, &case.absent_load_commands, "afs-ld") |
| 168 | - ensure_absent_load_commands(&outputs.ours, &case.absent_load_commands, "afs-ld"), | 203 | + }) { |
| 169 | - ) { | 204 | + return finish_case(report, case_started); |
| 170 | - return report; | | |
| 171 | } | 205 | } |
| 172 | - if !report.push( | 206 | + if !report.measure("Apple absent commands", || { |
| 173 | - "Apple absent commands", | 207 | + ensure_absent_load_commands(&outputs.theirs, &case.absent_load_commands, "Apple ld") |
| 174 | - ensure_absent_load_commands(&outputs.theirs, &case.absent_load_commands, "Apple ld"), | 208 | + }) { |
| 175 | - ) { | 209 | + return finish_case(report, case_started); |
| 176 | - return report; | | |
| 177 | } | 210 | } |
| 178 | - if !report.push( | 211 | + if !report.measure("afs-ld absent sections", || { |
| 179 | - "afs-ld absent sections", | 212 | + ensure_absent_sections(&outputs.ours, &case.absent_sections, "afs-ld") |
| 180 | - ensure_absent_sections(&outputs.ours, &case.absent_sections, "afs-ld"), | 213 | + }) { |
| 181 | - ) { | 214 | + return finish_case(report, case_started); |
| 182 | - return report; | | |
| 183 | } | 215 | } |
| 184 | - if !report.push( | 216 | + if !report.measure("Apple absent sections", || { |
| 185 | - "Apple absent sections", | 217 | + ensure_absent_sections(&outputs.theirs, &case.absent_sections, "Apple ld") |
| 186 | - ensure_absent_sections(&outputs.theirs, &case.absent_sections, "Apple ld"), | 218 | + }) { |
| 187 | - ) { | 219 | + return finish_case(report, case_started); |
| 188 | - return report; | | |
| 189 | } | 220 | } |
| 190 | - if !report.push( | 221 | + if !report.measure("section parity", || { |
| 191 | - "section parity", | | |
| 192 | compare_sections( | 222 | compare_sections( |
| 193 | &outputs.ours, | 223 | &outputs.ours, |
| 194 | &outputs.theirs, | 224 | &outputs.theirs, |
| 195 | &case.section_checks, | 225 | &case.section_checks, |
| 196 | &case.case_tolerances, | 226 | &case.case_tolerances, |
| 197 | - ), | 227 | + ) |
| 198 | - ) { | 228 | + }) { |
| 199 | - return report; | 229 | + return finish_case(report, case_started); |
| 200 | } | 230 | } |
| 201 | - if !report.push( | 231 | + if !report.measure("page-ref parity", || { |
| 202 | - "page-ref parity", | 232 | + compare_page_refs(&outputs.ours, &outputs.theirs, &case.page_ref_checks) |
| 203 | - compare_page_refs(&outputs.ours, &outputs.theirs, &case.page_ref_checks), | 233 | + }) { |
| 204 | - ) { | 234 | + return finish_case(report, case_started); |
| 205 | - return report; | | |
| 206 | } | 235 | } |
| 207 | if !case.runtime_args.is_empty() || case.dir.join("runtime.txt").exists() { | 236 | if !case.runtime_args.is_empty() || case.dir.join("runtime.txt").exists() { |
| 208 | - report.push( | 237 | + report.measure("runtime parity", || { |
| 209 | - "runtime parity", | 238 | + compare_runtime(&outputs.our_path, &outputs.their_path, &case.runtime_args) |
| 210 | - compare_runtime(&outputs.our_path, &outputs.their_path, &case.runtime_args), | 239 | + }); |
| 211 | - ); | | |
| 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 | report | 247 | report |
| 215 | } | 248 | } |
| 216 | | 249 | |
@@ -228,16 +261,22 @@ fn write_case_artifact(dir: &Path, case: &LinkCase, report: &CaseReport) -> Resu |
| 228 | if report.passed() { "ok" } else { "fail" }, | 261 | if report.passed() { "ok" } else { "fail" }, |
| 229 | if report.passed() { "PASS" } else { "FAIL" } | 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 | html.push_str("<h2>Steps</h2><ul>"); | 268 | html.push_str("<h2>Steps</h2><ul>"); |
| 232 | for step in &report.steps { | 269 | for step in &report.steps { |
| 233 | match &step.error { | 270 | match &step.error { |
| 234 | None => html.push_str(&format!( | 271 | None => html.push_str(&format!( |
| 235 | - "<li><span class=\"ok\">PASS</span> {}</li>", | 272 | + "<li><span class=\"ok\">PASS</span> {} <span class=\"time\">{}</span></li>", |
| 236 | - escape_html(step.name) | 273 | + escape_html(step.name), |
| | 274 | + format_duration(step.duration) |
| 237 | )), | 275 | )), |
| 238 | Some(error) => html.push_str(&format!( | 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 | escape_html(step.name), | 278 | escape_html(step.name), |
| | 279 | + format_duration(step.duration), |
| 241 | escape_html(error) | 280 | escape_html(error) |
| 242 | )), | 281 | )), |
| 243 | } | 282 | } |
@@ -258,16 +297,32 @@ fn write_case_artifact(dir: &Path, case: &LinkCase, report: &CaseReport) -> Resu |
| 258 | fn write_index_artifact(dir: &Path, cases: &[(LinkCase, CaseReport)]) -> Result<(), String> { | 297 | fn write_index_artifact(dir: &Path, cases: &[(LinkCase, CaseReport)]) -> Result<(), String> { |
| 259 | let mut html = String::new(); | 298 | let mut html = String::new(); |
| 260 | html.push_str("<!doctype html><html><head><meta charset=\"utf-8\">"); | 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>"); | 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>"); |
| 262 | - html.push_str("<h1>Parity Matrix</h1><ul>"); | 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 | for (case, report) in cases { | 317 | for (case, report) in cases { |
| 264 | let slug = slug(&case.name); | 318 | let slug = slug(&case.name); |
| 265 | html.push_str(&format!( | 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 | slug, | 321 | slug, |
| 268 | escape_html(&case.name), | 322 | escape_html(&case.name), |
| 269 | if report.passed() { "ok" } else { "fail" }, | 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 | html.push_str("</ul></body></html>"); | 328 | html.push_str("</ul></body></html>"); |
@@ -275,6 +330,43 @@ fn write_index_artifact(dir: &Path, cases: &[(LinkCase, CaseReport)]) -> Result< |
| 275 | fs::write(&path, html).map_err(|e| format!("write {}: {e}", path.display())) | 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 | fn slug(name: &str) -> String { | 370 | fn slug(name: &str) -> String { |
| 279 | name.chars() | 371 | name.chars() |
| 280 | .map(|ch| { | 372 | .map(|ch| { |
@@ -293,6 +385,15 @@ fn parity_matrix_time_limit() -> Option<Duration> { |
| 293 | Some(Duration::from_secs(seconds)) | 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 | fn escape_html(text: &str) -> String { | 397 | fn escape_html(text: &str) -> String { |
| 297 | text.replace('&', "&") | 398 | text.replace('&', "&") |
| 298 | .replace('<', "<") | 399 | .replace('<', "<") |