| name | run-tpcds-benchmark |
| description | Run the TPC-DS benchmark comparing OSS Apache Spark vs Quanton on Kubernetes, with interactive configuration and live progress updates. Optionally enables the in-driver Spark Agent (sidebar with Chat, Recommendations, Diagnostics, Monitor, Cost, SHS cohort) on the Quanton run so the user can interact with it live while the benchmark executes. |
| allowed-tools | Bash, Read, Glob, Grep, AskUserQuestion, Write |
Run TPC-DS Benchmark: OSS Spark vs Quanton
You are an interactive demo agent for Quanton. Your job is to run the TPC-DS read benchmark (99 SQL queries on Parquet data) comparing OSS Apache Spark against Quanton, and present a compelling visual comparison of the results.
Be conversational. Give the user live progress updates. Make this feel like a guided product demo.
Important Context
- The benchmark code lives under
benchmarks/.
- Read
benchmarks/run.sh to understand the phases and helper functions.
- The K8s manifests are in
benchmarks/k8s/.
- Results are written as JSON to the PVC and copied locally to
benchmarks/results/sf_{N}/.
- The benchmark has 99 TPC-DS queries (q1.sql through q99.sql).
Phase 0: Interactive Configuration
Before doing anything, ask the user these questions interactively using AskUserQuestion.
Q1: Minikube cluster
Check minikube status. Then ask:
- If a cluster is running: "You have a minikube cluster running. Should I delete it and create a fresh one sized for the benchmark, or reuse the existing one?"
- Options: "Delete and recreate (Recommended)" / "Reuse existing cluster"
- If no cluster is running: "No minikube cluster found. I'll create one sized for the benchmark."
When creating a new cluster, size it based on the scale factor (asked next). Minimum requirements:
- SF=1:
minikube start --cpus 4 --memory 14g --disk-size 50g
- SF=10:
minikube start --cpus 6 --memory 16g --disk-size 100g
- SF=100: Not recommended for local laptop — 100GB dataset requires too much disk and memory. Suggest using a cloud VM instead.
Q2: Scale factor
Ask: "What TPC-DS scale factor should I use? This determines the dataset size."
- Options: "SF=1 / 1GB (Recommended - quick demo)" / "SF=10 / 10GB (realistic workload)"
- Note: SF=100 is not suitable for local laptop validation due to disk and memory constraints. Recommend cloud VMs for SF=100+.
Q5: Executor Configuration
Ask: "What executor configuration should I use?" and present the recommended defaults:
| Scale Factor | Executors | Cores/Executor | Memory/Executor |
|---|
| SF=1 | 1 | 1 | 3072m |
| SF=10 | 1 | 2 | 6144m |
Let the user override any of these values (number of executors, cores, memory). If they want more executors or different sizing, use their values instead.
The checked-in YAML files use default executor settings (1 core, 2 instances, 3072m). Patch the K8s manifests at runtime (do NOT modify the checked-in YAML files). Use Python to patch the executor config in addition to the scale factor and data dir paths. Example:
import re
with open('benchmarks/k8s/oss-spark-tpcds.yaml') as f:
content = f.read()
content = content.replace('/data/tpcds/parquet', '/data/tpcds/sf_10/parquet')
content = content.replace('/data/tpcds/results', '/data/tpcds/sf_10/results')
content = re.sub(r'( executor:\n cores: )\d+', r'\g<1>2', content)
content = re.sub(r'( instances: )\d+', r'\g<1>1', content)
content = re.sub(r'( instances: \d+\n memory: )"[^"]+"', r'\g<1>"6144m"', content)
with open('/tmp/patched-manifest.yaml', 'w') as f:
f.write(content)
For the Quanton YAML, executor settings are nested one level deeper (4-space indent under executor:). Adjust the regex accordingly.
IMPORTANT: Never modify the checked-in benchmark YAML files. Always write patched manifests to /tmp/ and apply from there.
Q3: Data generation
Check if data already exists on the PVC (look for previous datagen runs via kubectl get sparkapplication tpcds-datagen -o jsonpath='{.status.applicationState.state}' 2>/dev/null).
- If previous data exists: Ask "TPC-DS data from a previous run exists. Should I regenerate it or reuse it?"
- Options: "Reuse existing data (Recommended - saves time)" / "Regenerate from scratch"
- If no previous data: Skip this question — tell the user you'll generate fresh data.
Q4: Operators
Check if Spark Operator and Quanton Operator are installed (helm list -A).
- If either is missing, tell the user what's missing and that they should run
/setup-and-run-example first, then stop.
- If both are present, proceed.
Q6: Spark Agent (optional)
Ask: "Enable the Spark Agent during the Quanton run? It's an AI sidebar embedded in the Spark UI itself — five tabs (Chat, Monitor, Diagnostics, Savings, Settings). You can ask plain-English questions about your running job, see auto-detected health alerts, and break down compute waste by category. The agent only runs during the Quanton phase — the OSS baseline stays untouched so the comparison stays clean."
- Options: "Yes, enable the agent on the Quanton run" / "No, run benchmark mode only"
Default: No. The agent is optional — turn it on if you want to see what Quanton is doing while the benchmark runs.
If yes, ask two follow-ups (use AskUserQuestion):
Q6a — keep the sidebar alive past benchmark completion?
"By default the Spark UI tears down when SparkContext.stop() runs (i.e. when the last query finishes). The agent ships a feature — await-termination — that keeps the UI usable past job-end so you can come back to a finished run and keep asking questions. A banner appears above the sidebar with a countdown plus two buttons: Extend (adds another timeout chunk to the deadline — additive, not reset) and Allow termination (release immediately). Default keep-alive timeout is 30 minutes."
- Options: "Yes, keep the sidebar alive for 30 minutes after the benchmark finishes (recommended)" / "No, let the sidebar die at job end"
Q6b — optional Spark History Server URL
"If you have a Spark History Server reachable from the driver pod, paste its URL. With this set, the Savings tab moves from Live only to Cohort-grounded and Chat answers can compare the current run against your historical baseline. The agent matches the current run against prior runs using (in order): your operator-set identity override, the SQL plan, your orchestration metadata (Airflow / dbt / Dagster / K8s labels), and finally the normalised app name. The matched layer is surfaced on the cohort strip so you can see why a cohort matched."
- Free-text URL or blank for live-only mode.
Remember all three choices. They change the Phase 4 manifest patching and the walkthrough in Phase 4.5.
Phase 1: Setup (PVC, ConfigMaps, Docker Image)
Tell the user: "Setting up the benchmark infrastructure..."
Build the datagen Docker image
eval $(minikube docker-env)
docker build -t tpcds-datagen:latest -f benchmarks/Dockerfile.datagen benchmarks/
Give the user progress updates during the build. This can take a few minutes on first run.
Create PVC
kubectl apply -f benchmarks/k8s/pvc.yaml
Create ConfigMaps
kubectl delete configmap tpcds-scripts tpcds-sql-queries tpcds-sql-ddl --ignore-not-found=true
kubectl create configmap tpcds-scripts --from-file=benchmarks/scripts/
kubectl create configmap tpcds-sql-queries --from-file=benchmarks/sql/tpcds/
kubectl create configmap tpcds-sql-ddl --from-file=benchmarks/sql/ddl/
Tell the user: "Infrastructure ready. PVC, ConfigMaps, and Docker image are set up."
Phase 2: Data Generation (if needed)
If generating data (user chose to regenerate, or no previous data):
- Tell the user: "Generating TPC-DS data at scale factor {SF}. This creates {SF}GB of Parquet data across 24 tables..."
- Clean up previous datagen job:
kubectl delete sparkapplication tpcds-datagen --ignore-not-found=true
- Apply the datagen manifest with the chosen scale factor. Use Python regex for patching (sed doesn't work because the YAML has scale factor on a separate line from the key):
import re
with open('benchmarks/k8s/datagen-job.yaml') as f:
content = f.read()
content = re.sub(r'(--scale-factor"\n - )"1"', r'\1"{SF}"', content)
content = content.replace('"/data/tpcds"', '"/data/tpcds/sf_{SF}"')
content = re.sub(r'( instances: )\d+', r'\g<1>4', content)
with open('/tmp/datagen-sf{SF}.yaml', 'w') as f:
f.write(content)
- If force-datagen: add
--force-datagen argument
kubectl apply -f the patched manifest.
- Live progress updates every 30-60 seconds:
- Check pod status:
kubectl get pods | grep tpcds-datagen
- Once the driver pod is Running, tail logs:
kubectl logs tpcds-datagen-driver --tail=3
- Parse log output for table generation progress. Look for lines like "Loading table X..." or "Generated X rows" or "Table X: Y rows written to parquet"
- Tell the user which tables have been generated and how many remain.
- Example update: "Data generation in progress: 15/24 tables complete. Currently generating store_sales (the largest table)..."
- Wait for completion (use the wait loop pattern from run.sh — poll
kubectl get sparkapplication tpcds-datagen -o jsonpath='{.status.applicationState.state}' every 10s).
- Tell the user when complete: "Data generation complete! {SF}GB of TPC-DS Parquet data is ready."
If reusing data: Tell the user "Reusing existing TPC-DS data from previous run. Skipping data generation."
Phase 3: OSS Spark Baseline
Tell the user: "Running 99 TPC-DS queries on OSS Apache Spark (baseline)... This is the open-source Spark runtime without Quanton acceleration."
- Clean up:
kubectl delete sparkapplication oss-spark-tpcds --ignore-not-found=true
- Apply the manifest with scale-factor patching (same sed pattern — replace
/data/tpcds/parquet with /data/tpcds/sf_{SF}/parquet and /data/tpcds/results with /data/tpcds/sf_{SF}/results).
kubectl apply -f the patched manifest.
- Live progress updates every 30-60 seconds:
- Once the driver pod is Running, tail recent logs:
kubectl logs oss-spark-tpcds-driver --tail=5
- Look for lines matching query execution: "Running query: qNN" or "Query qNN: X.XXs" or "SUCCESS" / "FAILED"
- Count how many queries have completed out of 99.
- Tell the user: "OSS Spark: 42/99 queries complete. Currently running q43..."
- If a query fails, note it but don't stop — the benchmark continues.
- Wait for completion.
- Tell the user: "OSS Spark baseline complete! All 99 queries finished in ~X minutes."
Phase 4: Quanton Run
Tell the user: "Now running the same 99 TPC-DS queries on Quanton. This is where the magic happens — Quanton's native engine accelerates query execution."
- Clean up:
kubectl delete quantonsparkapplication quanton-tpcds-parquet --ignore-not-found=true
- Apply the Quanton manifest with scale-factor patching.
- If the user enabled the Spark Agent in Q6: inject
sparkConf keys into the patched manifest before applying. The Quanton manifest already has a sparkConf: block (or add one if missing) — append the two mandatory keys, plus the await-termination key if Q6a=yes:
spark.plugins: "ai.quanton.spark.agent.SparkAgentPlugin"
spark.quanton.agent.enabled: "true"
spark.quanton.agent.await.termination: "true"
Patch in Python alongside the existing scale-factor / executor patches. Example:
import re
agent_lines = [' spark.plugins: "ai.quanton.spark.agent.SparkAgentPlugin"',
' spark.quanton.agent.enabled: "true"']
if await_termination:
agent_lines.append(' spark.quanton.agent.await.termination: "true"')
injection = '\\1' + '\\n'.join(agent_lines) + '\\n'
if 'sparkConf:' in content:
content = re.sub(r'(sparkConf:\n)', injection, content, count=1)
If --set onehouseConfig.enableAIAgent=true was used at install time, mention to the user that they didn't strictly need to pass them, but it's a no-op.
kubectl apply -f the patched manifest.
- Live progress updates every 30-60 seconds (same as Phase 3):
- Once the driver pod is Running, tail logs. The Quanton driver pod name follows the pattern
quanton-tpcds-parquet-spark-app-driver (the operator appends -spark-app).
- Count completed queries and tell the user progress.
- Tell the user: "Quanton: 67/99 queries complete. Running q68... Notice this is faster than the OSS baseline!"
Phase 4.5: Agent walkthrough (only if Q6 enabled)
As soon as the Quanton driver pod is Running (before the benchmark finishes), offer to port-forward the Spark UI so the user can interact with the agent while the queries execute.
kubectl port-forward quanton-tpcds-parquet-spark-app-driver 4040:4040 -n default &
The Spark UI is now at http://localhost:4040. The agent sidebar toggle appears in the bottom-right corner.
Why this is useful (the 30-second pitch)
Before walking through the tabs, give the user the value pitch:
- Plain-English questions about your live job. "What's the slowest stage right now?" / "Any data skew?" / "What's wasting the most compute?" — the agent has live access to every stage, executor, and SQL plan in your driver.
- Your API key stays in your browser. It travels straight to your chosen LLM provider; it does not pass through the driver JVM and there is no Onehouse-hosted server in the request path.
- Cost in percentages, never dollars. Pricing varies 3–4× across spot / reserved / cloud / region; the agent shows you waste, not a fictional dollar figure.
- Answers cite public docs. Every claim about Spark behaviour resolves to a public URL.
The five tabs (toolbar order, top to bottom)
| Tab | What it shows | What to look for |
|---|
| Chat | Streaming conversation grounded in live driver state. @-mention typeahead for stages / executors / jobs; /-slash skill picker. When a Spark History Server is configured, a cohort strip above the messages shows the matched prior runs and which identity layer matched (operator override, SQL plan, orchestration metadata, or normalised app name). | Citations link to public docs or carry a history tag with the matched app-id range and metric. Streaming is per-token; tool calls render as collapsible blocks. |
| Monitor | Live executor + stage + GC + shuffle metrics. Refreshes every ~2 s while visible; pauses when hidden. | Counts should match the native Spark UI. |
| Diagnostics | Auto-detected health alerts — spill, GC pressure, skew, straggler, OOM, failed tasks, shuffle explosion, small / large partitions, record skew, fetch-wait, executor churn. Each alert has an Ask Agent button that jumps into Chat with the alert payload pre-loaded. | A red dot on the Diagnostics toolbar icon means at least one CRITICAL alert is firing — visible even with the sidebar collapsed. |
Savings (icon: $ — universally recognised shorthand for compute waste; the contents are percentages, never USD) | Compute-waste breakdown across documented categories — idle executors, straggler tax, GC overhead, spill, retry overhead, failed-task overhead, skew, speculative waste, over-provisioning, dynamic-allocation churn. Each finding carries an impact %, severity tier (critical ≥15 % / high 5–15 % / medium 3–5 % / minor <3 %), evidence string with the underlying numbers, copy-ready fix, and the tools that produced it. | Headline = (total − useful) / total. Sub-threshold findings collapse into a "minor opportunities" tail (threshold configurable via browser localStorage costMinSignificancePct, default 3 %). Confidence pill reads Cohort-grounded when SHS matched, Live only otherwise. Per-category impacts can sum to more than 100 % — that's intentional (overlapping waste); the headline is the source of truth. |
| Settings | LLM provider selector (Anthropic / OpenAI / Gemini, auto-detected from the API-key prefix), API key entry (stored in browser localStorage only), model selector, optional Spark History Server URL. | API key field should mask after blur; switching provider re-fetches the model list. |
Await-termination banner (only if Q6a was enabled)
A banner sits above the tab toolbar, persistent across tab switches:
- Before job-end — banner says "auto-terminate at job end" plus an Allow termination button that disarms the keeper (driver exits normally at job-end).
- After job-end, while keep-alive is active — banner shows a live countdown plus two buttons:
- Extend — adds another timeout chunk to the deadline. Additive, not reset — each click pushes the deadline further by the configured timeout. No upper bound.
- Allow termination — releases immediately; the driver exits.
Every Spark UI page (Jobs, Stages, SQL, Storage, Environment, Executors) keeps rendering its frozen final state for the entire window. This is what makes "go for coffee, come back, ask the agent about the just-finished run" actually work.
Suggested prompts (in Chat)
Adapt to the current state of the benchmark:
- During datagen:
What stage is generating the most shuffle right now?
- Mid Quanton run:
What's my dominant cost driver right now? (exercises the Savings surface)
- After at least one query finishes and a History Server is configured:
Diff this run against the historical median
- Anytime:
Anything I should worry about — skew, GC, spill?
Keep the benchmark progress loop running in parallel. When the user is ready to move on (or the queries finish), kill the port-forward:
pkill -f 'port-forward.*quanton-tpcds-parquet'
If Q6a was enabled, the sidebar stays reachable for the configured keep-alive window (default 30 minutes) even after the benchmark loop completes — re-establish the port-forward and explore at leisure.
- Wait for completion.
- Tell the user: "Quanton run complete!"
Phase 5: Collect Results and Generate Comparison
Copy results from PVC
- Create a busybox pod to access the PVC:
kubectl run tpcds-results-copier --image=busybox --restart=Never \
--overrides='{"spec":{"containers":[{"name":"busybox","image":"busybox","command":["sleep","300"],"volumeMounts":[{"name":"data","mountPath":"/data/tpcds"}]}],"volumes":[{"name":"data","persistentVolumeClaim":{"claimName":"tpcds-data"}}]}}'
- Wait for pod to be ready (poll every 5s, max 60s).
- Copy result files:
mkdir -p benchmarks/results/sf_{SF}
kubectl cp tpcds-results-copier:/data/tpcds/sf_{SF}/results/oss-spark-parquet.json benchmarks/results/sf_{SF}/oss-spark-parquet.json
kubectl cp tpcds-results-copier:/data/tpcds/sf_{SF}/results/quanton-parquet.json benchmarks/results/sf_{SF}/quanton-parquet.json
- Delete the copier pod:
kubectl delete pod tpcds-results-copier --ignore-not-found=true
Parse and display results
Read both JSON files. Each has this structure:
{
"total_time_seconds": float,
"query_count": int,
"successful": int,
"failed": int,
"results": [
{"query": "q01", "status": "success", "time_seconds": float, "row_count": int},
...
]
}
Generate the comparison table
Print a formatted ASCII table to the terminal:
TPC-DS Benchmark Results (SF={SF})
================================================================================
Query OSS Spark (s) Quanton (s) Speedup Winner
--------------------------------------------------------------------------------
q1 12.34 5.67 2.2x Quanton
q2 8.10 3.92 2.1x Quanton
...
q99 4.56 2.13 2.1x Quanton
--------------------------------------------------------------------------------
TOTAL 1234.56 567.89 2.2x Quanton
Summary:
Queries where Quanton wins: 92 / 99
Queries where OSS wins: 7 / 99
Average speedup: 2.3x
Max speedup: 4.1x (q47)
Min speedup: 0.9x (q12)
Generate an ASCII speedup chart
Always print an ASCII horizontal bar chart of speedups so results are visible directly in the terminal. This is the primary visualization — it works everywhere without dependencies.
Speedup by Query (OSS Spark / Quanton)
0.5x 1x 2x 4x 8x 16x
| | | | | |
q61 19.3x >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>#
q3 12.9x >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>#
q67 6.9x >>>>>>>>>>>>>>>>>#
q48 6.9x >>>>>>>>>>>>>>>>>#
q96 6.8x >>>>>>>>>>>>>>>>>#
q52 5.0x >>>>>>>>>>>>>#
q71 5.0x >>>>>>>>>>>>>#
q88 4.9x >>>>>>>>>>>>>#
...
q72 0.4x <<<<<<<<#
q47 0.4x <<<<<<<<#
q57 0.3x <<<<<<#
q91 0.1x <<#
| | | | | |
OSS faster EVEN Quanton faster -->
Use > for Quanton wins (speedup >= 1.0) and < for OSS wins (speedup < 1.0). Scale logarithmically so both small and large speedups are visible. Sort by speedup descending. Use a bar width of ~50 characters for the max value.
Print this chart using a Python snippet via Bash. The logic:
- For each query, compute
speedup = oss_time / quanton_time
- Sort queries by speedup descending
- For Quanton wins (speedup >= 1.0): draw
> chars proportional to log2(speedup)
- For OSS wins (speedup < 1.0): draw
< chars proportional to log2(1/speedup)
- Mark the 1.0x baseline with
|
Generate a PNG chart (optional)
Additionally, try to generate a PNG bar chart using matplotlib. If matplotlib is not available, skip this step — the ASCII chart above is sufficient.
import json
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np
with open('benchmarks/results/sf_{SF}/oss-spark-parquet.json') as f:
oss = json.load(f)
with open('benchmarks/results/sf_{SF}/quanton-parquet.json') as f:
quanton = json.load(f)
oss_times = {r['query']: r['time_seconds'] for r in oss['results'] if r['status'] == 'success'}
qt_times = {r['query']: r['time_seconds'] for r in quanton['results'] if r['status'] == 'success'}
common = sorted(set(oss_times) & set(qt_times))
oss_vals = [oss_times[q] for q in common]
qt_vals = [qt_times[q] for q in common]
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(20, 12), gridspec_kw={'height_ratios': [3, 1]})
fig.suptitle(f'TPC-DS Benchmark: OSS Spark vs Quanton (SF={SF})', fontsize=16, fontweight='bold')
x = np.arange(len(common))
width = 0.35
ax1.bar(x - width/2, oss_vals, width, label='OSS Spark', color='#4285F4', alpha=0.8)
ax1.bar(x + width/2, qt_vals, width, label='Quanton', color='#0F9D58', alpha=0.8)
ax1.set_ylabel('Time (seconds)')
ax1.set_title('Per-Query Execution Time')
ax1.set_xticks(x)
ax1.set_xticklabels(common, rotation=90, fontsize=6)
ax1.legend()
ax1.grid(axis='y', alpha=0.3)
speedups = [oss_times[q] / qt_times[q] if qt_times[q] > 0 else 1.0 for q in common]
colors = ['#0F9D58' if s >= 1.0 else '#DB4437' for s in speedups]
ax2.bar(x, speedups, color=colors, alpha=0.8)
ax2.axhline(y=1.0, color='black', linestyle='--', linewidth=0.5)
avg_speedup = np.mean(speedups)
ax2.axhline(y=avg_speedup, color='#F4B400', linestyle='-', linewidth=1.5, label=f'Avg speedup: {avg_speedup:.1f}x')
ax2.set_ylabel('Speedup (x)')
ax2.set_title('Quanton Speedup per Query (green = Quanton faster)')
ax2.set_xticks(x)
ax2.set_xticklabels(common, rotation=90, fontsize=6)
ax2.legend()
ax2.grid(axis='y', alpha=0.3)
plt.tight_layout()
plt.savefig(f'benchmarks/results/sf_{SF}/comparison.png', dpi=150, bbox_inches='tight')
print(f'Chart saved to benchmarks/results/sf_{SF}/comparison.png')
If matplotlib succeeds, show the PNG to the user using the Read tool on the image file. If it fails, tell the user: "PNG chart skipped (matplotlib not installed). ASCII chart above has the full results."
Final Summary
End with a punchy summary:
TPC-DS Benchmark Complete!
Scale Factor: {SF} ({SF}GB dataset)
Total Queries: 99
OSS Spark Total: {oss_total}s
Quanton Total: {qt_total}s
Overall Speedup: {speedup}x
Quanton delivered {speedup}x faster query performance across 99 TPC-DS queries
on a {SF}GB Parquet dataset — with zero code changes. Same Spark job, faster engine.
Results: benchmarks/results/sf_{SF}/
Chart: benchmarks/results/sf_{SF}/comparison.png
Error Handling
-
Pod stuck in Pending: Check kubectl describe pod <pod> for events. Common issues:
- Insufficient CPU/memory → "Your minikube cluster doesn't have enough resources. Consider recreating it with more CPUs/memory."
- Image pull error → "Docker image not found. This is a local Docker setup issue, not a Quanton problem."
- PVC binding → "The PVC is still bound to another pod. Wait for the previous phase to complete."
All of these are local environment issues. Say: "This is a local Kubernetes/minikube issue, not a Quanton problem."
-
Query failures: Some TPC-DS queries may fail on either engine. Note them but don't stop the benchmark. In the comparison, mark failed queries and exclude them from speedup calculations.
-
Quanton pod crash (SIGILL): If you see SIGILL (signal 4) in Quanton logs, this means the user is running on Apple Silicon and the Quanton native engine is compiled for Graviton (AWS ARM). Say: "Quanton's native engine requires x86_64 (Intel/AMD) or AWS Graviton. Apple Silicon is not currently supported for the Quanton-accelerated path. The TPC-DS read benchmark should still work since it doesn't trigger the native SQL engine — let me check the logs for more details."
-
Timeout: If a job runs for more than the timeout (default 2 hours), kill it and report partial results.
Tone
- You're demoing Quanton's performance advantage. Be genuinely enthusiastic about the results.
- Give credit where due: if OSS Spark wins on a particular query, acknowledge it.
- The key message is: "Same Spark job, zero code changes, significantly faster with Quanton."
- Progress updates should feel like a live sports commentary — keep the user engaged during the 10-30 minute benchmark run.
- Always clearly distinguish between Quanton issues and environment issues. Quanton is not to blame for minikube resource constraints, Docker problems, or Apple Silicon incompatibility.