Feat: Drop report.txt and use results_summary.json#353
Conversation
|
MLCommons CLA bot All contributors have signed the MLCommons CLA ✍️ ✅ |
There was a problem hiding this comment.
Code Review
This pull request introduces a new truncate-results CLI command to shrink large benchmark results files by keeping a few full responses and hashing the rest. It also optimizes artifact generation by only running the event logger and saving event logs or sample index maps when accuracy scoring is active or when explicitly requested via configuration flags. Additionally, derived throughput metrics (qps and tps) are now directly serialized into the JSON report. The review feedback suggests several robust improvements: implementing atomic file writes and better error handling in the truncation command, wrapping the tmpfs salvage process in a try-except block to ensure resource cleanup, and using msgspec.structs.asdict for a more efficient struct-to-dict conversion.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| if config.in_place: | ||
| out_path = config.results | ||
| elif config.output is not None: | ||
| out_path = config.output | ||
| else: | ||
| out_path = config.results.with_name(config.results.stem + ".truncated.json") | ||
|
|
||
| out_path.write_text(json.dumps(truncated, indent=2)) |
There was a problem hiding this comment.
Writing directly to the target file is risky, especially for large files (which can be gigabytes in size). If the write operation is interrupted or fails due to disk space issues, the original file will be corrupted or lost. Writing to a temporary file first and then atomically replacing the target file prevents corruption.
if config.in_place:
out_path = config.results
elif config.output is not None:
out_path = config.output
else:
out_path = config.results.with_name(config.results.stem + ".truncated.json")
temp_path = out_path.with_suffix(".tmp")
try:
temp_path.write_text(json.dumps(truncated, indent=2))
temp_path.replace(out_path)
except Exception as e:
if temp_path.exists():
temp_path.unlink()
raise e| _salvage_tmpfs( | ||
| ctx.report_dir, | ||
| bench.tmpfs_dir, | ||
| keep_events=ctx.config.keep_events_artifacts, | ||
| ) |
There was a problem hiding this comment.
If _salvage_tmpfs raises an exception (e.g., due to a destination disk-full or permission error during shutil.copy2), the subsequent shutil.rmtree call will be skipped. This will leak the multi-gigabyte tmpfs_dir in RAM (/dev/shm), potentially leading to RAM exhaustion on subsequent runs. Wrapping the salvage call in a try-except block ensures cleanup always runs.
| _salvage_tmpfs( | |
| ctx.report_dir, | |
| bench.tmpfs_dir, | |
| keep_events=ctx.config.keep_events_artifacts, | |
| ) | |
| try: | |
| _salvage_tmpfs( | |
| ctx.report_dir, | |
| bench.tmpfs_dir, | |
| keep_events=ctx.config.keep_events_artifacts, | |
| ) | |
| except Exception as e: | |
| logger.warning(f"Failed to salvage tmpfs: {e}") |
| if not config.results.exists(): | ||
| raise InputValidationError(f"Results file not found: {config.results}") |
There was a problem hiding this comment.
Using is_file() is safer than exists() because it ensures the path is a regular file and not a directory, preventing potential IsADirectoryError exceptions when calling read_text().
| if not config.results.exists(): | |
| raise InputValidationError(f"Results file not found: {config.results}") | |
| if not config.results.is_file(): | |
| raise InputValidationError(f"Results file not found or is not a file: {config.results}") |
| if not config.results.exists(): | ||
| raise InputValidationError(f"Results file not found: {config.results}") | ||
|
|
||
| data = json.loads(config.results.read_text()) |
There was a problem hiding this comment.
If the results file is empty or contains malformed JSON, json.loads will raise a json.JSONDecodeError, which propagates as an unhandled exception and prints a full traceback. Handling this gracefully and raising an InputValidationError provides a much better user experience.
try:
data = json.loads(config.results.read_text())
except json.JSONDecodeError as e:
raise InputValidationError(f"Invalid JSON in results file: {e}") from e| # JSON is self-complete (consumers don't recompute qps/tps from | ||
| # duration + counts). They are methods, not stored fields, to avoid | ||
| # duplicating data already present in duration_ns / counters / osl. | ||
| payload: dict[str, Any] = msgspec.json.decode(msgspec.json.encode(self)) |
There was a problem hiding this comment.
Using msgspec.json.decode(msgspec.json.encode(self)) to convert a msgspec.Struct to a dictionary introduces unnecessary serialization and deserialization overhead. Using msgspec.structs.asdict is the standard, high-performance way to convert a struct to a dictionary.
| payload: dict[str, Any] = msgspec.json.decode(msgspec.json.encode(self)) | |
| import msgspec | |
| payload = msgspec.structs.asdict(self) |
result_summary.json is now the single source of truth for the run summary. Report.to_json injects the derived qps/tps so the JSON is self-complete and consumers no longer recompute them from duration + counts. report.txt (a human-readable dump of the same Report, with no programmatic consumers) is no longer written — the same summary is still logged to the console. Tests: result_summary serializes qps/tps (and null when there is no duration); an end-to-end perf run confirms result_summary.json is self-complete and no report.txt is produced. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7a76518 to
8557e26
Compare
Asserting that report.txt is *not* written guards nothing meaningful — the code simply no longer writes it. Keep the assertions that matter (result_summary.json carries qps/tps, i.e. it is self-complete) and rename the test to test_result_summary_self_complete. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the `msgspec.json.decode(msgspec.json.encode(self))` round-trip in Report.to_json with `msgspec.to_builtins(self)` — same wire-shaped dict (honors the encoder config, so the JSON schema is unchanged) without the encode→decode→encode detour. Addresses the reviewer note on this line; note that the suggested `msgspec.structs.asdict` would NOT be schema-safe (it uses Python attribute names rather than the encoder's field names). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Compute qps/tps once in Report.from_snapshot and store them as struct fields (default None) so they serialize naturally — to_json is a plain encode again, with no to_builtins/dict round-trip and no method-vs-JSON-key divergence (msgspec.json.encode(report) now matches report.to_json()). qps()/tps() methods are dropped; display() and execute.py read the attributes. Production only builds Report via from_snapshot, so the fields are always computed there. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Narrowed scope: this PR now covers only the
result_summary.json/report.txtchange.What & why
result_summary.jsonandreport.txtare two renderings of the sameReport.report.txthas no programmatic consumers, andresult_summary.jsonwas missing the computed QPS/TPS (callers had to recompute them from duration + counts).Changes
result_summary.jsonis now self-complete.Report.to_jsoninjects the derivedqps/tpsinto the serialized JSON. They remain methods (not stored fields) to avoid duplicating data already induration_ns/ counters / OSL, so noReportconstructor sites change.report.txtis no longer written. It was a human-readable dump of the sameReportwith zero programmatic/test consumers; the same summary is still logged to the console.docs/LOCAL_TESTING.md—result_summary.jsondescribed as the primary, self-complete report; droppedreport.txtand the never-writtenruntime_settings.jsonfrom the artifacts list.Tests
result_summary.jsonserializesqps/tps(concrete value) and serializes them asnullwhen there is no duration.result_summary.jsonis self-complete and noreport.txtis produced.pre-commitgreen.🤖 Generated with Claude Code