Skip to content

Commit 83336d5

Browse files
brunoborgesCopilot
andcommitted
Restructure benchmark into training/build cost vs steady-state execution
Phase 1: one-time costs (Python __pycache__, JBang export, AOT training) Phase 2: steady-state avg of 5 runs (FAT JAR+AOT, FAT JAR, JBang, Python) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 8418580 commit 83336d5

File tree

3 files changed

+167
-148
lines changed

3 files changed

+167
-148
lines changed

.github/workflows/benchmark.yml

Lines changed: 38 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -28,18 +28,12 @@ jobs:
2828
with:
2929
python-version: '3.13'
3030

31-
- name: Build fat JAR
32-
run: jbang export fatjar --force --output html-generators/generate.jar html-generators/generate.java
33-
34-
- name: Build AOT cache
35-
run: java -XX:AOTCacheOutput=html-generators/generate.aot -jar html-generators/generate.jar
36-
3731
- name: Run benchmark
3832
shell: bash
3933
run: |
4034
JAR="html-generators/generate.jar"
4135
AOT="html-generators/generate.aot"
42-
RUNS=6
36+
STEADY_RUNS=5
4337
4438
snippet_count=$(find content -name '*.json' | wc -l | tr -d ' ')
4539
java_ver=$(java -version 2>&1 | head -1 | sed 's/.*"\(.*\)".*/\1/')
@@ -53,40 +47,53 @@ jobs:
5347
echo "$t"
5448
}
5549
56-
bench() {
57-
local label="$1"; shift
58-
local times=()
59-
for ((i = 1; i <= RUNS; i++)); do
60-
t=$(measure "$@")
61-
times+=("$t")
62-
done
63-
local cold="${times[0]}"
50+
avg_runs() {
51+
local n="$1"; shift
6452
local sum=0
65-
for ((i = 1; i < RUNS; i++)); do
66-
sum=$(awk "BEGIN {print $sum + ${times[$i]}}")
53+
for ((i = 1; i <= n; i++)); do
54+
local t
55+
t=$(measure "$@")
56+
sum=$(awk "BEGIN {print $sum + $t}")
6757
done
68-
local warm
69-
warm=$(awk "BEGIN {printf \"%.2f\", $sum / ($RUNS - 1)}")
70-
echo "${label}|${cold}|${warm}"
58+
awk "BEGIN {printf \"%.2f\", $sum / $n}"
7159
}
7260
7361
echo "Running benchmark on $os_name (Java $java_ver, $snippet_count snippets)..."
7462
75-
aot_result=$(bench "Fat JAR + AOT" java -XX:AOTCache="$AOT" -jar "$JAR")
76-
jar_result=$(bench "Fat JAR" java -jar "$JAR")
77-
jbang_result=$(bench "JBang" jbang html-generators/generate.java)
78-
python_result=$(bench "Python" python3 html-generators/generate.py)
63+
# --- Phase 1: Training / build cost ---
64+
rm -f "$JAR" "$AOT"
65+
find html-generators -name '__pycache__' -type d -exec rm -rf {} + 2>/dev/null || true
66+
67+
PY_TRAIN=$(measure python3 html-generators/generate.py)
68+
JBANG_EXPORT=$(measure jbang export fatjar --force --output "$JAR" html-generators/generate.java)
69+
AOT_TRAIN=$(measure java -XX:AOTCacheOutput="$AOT" -jar "$JAR")
70+
71+
# --- Phase 2: Steady-state execution ---
72+
PY_STEADY=$(avg_runs $STEADY_RUNS python3 html-generators/generate.py)
73+
JBANG_STEADY=$(avg_runs $STEADY_RUNS jbang html-generators/generate.java)
74+
JAR_STEADY=$(avg_runs $STEADY_RUNS java -jar "$JAR")
75+
AOT_STEADY=$(avg_runs $STEADY_RUNS java -XX:AOTCache="$AOT" -jar "$JAR")
7976
8077
# Write to GitHub Actions Job Summary
8178
{
8279
echo "## Benchmark Results — \`$os_name\`"
8380
echo ""
84-
echo "Java $java_ver · $snippet_count snippets · $RUNS runs (1 cold + $((RUNS - 1)) warm)"
81+
echo "Java $java_ver · $snippet_count snippets"
8582
echo ""
86-
echo "| Method | Cold Start | Warm Average |"
87-
echo "|--------|-----------|-------------|"
88-
for result in "$aot_result" "$jar_result" "$jbang_result" "$python_result"; do
89-
IFS='|' read -r label cold warm <<< "$result"
90-
echo "| **$label** | ${cold}s | ${warm}s |"
91-
done
83+
echo "### Phase 1: Training / Build Cost (one-time)"
84+
echo ""
85+
echo "| Step | Time | What it does |"
86+
echo "|------|------|-------------|"
87+
echo "| Python first run | ${PY_TRAIN}s | Interprets source, creates \`__pycache__\` bytecode |"
88+
echo "| JBang export | ${JBANG_EXPORT}s | Compiles source + bundles dependencies into fat JAR |"
89+
echo "| AOT training run | ${AOT_TRAIN}s | Runs JAR once to record class loading, produces \`.aot\` cache |"
90+
echo ""
91+
echo "### Phase 2: Steady-State Execution (avg of $STEADY_RUNS runs)"
92+
echo ""
93+
echo "| Method | Avg Time |"
94+
echo "|--------|---------|"
95+
echo "| **Fat JAR + AOT** | **${AOT_STEADY}s** |"
96+
echo "| **Fat JAR** | ${JAR_STEADY}s |"
97+
echo "| **JBang** | ${JBANG_STEADY}s |"
98+
echo "| **Python** | ${PY_STEADY}s |"
9299
} >> "$GITHUB_STEP_SUMMARY"

html-generators/benchmark/README.md

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,48 @@
11
# Generator Benchmarks
22

3-
Performance comparison of the four ways to run the HTML generator, measured on 95 snippets across 10 categories.
3+
Performance comparison of execution methods for the HTML generator, measured on 95 snippets across 10 categories.
44

5-
## Results
5+
## Phase 1: Training / Build Cost (one-time)
66

7-
| Method | Cold Start | Warm Average | Notes |
8-
|--------|-----------|-------------|-------|
9-
| **Fat JAR + AOT** (`java -XX:AOTCache`) | 0.47s | **0.43s** | Fastest overall; requires one-time cache build |
10-
| **Fat JAR** (`java -jar`) | 0.43s | 0.55s | No setup needed |
11-
| **JBang** (`jbang generate.java`) | 0.93s | 0.98s | Includes JBang overhead |
12-
| **Python** (`python3 generate.py`) | 0.37s | 1.60s | Fast cold start; slowest warm |
7+
These are one-time setup costs, comparable across languages.
138

14-
- **Cold start**: First run after clearing caches / fresh process
15-
- **Warm average**: Mean of 5 subsequent runs
9+
| Step | Time | What it does |
10+
|------|------|-------------|
11+
| Python first run | 2.34s | Interprets source, creates `__pycache__` bytecode |
12+
| JBang export | 2.92s | Compiles source + bundles dependencies into fat JAR |
13+
| AOT training run | 3.14s | Runs JAR once to record class loading, produces `.aot` cache |
14+
15+
## Phase 2: Steady-State Execution (avg of 5 runs)
16+
17+
After one-time setup, these are the per-run execution times.
18+
19+
| Method | Avg Time | Notes |
20+
|--------|---------|-------|
21+
| **Fat JAR + AOT** | **0.35s** | Fastest; pre-loaded classes from AOT cache |
22+
| **Fat JAR** | 0.50s | JVM class loading on every run |
23+
| **JBang** | 1.19s | Includes JBang launcher overhead |
24+
| **Python** | 1.37s | Uses cached `__pycache__` bytecode |
25+
26+
## How It Works
27+
28+
- **Python** caches compiled bytecode in `__pycache__/` after the first run, similar to how Java's AOT cache works.
29+
- **Java AOT** (JEP 483) snapshots ~3,300 pre-loaded classes from a training run into a `.aot` file, eliminating class loading overhead on subsequent runs.
30+
- **JBang** compiles and caches internally but adds launcher overhead on every invocation.
31+
- **Fat JAR** (`java -jar`) loads and links all classes from scratch each time.
1632

1733
## AOT Cache Setup
1834

1935
```bash
20-
# One-time: build the cache (~21 MB, platform-specific)
36+
# One-time: build the fat JAR
37+
jbang export fatjar --force --output html-generators/generate.jar html-generators/generate.java
38+
39+
# One-time: build the AOT cache (~21 MB, platform-specific)
2140
java -XX:AOTCacheOutput=html-generators/generate.aot -jar html-generators/generate.jar
2241

23-
# Use it
42+
# Steady-state: run with AOT cache
2443
java -XX:AOTCache=html-generators/generate.aot -jar html-generators/generate.jar
2544
```
2645

27-
The AOT cache uses Java 25 CDS (JEP 483) to pre-load classes from a training run. It is platform-specific (CPU arch + JDK version).
28-
2946
## Environment
3047

3148
| | |
@@ -37,10 +54,6 @@ The AOT cache uses Java 25 CDS (JEP 483) to pre-load classes from a training run
3754
| **Python** | 3.14.3 |
3855
| **OS** | Darwin |
3956

40-
## Methodology
41-
42-
Each method was timed 6 times using `/usr/bin/time -p`. The first run is reported as "cold start" and the remaining 5 runs are averaged for "warm average". Between each run, `site/index.html` was reset via `git checkout` to ensure the generator runs fully each time.
43-
4457
## Reproduce
4558

4659
```bash

0 commit comments

Comments
 (0)