Skip to content

Commit d2d2836

Browse files
authored
Merge pull request #87 from githubnext/copilot/add-autoloop-performance-test
Add autoloop perf-comparison program for tsb vs pandas benchmarking
2 parents 09f29f3 + 6ad6a7a commit d2d2836

9 files changed

Lines changed: 664 additions & 1 deletion

File tree

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
---
2+
schedule: every 6h
3+
---
4+
5+
# Performance Comparison: tsb (TypeScript) vs pandas (Python)
6+
7+
## Goal
8+
9+
Systematically benchmark every tsb function against its pandas equivalent, one function per iteration. Each iteration picks a function that has not yet been benchmarked, writes a matching performance test for both tsb (TypeScript/Bun) and pandas (Python), runs both, and records the timing results. The benchmark results are displayed on the playground pages doc site.
10+
11+
This is an open-ended program — it runs continuously, always adding the next benchmark comparison.
12+
13+
### How each iteration works
14+
15+
1. **Read existing benchmarks** — check `benchmarks/tsb/` and `benchmarks/pandas/` to see which functions are already benchmarked.
16+
2. **Pick ONE function** from `src/` that has no benchmark yet. Prioritize core operations (Series, DataFrame, GroupBy, etc.).
17+
3. **Write a TypeScript benchmark** in `benchmarks/tsb/bench_{function}.ts` that:
18+
- Creates a realistic dataset (e.g. 100,000 rows)
19+
- Runs the operation in a tight loop (warm-up + measured iterations)
20+
- Outputs JSON: `{"function": "...", "mean_ms": ..., "iterations": ..., "total_ms": ...}`
21+
4. **Write a matching Python benchmark** in `benchmarks/pandas/bench_{function}.py` that:
22+
- Creates the same dataset as the TypeScript version
23+
- Runs the same operation with the same loop structure
24+
- Outputs the same JSON format
25+
5. **Run both benchmarks** via `benchmarks/run_benchmarks.sh` and capture results.
26+
6. **Update `benchmarks/results.json`** with the new timing data.
27+
7. **Update `playground/benchmarks.html`** to display the new function's comparison metrics.
28+
29+
### Key constraints
30+
31+
- **Matching datasets** — both benchmarks must use identical data (same size, same values where possible).
32+
- **Fair comparison** — same number of warm-up and measured iterations for both.
33+
- **JSON output** — every benchmark script must output a single JSON line to stdout.
34+
- **No modifications to `src/`** — benchmark code is separate from library code.
35+
- **Python environment** — install pandas via pip if not present.
36+
37+
## Target
38+
39+
Only modify these files:
40+
- `benchmarks/**` — benchmark scripts and results
41+
- `playground/benchmarks.html` — performance comparison playground page
42+
- `playground/index.html` — add/update link to benchmarks page
43+
44+
Do NOT modify:
45+
- `src/**` — library source code
46+
- `tests/**` — test files
47+
- `README.md` — read-only
48+
- `.autoloop/programs/**` — program definitions (except this file's code/ dir)
49+
- `.github/workflows/autoloop*` — autoloop workflow files
50+
51+
## Evaluation
52+
53+
```bash
54+
# Set up Python environment if needed
55+
if ! command -v python3 &>/dev/null; then
56+
echo "Python3 not found, skipping"
57+
fi
58+
pip3 install pandas --quiet 2>/dev/null || true
59+
60+
# Count the number of benchmark pairs (functions with both TS and Python benchmarks)
61+
ts_benchmarks=$(ls benchmarks/tsb/bench_*.ts 2>/dev/null | wc -l | tr -d ' ')
62+
py_benchmarks=$(ls benchmarks/pandas/bench_*.py 2>/dev/null | wc -l | tr -d ' ')
63+
64+
# The metric is the minimum of the two (both must exist for a complete benchmark)
65+
if [ "$ts_benchmarks" -lt "$py_benchmarks" ]; then
66+
count=$ts_benchmarks
67+
else
68+
count=$py_benchmarks
69+
fi
70+
71+
echo "{\"benchmarked_functions\": ${count:-0}}"
72+
```
73+
74+
The metric is `benchmarked_functions`. **Higher is better.**

.github/workflows/pages.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ jobs:
3636
- name: Bundle TypeScript compiler for offline playground
3737
run: cp node_modules/typescript/lib/typescript.js ./playground/dist/typescript.js
3838

39+
- name: Copy benchmark results to playground
40+
run: |
41+
mkdir -p ./playground/benchmarks
42+
if [ -f benchmarks/results.json ]; then
43+
cp benchmarks/results.json ./playground/benchmarks/results.json
44+
fi
45+
3946
- name: Setup Python
4047
uses: actions/setup-python@v5
4148
with:
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
"""
2+
Benchmark: Series creation
3+
4+
Creates a Series from a large numeric array and measures the time.
5+
Outputs JSON: {"function": "series_creation", "mean_ms": ..., "iterations": ..., "total_ms": ...}
6+
"""
7+
8+
import json
9+
import time
10+
11+
import pandas as pd
12+
13+
SIZE = 100_000
14+
WARMUP = 5
15+
ITERATIONS = 50
16+
17+
18+
def generate_data(n: int) -> "list[float]":
19+
"""Generate a deterministic numeric array of the given size."""
20+
return [i * 1.1 + 0.5 for i in range(n)]
21+
22+
23+
data = generate_data(SIZE)
24+
25+
# Warm-up
26+
for _ in range(WARMUP):
27+
pd.Series(list(data))
28+
29+
# Measured runs
30+
times: "list[float]" = []
31+
for _ in range(ITERATIONS):
32+
start = time.perf_counter()
33+
pd.Series(list(data))
34+
end = time.perf_counter()
35+
times.append((end - start) * 1000) # convert to ms
36+
37+
total_ms = sum(times)
38+
mean_ms = total_ms / ITERATIONS
39+
40+
result = {
41+
"function": "series_creation",
42+
"mean_ms": round(mean_ms, 3),
43+
"iterations": ITERATIONS,
44+
"total_ms": round(total_ms, 3),
45+
}
46+
47+
print(json.dumps(result))

benchmarks/results.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{ "benchmarks": [], "timestamp": null }

benchmarks/run_benchmarks.sh

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
#!/usr/bin/env bash
2+
#
3+
# Run all tsb (TypeScript) and pandas (Python) benchmarks and collect results.
4+
#
5+
# Usage: ./benchmarks/run_benchmarks.sh
6+
#
7+
# Outputs: benchmarks/results.json with all benchmark results
8+
#
9+
set -euo pipefail
10+
11+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
12+
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
13+
14+
# Ensure Python and pandas are available
15+
if ! command -v python3 &>/dev/null; then
16+
echo "ERROR: python3 is required but not found" >&2
17+
exit 1
18+
fi
19+
20+
python3 -c "import pandas" 2>/dev/null || {
21+
echo "Installing pandas..."
22+
pip3 install pandas --quiet
23+
}
24+
25+
# Ensure Bun is available
26+
if ! command -v bun &>/dev/null; then
27+
echo "ERROR: bun is required but not found" >&2
28+
exit 1
29+
fi
30+
31+
# Collect results
32+
results='{"benchmarks": [], "timestamp": "'$(date -u +"%Y-%m-%dT%H:%M:%SZ")'"}'
33+
34+
echo "=== Running Performance Benchmarks ==="
35+
echo ""
36+
37+
# Find all TypeScript benchmark files
38+
for ts_bench in "$SCRIPT_DIR"/tsb/bench_*.ts; do
39+
[ -f "$ts_bench" ] || continue
40+
bench_name=$(basename "$ts_bench" .ts | sed 's/^bench_//')
41+
42+
# Check for matching Python benchmark
43+
py_bench="$SCRIPT_DIR/pandas/bench_${bench_name}.py"
44+
if [ ! -f "$py_bench" ]; then
45+
echo "SKIP: $bench_name (no matching Python benchmark)"
46+
continue
47+
fi
48+
49+
echo "--- Benchmarking: $bench_name ---"
50+
51+
# Run TypeScript benchmark
52+
echo " Running tsb (TypeScript)..."
53+
ts_result=$(cd "$REPO_ROOT" && bun run "$ts_bench" 2>/dev/null) || {
54+
echo " ERROR: TypeScript benchmark failed"
55+
continue
56+
}
57+
echo " tsb result: $ts_result"
58+
59+
# Run Python benchmark
60+
echo " Running pandas (Python)..."
61+
py_result=$(cd "$REPO_ROOT" && python3 "$py_bench" 2>/dev/null) || {
62+
echo " ERROR: Python benchmark failed"
63+
continue
64+
}
65+
echo " pandas result: $py_result"
66+
67+
# Extract mean_ms from both
68+
ts_mean=$(echo "$ts_result" | python3 -c "import sys, json; d=json.load(sys.stdin); print(d['mean_ms'])" 2>/dev/null) || {
69+
echo " ERROR: could not parse tsb benchmark result"
70+
continue
71+
}
72+
py_mean=$(echo "$py_result" | python3 -c "import sys, json; d=json.load(sys.stdin); print(d['mean_ms'])" 2>/dev/null) || {
73+
echo " ERROR: could not parse pandas benchmark result"
74+
continue
75+
}
76+
77+
# Calculate ratio (tsb / pandas) — < 1.0 means tsb is faster
78+
ratio=$(python3 -c "
79+
ts, py = $ts_mean, $py_mean
80+
if py <= 0:
81+
print('null')
82+
else:
83+
print(round(ts / py, 3))
84+
")
85+
if [ "$ratio" = "null" ]; then
86+
echo " ERROR: pandas mean_ms is zero, cannot compute ratio"
87+
continue
88+
fi
89+
90+
echo " Ratio (tsb/pandas): ${ratio}x"
91+
echo ""
92+
93+
# Add to results JSON
94+
results=$(echo "$results" | python3 -c "
95+
import sys, json
96+
data = json.load(sys.stdin)
97+
data['benchmarks'].append({
98+
'function': '$bench_name',
99+
'tsb': $ts_result,
100+
'pandas': $py_result,
101+
'ratio': $ratio
102+
})
103+
print(json.dumps(data, indent=2))
104+
")
105+
done
106+
107+
# Write results
108+
echo "$results" > "$SCRIPT_DIR/results.json"
109+
echo "=== Results written to benchmarks/results.json ==="
110+
echo ""
111+
112+
# Summary
113+
echo "=== Summary ==="
114+
echo "$results" | python3 -c "
115+
import sys, json
116+
data = json.load(sys.stdin)
117+
benchmarks = data.get('benchmarks', [])
118+
if not benchmarks:
119+
print('No benchmarks found.')
120+
else:
121+
print(f'Functions benchmarked: {len(benchmarks)}')
122+
for b in benchmarks:
123+
fn = b['function']
124+
ts = b['tsb']['mean_ms']
125+
py = b['pandas']['mean_ms']
126+
ratio = b['ratio']
127+
faster = 'tsb' if ratio < 1 else 'pandas'
128+
print(f' {fn}: tsb={ts}ms, pandas={py}ms, ratio={ratio}x ({faster} is faster)')
129+
"
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/**
2+
* Benchmark: Series creation
3+
*
4+
* Creates a Series from a large numeric array and measures the time.
5+
* Outputs JSON: {"function": "series_creation", "mean_ms": ..., "iterations": ..., "total_ms": ...}
6+
*/
7+
8+
import { Series } from "../../src/index.ts";
9+
10+
const SIZE = 100_000;
11+
const WARMUP = 5;
12+
const ITERATIONS = 50;
13+
14+
/** Generate a deterministic numeric array of the given size. */
15+
function generateData(n: number): readonly number[] {
16+
const arr: number[] = [];
17+
for (let i = 0; i < n; i++) {
18+
arr.push(i * 1.1 + 0.5);
19+
}
20+
return arr;
21+
}
22+
23+
const data = generateData(SIZE);
24+
25+
// Warm-up
26+
for (let i = 0; i < WARMUP; i++) {
27+
new Series({ data: [...data] });
28+
}
29+
30+
// Measured runs
31+
const times: number[] = [];
32+
for (let i = 0; i < ITERATIONS; i++) {
33+
const start = performance.now();
34+
new Series({ data: [...data] });
35+
const end = performance.now();
36+
times.push(end - start);
37+
}
38+
39+
const totalMs = times.reduce((a, b) => a + b, 0);
40+
const meanMs = totalMs / ITERATIONS;
41+
42+
const result = {
43+
function: "series_creation",
44+
mean_ms: Math.round(meanMs * 1000) / 1000,
45+
iterations: ITERATIONS,
46+
total_ms: Math.round(totalMs * 1000) / 1000,
47+
};
48+
49+
console.log(JSON.stringify(result));

0 commit comments

Comments
 (0)