|
16 | 16 | git_tracked_paths, |
17 | 17 | validate_provenance, |
18 | 18 | ) |
19 | | -from validate_tracked_json import validate_readme_results |
| 19 | +from validate_tracked_json import ( |
| 20 | + compare_performance_summaries, |
| 21 | + validate_readme_results, |
| 22 | +) |
20 | 23 |
|
21 | 24 |
|
22 | 25 | ROOT = Path(__file__).resolve().parents[1] |
23 | 26 | PROVENANCE_PATH = ROOT / "cortex-m-deployability/results/provenance.json" |
24 | 27 | RESULTS_PATH = ROOT / "cortex-m-deployability/scripts/results.py" |
25 | 28 | README_PATH = ROOT / "cortex-m-deployability/README.md" |
26 | 29 | SUMMARY_PATH = ROOT / "cortex-m-deployability/results/summary.json" |
| 30 | +ESP_SUMMARY_PATH = ROOT / "tflm-esp32s3/results/summary.json" |
27 | 31 | RESULTS_SPEC = importlib.util.spec_from_file_location( |
28 | 32 | "cortex_results", RESULTS_PATH) |
29 | 33 | assert RESULTS_SPEC and RESULTS_SPEC.loader |
@@ -138,6 +142,102 @@ def test_mutated_summary_result_is_rejected(self) -> None: |
138 | 142 | any("| TS | 2.56 ms" in error for error in errors), errors) |
139 | 143 |
|
140 | 144 |
|
| 145 | +class PerformanceRegressionContractTest(unittest.TestCase): |
| 146 | + def setUp(self) -> None: |
| 147 | + self.cortex = json.loads(SUMMARY_PATH.read_text()) |
| 148 | + self.esp = json.loads(ESP_SUMMARY_PATH.read_text()) |
| 149 | + |
| 150 | + @staticmethod |
| 151 | + def cell(document: dict, log_file: str) -> dict: |
| 152 | + return next( |
| 153 | + config for config in document["configs"] |
| 154 | + if config["log_file"] == log_file) |
| 155 | + |
| 156 | + def test_unchanged_snapshots_have_no_deltas(self) -> None: |
| 157 | + for suite, summary in (("cortex-m", self.cortex), ("esp32-s3", self.esp)): |
| 158 | + observations, errors = compare_performance_summaries( |
| 159 | + summary, copy.deepcopy(summary), suite) |
| 160 | + self.assertEqual(observations, []) |
| 161 | + self.assertEqual(errors, []) |
| 162 | + |
| 163 | + def test_material_cycle_regression_is_rejected(self) -> None: |
| 164 | + current = copy.deepcopy(self.cortex) |
| 165 | + cell = self.cell(current, "h753_ds_cnn_cmsis_nn.log") |
| 166 | + cell["latency_median_cycles"] = int( |
| 167 | + cell["latency_median_cycles"] * 1.051) |
| 168 | + observations, errors = compare_performance_summaries( |
| 169 | + current, self.cortex, "cortex-m") |
| 170 | + self.assertTrue(any("median latency" in item for item in observations)) |
| 171 | + self.assertTrue(any("exceeds +5%" in error for error in errors), errors) |
| 172 | + |
| 173 | + def test_timing_noise_below_limit_is_reported_but_allowed(self) -> None: |
| 174 | + current = copy.deepcopy(self.esp) |
| 175 | + cell = self.cell(current, "tigris_i8_espnn.log") |
| 176 | + cell["latency_mean_ms"] *= 1.049 |
| 177 | + observations, errors = compare_performance_summaries( |
| 178 | + current, self.esp, "esp32-s3") |
| 179 | + self.assertTrue(any("mean latency" in item for item in observations)) |
| 180 | + self.assertEqual(errors, []) |
| 181 | + |
| 182 | + def test_material_working_memory_growth_is_rejected(self) -> None: |
| 183 | + current = copy.deepcopy(self.cortex) |
| 184 | + cell = self.cell(current, "h753_ad_cmsis_nn.log") |
| 185 | + cell["sram_peak_bytes"] += 129 |
| 186 | + _, errors = compare_performance_summaries( |
| 187 | + current, self.cortex, "cortex-m") |
| 188 | + self.assertTrue( |
| 189 | + any("all-in working memory" in error for error in errors), errors) |
| 190 | + |
| 191 | + def test_small_absolute_memory_growth_is_reported_but_allowed(self) -> None: |
| 192 | + current = copy.deepcopy(self.cortex) |
| 193 | + cell = self.cell(current, "h753_ad_cmsis_nn.log") |
| 194 | + cell["sram_peak_bytes"] += 64 |
| 195 | + observations, errors = compare_performance_summaries( |
| 196 | + current, self.cortex, "cortex-m") |
| 197 | + self.assertTrue( |
| 198 | + any("all-in working memory" in item for item in observations)) |
| 199 | + self.assertEqual(errors, []) |
| 200 | + |
| 201 | + def test_exact_firmware_growth_is_rejected(self) -> None: |
| 202 | + current = copy.deepcopy(self.cortex) |
| 203 | + provenance = current["provenance"]["cells"] |
| 204 | + artifact = provenance["h753_ds_cnn_cmsis_nn.log"]["artifacts"][ |
| 205 | + "firmware"] |
| 206 | + artifact["size_bytes"] += 4096 |
| 207 | + _, errors = compare_performance_summaries( |
| 208 | + current, self.cortex, "cortex-m") |
| 209 | + self.assertTrue( |
| 210 | + any("firmware artifact" in error for error in errors), errors) |
| 211 | + |
| 212 | + def test_success_to_failure_is_rejected(self) -> None: |
| 213 | + current = copy.deepcopy(self.esp) |
| 214 | + self.cell(current, "tigris_i8_espnn.log")["status"] = "FAILED" |
| 215 | + _, errors = compare_performance_summaries( |
| 216 | + current, self.esp, "esp32-s3") |
| 217 | + self.assertTrue(any("status regressed" in error for error in errors), errors) |
| 218 | + |
| 219 | + def test_removed_baseline_cell_cannot_hide_a_regression(self) -> None: |
| 220 | + current = copy.deepcopy(self.cortex) |
| 221 | + current["configs"] = [ |
| 222 | + cell for cell in current["configs"] |
| 223 | + if cell["log_file"] != "h753_ds_cnn_cmsis_nn.log"] |
| 224 | + _, errors = compare_performance_summaries( |
| 225 | + current, self.cortex, "cortex-m") |
| 226 | + self.assertTrue( |
| 227 | + any("removed baseline cell" in error for error in errors), errors) |
| 228 | + |
| 229 | + def test_removed_baseline_metric_cannot_hide_a_regression(self) -> None: |
| 230 | + current = copy.deepcopy(self.esp) |
| 231 | + del self.cell(current, "tigris_i8_espnn.log")["latency_mean_ms"] |
| 232 | + _, errors = compare_performance_summaries( |
| 233 | + current, self.esp, "esp32-s3") |
| 234 | + self.assertTrue( |
| 235 | + any("removed baseline metric mean latency" in error |
| 236 | + for error in errors), |
| 237 | + errors, |
| 238 | + ) |
| 239 | + |
| 240 | + |
141 | 241 | def valid_capture_provenance() -> dict: |
142 | 242 | revision = "1" * 40 |
143 | 243 | digest = "2" * 64 |
|
0 commit comments