Result Export Formats¶
BenchBox exports benchmark results in multiple formats for analysis, visualization, and integration with external tools.
Export Commands¶
Basic Export¶
# Run benchmark and export results
benchbox run --platform duckdb --benchmark tpch --scale 0.1
# Results are automatically saved to benchmark_runs/results/
ls benchmark_runs/results/
# tpch_duckdb_sf0.01_20251212_143021.json
Export to Other Formats¶
# Export most recent result to CSV
benchbox export --last --format csv
# Export to multiple formats
benchbox export --last --format csv --format html
# Export a specific result file
benchbox export benchmark_runs/results/tpch_duckdb_sf0.01_20251212_143021.json --format csv --format html
Custom Output Directory¶
# Local directory
benchbox run --platform duckdb --benchmark tpch --output ./my_results/
# Cloud storage
benchbox run --platform snowflake --benchmark tpch --output s3://bucket/results/
JSON Format (Schema v2.2)¶
The JSON export is the canonical schema-v2 result bundle containing complete
benchmark details. BenchBox currently writes schema version "2.2" in the
top-level version field.
Consumer policy is intentionally split by use case:
Consumer |
Accepted versions |
Behavior |
|---|---|---|
Producer/exporter |
|
New bundles are written with the current producer version. |
Runtime loader and exporter listing |
|
Unknown versions fail closed and should be re-exported. |
Normalizer |
|
Known v2 bundles use exact v2 field mapping; v1.x and unknown shapes use legacy best-effort extraction. |
Public submission validator |
Numeric |
Forward-compatible for schema-v2 minor versions, but missing or malformed versions are rejected. |
Explorer pipeline input |
|
Unsupported bundles are rejected before explorer read-model projection. |
Schema Structure¶
{
"version": "2.2",
"run": {
"id": "tpch-duckdb-20260521",
"timestamp": "2026-05-21T14:30:21.123456Z",
"total_duration_ms": 45230,
"query_time_ms": 39800
},
"benchmark": {
"id": "tpch",
"name": "TPC-H",
"scale_factor": 0.1
},
"platform": {
"name": "duckdb",
"version": "1.2.0"
},
"config": {
"execution_mode": "sql"
},
"summary": {
"queries": {
"total": 22,
"passed": 22,
"failed": 0
},
"timing": {
"total_ms": 39800,
"avg_ms": 1809.1
},
"validation": "passed",
"tpc_metrics": {
"power_at_size": 89.5
}
},
"phases": {
"data_generation": { "...": "..." },
"schema_creation": { "...": "..." },
"data_loading": { "...": "..." },
"power_test": { "...": "..." }
},
"queries": [
{
"id": "Q1",
"ms": 1520,
"rows": 4,
"iter": 1,
"stream": 0,
"run_type": "measurement",
"status": "SUCCESS"
}
],
"environment": {
"os": "macOS",
"arch": "arm64"
}
}
Field Reference¶
Version¶
Field |
Type |
Description |
|---|---|---|
|
string |
Result bundle schema version. Current producer version is |
Benchmark Block¶
Field |
Type |
Description |
|---|---|---|
|
string |
Benchmark identifier (tpch, tpcds, ssb, etc.) |
|
string |
Display name |
|
float |
Data scale factor |
|
string |
Optional run/test classification |
Platform Block¶
Field |
Type |
Description |
|---|---|---|
|
string |
Platform identifier |
|
string |
Platform/driver version |
|
object |
Optional normalized deployment metadata |
|
object |
Optional normalized environment facets |
Platform-specific extensions should stay inside the existing schema-v2 blocks
where possible. Current canonical locations are platform.* for platform
facets and raw platform metadata, phases.<stage> for lifecycle-stage
summaries, and comparisons.* for cross-engine comparison data. New top-level
keys require a public-contract update and consumer tests.
platform.tuning (optional)¶
Present only when a run resolved a tuning configuration (--tuning other than
notuning producing an empty baseline). All values are self-attested -
they describe what was requested, not an independently verified record of
what physically applied (see docs/development/tuning-adr-001-trust-and-hash-semantics.md).
Field |
Type |
Description |
|---|---|---|
|
string |
Raw |
|
string |
Full 64-hex-char SHA-256 over the requested |
|
string |
ADR-1 honest execution-derived tuning verified-state: |
|
string |
Explicit tuning-policy generation marker (ADR-3 seam), currently |
|
number |
Number of tables with at least one table-level tuning (partitioning/clustering/distribution/sorting). |
|
array |
Sorted list of tuning categories actually active (constraint names, platform optimization flags, table-tuning clause types). |
|
object |
Optional workload-profile coverage metadata (unrelated to the requested-config hash). |
|
string |
Legacy bridge keys, kept for one schema generation so any external consumer of these documented keys keeps working (this is not the explorer ingest pipeline, which reads tuning facets from |
The .tuning.json companion file (same base filename, .tuning.json suffix)
carries the full detail: requested.constraints (primary/foreign key, unique,
and check constraint settings), requested.platform_optimizations
(non-default values only), requested.table_tunings (complete per-table
tuning structure), plus requested_config_hash, tuning_policy_generation,
tuning_source, and source_file (a repo-relative path or
"<basename>:<content-hash>" - never a raw local filesystem path).
Run Block¶
Field |
Type |
Description |
|---|---|---|
|
string |
Run start time (ISO 8601) |
|
string |
Run identifier |
|
number |
Total benchmark duration |
|
number |
Total query execution duration |
Phases Block¶
Phases are keyed by lifecycle stage. Each phase block may carry status, duration, counts, and stage-specific metadata.
{
"data_generation": {
"duration_ms": 5230,
"status": "SUCCESS",
"tables_generated": 8,
"total_rows_generated": 150000
},
"schema_creation": {
"duration_ms": 120,
"status": "SUCCESS",
"tables_created": 8
},
"data_loading": {
"duration_ms": 3500,
"status": "SUCCESS",
"tables_loaded": 8
},
"power_test": {
"duration_ms": 39800,
"status": "SUCCESS"
}
}
Summary Block¶
Field |
Type |
Description |
|---|---|---|
|
int |
Total query records represented in the bundle |
|
int |
Count of passed queries |
|
int |
Count of failed queries |
|
number |
Total query execution time in milliseconds |
|
number |
Average query execution time in milliseconds |
|
string or object |
Validation result summary |
|
object |
Optional TPC-style metrics such as |
Query Execution Details¶
Each query execution record contains:
{
"id": "Q6",
"ms": 234,
"rows": 1,
"iter": 1,
"stream": 0,
"run_type": "measurement",
"status": "SUCCESS",
"row_count_validation": {
"status": "PASSED",
"expected": 1,
"actual": 1
}
}
row_count_validation was introduced in schema 2.2 and is optional. When present, it records the bounded
per-query evidence behind the run-level validation status; SKIPPED or
ERROR evidence cannot support a clean validation pass.
CSV Format¶
CSV export provides tabular query-level data for spreadsheet analysis.
Query Results CSV¶
id,ms,rows,iter,stream,run_type,status
Q1,1520,4,1,0,measurement,SUCCESS
Q2,892,460,1,0,measurement,SUCCESS
Q3,1230,10,1,0,measurement,SUCCESS
...
Summary CSV¶
metric,value
benchmark,tpch
scale_factor,0.1
platform,duckdb
total_time_ms,39800
avg_time_ms,1809.1
power_at_size,89.5
total_duration_ms,45230
HTML Format¶
HTML export generates a standalone report with formatted tables.
# Generate HTML report from most recent result
benchbox export --last --format html
# Export a specific result file to HTML
benchbox export benchmark_runs/results/tpch_duckdb_sf0.01_20251212_143021.json --format html
The HTML report includes:
Summary metrics card
Query results table with timing data
Phase duration breakdown
Validation status table
Platform and configuration details
Visualizing Results¶
Use benchbox visualize to generate ASCII charts from any result file:
# Auto-detect latest result and render all applicable charts
benchbox visualize
# Visualize a specific result file
benchbox visualize benchmark_runs/results/tpch_duckdb_sf0.01_20251212_143021.json
# Specific chart type
benchbox visualize benchmark_runs/results/*.json --chart-type performance_bar
# Save plain-text output to file
benchbox visualize benchmark_runs/results/*.json --no-color > charts.txt
See the Visualization Guide for chart types, templates, and customization options.
Loading Results in Python¶
Load JSON Results¶
import json
from pathlib import Path
# Load result file
result_file = Path("benchmark_runs/results/tpch_duckdb_sf0.01_20251212_143021.json")
with result_file.open() as f:
results = json.load(f)
# Access metrics
print(f"Power at Size: {results['summary']['tpc_metrics']['power_at_size']}")
print(f"Total time: {results['summary']['timing']['total_ms']}ms")
# Access query details
for query in results['queries']:
print(f"{query['id']}: {query['ms']}ms")
Load into Pandas¶
import pandas as pd
import json
# Load JSON
with open("benchmark_runs/results/tpch_duckdb_sf0.01_*.json") as f:
results = json.load(f)
# Convert queries to DataFrame
queries = results['queries']
df = pd.DataFrame(queries)
# Analyze
print(df.describe())
print(df.groupby('id')['ms'].mean())
Load CSV Results¶
import pandas as pd
# Load query results
df = pd.read_csv("benchmark_runs/results/tpch_duckdb_sf0.01_queries.csv")
# Quick analysis
print(f"Total queries: {len(df)}")
print(f"Mean execution time: {df['ms'].mean():.2f}ms")
print(f"Slowest query: {df.loc[df['ms'].idxmax(), 'id']}")
Visualization Examples¶
CLI Visualization¶
# Render all applicable charts for a result file
benchbox visualize benchmark_runs/results/tpch_duckdb_sf0.01_*.json
# Compare multiple platforms
benchbox visualize duckdb_result.json sqlite_result.json --template head_to_head
# Per-query histogram (auto-splits for large benchmarks)
benchbox visualize tpcds_result.json --chart-type query_histogram
Python API Visualization¶
from benchbox.core.visualization import ResultPlotter
from benchbox.core.visualization.ascii import BarChart
from benchbox.core.visualization.ascii.bar_chart import BarData
# Load results from JSON files
plotter = ResultPlotter.from_sources(["results/duckdb.json", "results/sqlite.json"])
# Render a bar chart
bar_data = [BarData(label=r.platform, value=r.total_time_ms or 0) for r in plotter.results]
chart = BarChart(data=bar_data, title="Platform Comparison")
print(chart.render())
# Export to plain-text file
from benchbox.core.visualization.exporters import export_ascii
export_ascii(
ascii_content=chart.render(),
output_dir="./charts",
base_name="platform_comparison",
format="txt",
)
Schema Versioning¶
Current Version: 2.2¶
Schema v2.2 is the current producer version for BenchBox result bundles. It
uses top-level version, run, benchmark, platform, summary, queries,
and optional companion blocks such as phases, environment,
normalized_cost, validation, and comparisons.
Runtime loading and explorer generation intentionally accept only known v2
minor versions ("2.0", "2.1", and "2.2"). The public submission validator accepts
numeric 2.x versions to allow forward-compatible submissions, but it rejects
missing values and malformed strings such as "2.x". Legacy v1.x result shapes
are not runtime-loadable; they are handled only by the normalizer’s best-effort
compatibility path.
Version History¶
Version |
Changes |
|---|---|
2.2 |
Added bounded per-query |
2.1 |
Added typed result and companion metadata used by the previous producer |
2.0 |
First schema-v2 bundle contract consumed by loader, submissions, and explorer |
1.x |
Legacy shape supported only by normalization helpers |
Loading Legacy Results¶
import json
from benchbox.core.results.normalizer import normalize_result_dict
with open("old_result.json") as f:
result = normalize_result_dict(json.load(f))
print(f"Schema version family: {result.schema_version}")
Anonymization¶
Results are anonymized by default to remove sensitive information:
Connection strings → redacted
Hostnames, endpoints, buckets, accounts → replaced by a stable pseudonym
Absolute local paths → replaced by a stable pseudonym
Usernames, IPs, emails → stripped
API keys/tokens → redacted
Identifiers become <kind>_<12 hex> — for example machine_3d46427037d2 — from
a salted SHA-256. The pseudonym is stable for the same input, so results from
one machine group together, and anonymizing an already-anonymized payload is a
no-op rather than a second pseudonym.
A pseudonym is a grouping key only. Because an already-anonymized value passes through unchanged, a submitter can hand-craft one, so it is never a provenance or trust signal — trust labels carry provenance.
Anonymization Config¶
from benchbox.core.results.exporter import ResultExporter
from benchbox.core.results.anonymization import AnonymizationConfig
config = AnonymizationConfig(
# Scopes pseudonyms derived from raw values to your organization, so the
# same machine publishes different pseudonyms under different salts.
# See the salt-rotation note below for what this does *not* cover.
machine_id_salt="your-org-salt",
# Extra regexes stripped from free-text fields, on top of the built-in
# IP / email / SSN patterns.
custom_sanitizers={r"\bacct-\d+\b": "[REDACTED]"},
)
exporter = ResultExporter(anonymize=True, anonymization_config=config)
Default salt and residual confirmation oracle¶
machine_id_salt defaults to empty. With the empty default, anyone who knows
the documented algorithm can confirm candidate values against published
<kind>_<12 hex> tokens. Unread identifier fields are omitted entirely (see
docs/development/adr/adr-published-identifier-field-set.md). Retained fields
(endpoint, database_name, submission_path) still publish pseudonyms, so
the residual oracle on those fields is accepted for the OSS default and
documented rather than denied.
A non-empty salt closes the oracle only if it is not shipped in the public
tree. Operators who will publish community submissions must set
machine_id_salt or the BENCHBOX_MACHINE_ID_SALT environment variable to a
deployment-private value before the first public export, so retained-field
tokens are salted when they are minted. ResultExporter(anonymize=True)
soft-reads BENCHBOX_MACHINE_ID_SALT when present; without it, public-shaped
export still succeeds with the empty default (local/private use).
benchbox submit hard-refuses when that salt env is unset/empty — a
community-path gate so operators cannot forget to configure salt on the
submission machine. That gate does not re-hash already-exported files:
already-public-shaped tokens pass through under the publication fixed point,
so setting salt only at submit time does not close the empty-salt oracle for
bundles that were minted earlier without salt. A repository-baked “default
salt” would still be public and is rejected.
Salt rotation¶
The salt applies when a raw value is hashed. A value that is already a pseudonym passes through unchanged — that is what makes anonymization idempotent — and the pass-through happens before the salt is consulted:
a = AnonymizationManager(AnonymizationConfig(machine_id_salt="org-A"))
b = AnonymizationManager(AnonymizationConfig(machine_id_salt="org-B"))
a.anonymize_result_payload({"machine_id": raw}) # machine_9ba319f754a5
b.anonymize_result_payload({"machine_id": raw}) # machine_ac228f1f75af (differs)
# But B re-anonymizing A's already-published bundle:
b.anonymize_result_payload({"machine_id": "machine_9ba319f754a5"})
# -> machine_9ba319f754a5 (unchanged; B's salt is never applied)
So changing the salt does not re-pseudonymize an already-anonymized corpus. New captures adopt the new salt while stored bundles keep the old pseudonyms, which splits one machine across two identities. Rotating the salt therefore means re-deriving the corpus from the pre-anonymization originals, not just changing the config.
Disable Anonymization¶
Pass anonymize=False to ResultExporter. Exports then contain raw local
paths and host details, so treat them as private and do not submit them.
Integration Examples¶
Export to Data Warehouse¶
import json
import pandas as pd
# Load results
with open("results.json") as f:
results = json.load(f)
# Flatten to table
queries = []
for q in results['queries']:
queries.append({
'run_id': results['run']['id'],
'benchmark': results['benchmark']['id'],
'platform': results['platform']['name'],
'scale_factor': results['benchmark']['scale_factor'],
**q
})
df = pd.DataFrame(queries)
# Upload to warehouse
# df.to_sql('benchmark_queries', engine, if_exists='append')
CI/CD Integration¶
# Run benchmark and check threshold
benchbox run --platform duckdb --benchmark tpch --scale 0.01 \
--output ./results/
# Parse results in CI script
uv run -- python -c "
import json
import sys
with open('results/tpch_duckdb_sf0.01_*.json') as f:
results = json.load(f)
power = results['summary']['tpc_metrics']['power_at_size']
if power < 50: # Performance threshold
print(f'FAIL: Power@Size {power} below threshold 50')
sys.exit(1)
print(f'PASS: Power@Size {power}')
"