Skip to content

Commit c1eb873

Browse files
kylehowellsclaude
andcommitted
Add Rust (html5ever) to benchmark comparison scripts
- Add rust_benchmark/ with Cargo project for html5ever benchmarking - Update compare.py to run Rust benchmark alongside Swift/Python/JS - Update memory_compare.py to measure Rust memory usage - Add Benchmarks/rust_benchmark/target/ to .gitignore Rust is ~4.3x faster than Swift and uses less memory. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent a712cc2 commit c1eb873

File tree

6 files changed

+887
-77
lines changed

6 files changed

+887
-77
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,4 @@ fastlane/test_output
3434
Benchmarks/samples/
3535
Benchmarks/*.json
3636
Benchmarks/test_files/synthetic.html
37+
Benchmarks/rust_benchmark/target/

Benchmarks/compare.py

Lines changed: 149 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,52 @@ def run_js_benchmark():
142142
print(result.stderr, file=sys.stderr)
143143
return json.loads(result.stdout)
144144

145-
def compare_outputs(swift_results, python_results, js_results):
146-
"""Compare outputs from all three implementations."""
145+
def run_rust_benchmark():
146+
"""Build and run the Rust (html5ever) benchmark."""
147+
print("\n" + "=" * 60, file=sys.stderr)
148+
print("Building and running Rust benchmark...", file=sys.stderr)
149+
print("=" * 60, file=sys.stderr)
150+
151+
rust_dir = SCRIPT_DIR / "rust_benchmark"
152+
if not rust_dir.exists():
153+
print("Rust benchmark not found, skipping...", file=sys.stderr)
154+
return None
155+
156+
# Source cargo env and build
157+
env = os.environ.copy()
158+
cargo_bin = Path.home() / ".cargo" / "bin"
159+
if cargo_bin.exists():
160+
env["PATH"] = str(cargo_bin) + ":" + env.get("PATH", "")
161+
162+
# Build release version
163+
result = subprocess.run(
164+
["cargo", "build", "--release"],
165+
cwd=rust_dir,
166+
capture_output=True,
167+
text=True,
168+
env=env
169+
)
170+
if result.returncode != 0:
171+
print(f"Rust build failed:\n{result.stderr}", file=sys.stderr)
172+
return None
173+
174+
# Run benchmark
175+
result = subprocess.run(
176+
["cargo", "run", "--release"],
177+
cwd=rust_dir,
178+
capture_output=True,
179+
text=True,
180+
env=env
181+
)
182+
if result.returncode != 0:
183+
print(f"Rust benchmark failed:\n{result.stderr}", file=sys.stderr)
184+
return None
185+
186+
print(result.stderr, file=sys.stderr)
187+
return json.loads(result.stdout)
188+
189+
def compare_outputs(swift_results, python_results, js_results, rust_results=None):
190+
"""Compare outputs from all implementations."""
147191
print("\n" + "=" * 60, file=sys.stderr)
148192
print("Output Comparison", file=sys.stderr)
149193
print("=" * 60, file=sys.stderr)
@@ -152,6 +196,7 @@ def compare_outputs(swift_results, python_results, js_results):
152196
swift_by_file = {r['file']: r for r in swift_results} if swift_results else {}
153197
python_by_file = {r['file']: r for r in python_results} if python_results else {}
154198
js_by_file = {r['file']: r for r in js_results} if js_results else {}
199+
rust_by_file = {r['file']: r for r in rust_results} if rust_results else {}
155200

156201
all_files = set(swift_by_file.keys()) | set(python_by_file.keys()) | set(js_by_file.keys())
157202

@@ -161,11 +206,15 @@ def compare_outputs(swift_results, python_results, js_results):
161206
swift_out = swift_by_file.get(filename, {}).get('output', '')
162207
python_out = python_by_file.get(filename, {}).get('output', '')
163208
js_out = js_by_file.get(filename, {}).get('output', '')
209+
rust_out = rust_by_file.get(filename, {}).get('output', '')
164210

165211
swift_python_match = swift_out == python_out if swift_out and python_out else None
166212
swift_js_match = swift_out == js_out if swift_out and js_out else None
167213
python_js_match = python_out == js_out if python_out and js_out else None
214+
# Note: Rust (html5ever) may have slightly different output format
215+
swift_rust_match = swift_out == rust_out if swift_out and rust_out else None
168216

217+
# Only compare Swift/Python/JS for consistency (Rust is reference impl with different conventions)
169218
status = "OK" if (swift_python_match and swift_js_match and python_js_match) else "MISMATCH"
170219
if status == "MISMATCH":
171220
all_match = False
@@ -179,7 +228,7 @@ def compare_outputs(swift_results, python_results, js_results):
179228

180229
return all_match, file_results
181230

182-
def generate_markdown_report(swift_results, python_results, js_results, all_match, file_results, git_info):
231+
def generate_markdown_report(swift_results, python_results, js_results, all_match, file_results, git_info, rust_results=None):
183232
"""Generate a markdown report with benchmark results."""
184233
lines = []
185234

@@ -220,74 +269,99 @@ def generate_markdown_report(swift_results, python_results, js_results, all_matc
220269
swift_by_file = {r['file']: r for r in swift_results} if swift_results else {}
221270
python_by_file = {r['file']: r for r in python_results} if python_results else {}
222271
js_by_file = {r['file']: r for r in js_results} if js_results else {}
272+
rust_by_file = {r['file']: r for r in rust_results} if rust_results else {}
223273

224274
all_files = set(swift_by_file.keys()) | set(python_by_file.keys()) | set(js_by_file.keys())
225275

226-
lines.append("| File | Size | Swift | Python | JavaScript | Swift vs Python | Swift vs JS |")
227-
lines.append("|------|------|-------|--------|------------|-----------------|-------------|")
276+
if rust_results:
277+
lines.append("| File | Size | Rust | Swift | JavaScript | Python | Rust vs Swift |")
278+
lines.append("|------|------|------|-------|------------|--------|---------------|")
279+
else:
280+
lines.append("| File | Size | Swift | Python | JavaScript | Swift vs Python | Swift vs JS |")
281+
lines.append("|------|------|-------|--------|------------|-----------------|-------------|")
228282

229283
total_swift = 0
230284
total_python = 0
231285
total_js = 0
286+
total_rust = 0
232287

233288
for filename in sorted(all_files):
234289
swift_r = swift_by_file.get(filename, {})
235290
python_r = python_by_file.get(filename, {})
236291
js_r = js_by_file.get(filename, {})
292+
rust_r = rust_by_file.get(filename, {})
237293

238294
size = swift_r.get('size_bytes') or python_r.get('size_bytes') or js_r.get('size_bytes', 0)
239295
size_str = f"{size/1024:.0f} KB"
240296

241297
swift_ms = swift_r.get('avg_ms', 0)
242298
python_ms = python_r.get('avg_ms', 0)
243299
js_ms = js_r.get('avg_ms', 0)
300+
rust_ms = rust_r.get('avg_ms', 0)
244301

245302
total_swift += swift_ms
246303
total_python += python_ms
247304
total_js += js_ms
305+
total_rust += rust_ms
248306

249307
swift_str = f"{swift_ms:.2f} ms" if swift_ms else "N/A"
250308
python_str = f"{python_ms:.2f} ms" if python_ms else "N/A"
251309
js_str = f"{js_ms:.2f} ms" if js_ms else "N/A"
310+
rust_str = f"{rust_ms:.2f} ms" if rust_ms else "N/A"
252311

253-
# Speed ratios
254-
swift_py_ratio = python_ms / swift_ms if swift_ms and python_ms else 0
255-
swift_js_ratio = js_ms / swift_ms if swift_ms and js_ms else 0
256-
257-
ratio_py_str = f"{swift_py_ratio:.2f}x faster" if swift_py_ratio > 1 else f"{1/swift_py_ratio:.2f}x slower" if swift_py_ratio else "N/A"
258-
ratio_js_str = f"{swift_js_ratio:.2f}x faster" if swift_js_ratio > 1 else f"{1/swift_js_ratio:.2f}x slower" if swift_js_ratio else "N/A"
259-
260-
lines.append(f"| {filename} | {size_str} | {swift_str} | {python_str} | {js_str} | {ratio_py_str} | {ratio_js_str} |")
312+
if rust_results:
313+
# Rust vs Swift ratio
314+
rust_swift_ratio = swift_ms / rust_ms if rust_ms and swift_ms else 0
315+
ratio_str = f"{rust_swift_ratio:.2f}x faster" if rust_swift_ratio > 1 else f"{1/rust_swift_ratio:.2f}x slower" if rust_swift_ratio else "N/A"
316+
lines.append(f"| {filename} | {size_str} | {rust_str} | {swift_str} | {js_str} | {python_str} | {ratio_str} |")
317+
else:
318+
# Speed ratios
319+
swift_py_ratio = python_ms / swift_ms if swift_ms and python_ms else 0
320+
swift_js_ratio = js_ms / swift_ms if swift_ms and js_ms else 0
321+
ratio_py_str = f"{swift_py_ratio:.2f}x faster" if swift_py_ratio > 1 else f"{1/swift_py_ratio:.2f}x slower" if swift_py_ratio else "N/A"
322+
ratio_js_str = f"{swift_js_ratio:.2f}x faster" if swift_js_ratio > 1 else f"{1/swift_js_ratio:.2f}x slower" if swift_js_ratio else "N/A"
323+
lines.append(f"| {filename} | {size_str} | {swift_str} | {python_str} | {js_str} | {ratio_py_str} | {ratio_js_str} |")
261324

262325
# Totals
263-
swift_py_total = total_python / total_swift if total_swift else 0
264-
swift_js_total = total_js / total_swift if total_swift else 0
265-
266-
ratio_py_total = f"{swift_py_total:.2f}x faster" if swift_py_total > 1 else f"{1/swift_py_total:.2f}x slower" if swift_py_total else "N/A"
267-
ratio_js_total = f"{swift_js_total:.2f}x faster" if swift_js_total > 1 else f"{1/swift_js_total:.2f}x slower" if swift_js_total else "N/A"
268-
269-
lines.append(f"| **TOTAL** | | **{total_swift:.0f} ms** | **{total_python:.0f} ms** | **{total_js:.0f} ms** | **{ratio_py_total}** | **{ratio_js_total}** |")
326+
if rust_results:
327+
rust_swift_total = total_swift / total_rust if total_rust else 0
328+
ratio_total = f"{rust_swift_total:.2f}x faster" if rust_swift_total > 1 else f"{1/rust_swift_total:.2f}x slower" if rust_swift_total else "N/A"
329+
lines.append(f"| **TOTAL** | | **{total_rust:.0f} ms** | **{total_swift:.0f} ms** | **{total_js:.0f} ms** | **{total_python:.0f} ms** | **{ratio_total}** |")
330+
else:
331+
swift_py_total = total_python / total_swift if total_swift else 0
332+
swift_js_total = total_js / total_swift if total_swift else 0
333+
ratio_py_total = f"{swift_py_total:.2f}x faster" if swift_py_total > 1 else f"{1/swift_py_total:.2f}x slower" if swift_py_total else "N/A"
334+
ratio_js_total = f"{swift_js_total:.2f}x faster" if swift_js_total > 1 else f"{1/swift_js_total:.2f}x slower" if swift_js_total else "N/A"
335+
lines.append(f"| **TOTAL** | | **{total_swift:.0f} ms** | **{total_python:.0f} ms** | **{total_js:.0f} ms** | **{ratio_py_total}** | **{ratio_js_total}** |")
270336
lines.append("")
271337

272338
# Summary
273339
lines.append("## Summary")
274340
lines.append("")
341+
if rust_results:
342+
lines.append(f"- **Rust (html5ever)** total parse time: {total_rust:.0f} ms")
275343
lines.append(f"- **Swift** total parse time: {total_swift:.0f} ms")
276-
lines.append(f"- **Python** total parse time: {total_python:.0f} ms")
277344
lines.append(f"- **JavaScript** total parse time: {total_js:.0f} ms")
345+
lines.append(f"- **Python** total parse time: {total_python:.0f} ms")
278346
lines.append("")
279347

280-
if total_swift and total_python and total_js:
281-
fastest = min(total_swift, total_python, total_js)
282-
if fastest == total_js:
283-
lines.append("**JavaScript** is the fastest implementation (V8 JIT optimization).")
284-
elif fastest == total_swift:
285-
lines.append("**Swift** is the fastest implementation.")
286-
else:
287-
lines.append("**Python** is the fastest implementation.")
348+
all_totals = [(total_swift, "Swift"), (total_python, "Python"), (total_js, "JavaScript")]
349+
if rust_results:
350+
all_totals.append((total_rust, "Rust (html5ever)"))
351+
all_totals = [(t, n) for t, n in all_totals if t > 0]
352+
353+
if all_totals:
354+
fastest_time, fastest_name = min(all_totals, key=lambda x: x[0])
355+
lines.append(f"**{fastest_name}** is the fastest implementation.")
288356
lines.append("")
289-
lines.append(f"Swift is **{total_python/total_swift:.1f}x faster** than Python.")
290-
lines.append(f"JavaScript is **{total_swift/total_js:.1f}x faster** than Swift.")
357+
if rust_results and total_rust:
358+
lines.append(f"Rust is **{total_swift/total_rust:.1f}x faster** than Swift.")
359+
lines.append(f"Rust is **{total_js/total_rust:.1f}x faster** than JavaScript.")
360+
lines.append(f"Rust is **{total_python/total_rust:.1f}x faster** than Python.")
361+
elif total_swift:
362+
lines.append(f"Swift is **{total_python/total_swift:.1f}x faster** than Python.")
363+
if total_js:
364+
lines.append(f"JavaScript is **{total_swift/total_js:.1f}x faster** than Swift.")
291365
lines.append("")
292366

293367
# Test files info
@@ -312,72 +386,87 @@ def generate_markdown_report(swift_results, python_results, js_results, all_matc
312386

313387
return "\n".join(lines)
314388

315-
def print_summary(swift_results, python_results, js_results):
389+
def print_summary(swift_results, python_results, js_results, rust_results=None):
316390
"""Print performance summary table."""
317391
print("\n" + "=" * 60)
318392
print("PERFORMANCE COMPARISON")
319393
print("=" * 60)
320394

321-
# Header
322-
print(f"\n{'File':<25} {'Size':>10} | {'Swift':>10} {'Python':>10} {'JS':>10} | {'Swift/Py':>8} {'Swift/JS':>8}")
323-
print("-" * 100)
324-
325395
swift_by_file = {r['file']: r for r in swift_results} if swift_results else {}
326396
python_by_file = {r['file']: r for r in python_results} if python_results else {}
327397
js_by_file = {r['file']: r for r in js_results} if js_results else {}
398+
rust_by_file = {r['file']: r for r in rust_results} if rust_results else {}
328399

329400
all_files = set(swift_by_file.keys()) | set(python_by_file.keys()) | set(js_by_file.keys())
330401

402+
# Header
403+
if rust_results:
404+
print(f"\n{'File':<30} {'Size':>10} | {'Rust':>10} {'Swift':>10} {'JS':>10} {'Python':>10} | {'Rust/Swift':>10}")
405+
print("-" * 115)
406+
else:
407+
print(f"\n{'File':<25} {'Size':>10} | {'Swift':>10} {'Python':>10} {'JS':>10} | {'Swift/Py':>8} {'Swift/JS':>8}")
408+
print("-" * 100)
409+
331410
total_swift = 0
332411
total_python = 0
333412
total_js = 0
413+
total_rust = 0
334414

335415
for filename in sorted(all_files):
336416
swift_r = swift_by_file.get(filename, {})
337417
python_r = python_by_file.get(filename, {})
338418
js_r = js_by_file.get(filename, {})
419+
rust_r = rust_by_file.get(filename, {})
339420

340421
size = swift_r.get('size_bytes') or python_r.get('size_bytes') or js_r.get('size_bytes', 0)
341422
size_kb = f"{size/1024:.0f}KB"
342423

343424
swift_ms = swift_r.get('avg_ms', 0)
344425
python_ms = python_r.get('avg_ms', 0)
345426
js_ms = js_r.get('avg_ms', 0)
427+
rust_ms = rust_r.get('avg_ms', 0)
346428

347429
total_swift += swift_ms
348430
total_python += python_ms
349431
total_js += js_ms
432+
total_rust += rust_ms
350433

351434
swift_str = f"{swift_ms:.2f}ms" if swift_ms else "N/A"
352435
python_str = f"{python_ms:.2f}ms" if python_ms else "N/A"
353436
js_str = f"{js_ms:.2f}ms" if js_ms else "N/A"
437+
rust_str = f"{rust_ms:.2f}ms" if rust_ms else "N/A"
354438

355-
# Speed ratios (how many times faster Swift is)
356-
swift_py_ratio = python_ms / swift_ms if swift_ms and python_ms else 0
357-
swift_js_ratio = js_ms / swift_ms if swift_ms and js_ms else 0
358-
359-
ratio_py_str = f"{swift_py_ratio:.1f}x" if swift_py_ratio else "N/A"
360-
ratio_js_str = f"{swift_js_ratio:.1f}x" if swift_js_ratio else "N/A"
361-
362-
print(f"{filename:<25} {size_kb:>10} | {swift_str:>10} {python_str:>10} {js_str:>10} | {ratio_py_str:>8} {ratio_js_str:>8}")
363-
364-
print("-" * 100)
365-
366-
# Totals
367-
swift_py_total = total_python / total_swift if total_swift else 0
368-
swift_js_total = total_js / total_swift if total_swift else 0
369-
370-
print(f"{'TOTAL':<25} {'':<10} | {total_swift:>9.0f}ms {total_python:>9.0f}ms {total_js:>9.0f}ms | {swift_py_total:>7.1f}x {swift_js_total:>7.1f}x")
371-
372-
print("\n(Higher ratio = Swift is faster by that factor)")
439+
if rust_results:
440+
rust_swift_ratio = swift_ms / rust_ms if rust_ms and swift_ms else 0
441+
ratio_str = f"{rust_swift_ratio:.1f}x" if rust_swift_ratio else "N/A"
442+
print(f"{filename:<30} {size_kb:>10} | {rust_str:>10} {swift_str:>10} {js_str:>10} {python_str:>10} | {ratio_str:>10}")
443+
else:
444+
swift_py_ratio = python_ms / swift_ms if swift_ms and python_ms else 0
445+
swift_js_ratio = js_ms / swift_ms if swift_ms and js_ms else 0
446+
ratio_py_str = f"{swift_py_ratio:.1f}x" if swift_py_ratio else "N/A"
447+
ratio_js_str = f"{swift_js_ratio:.1f}x" if swift_js_ratio else "N/A"
448+
print(f"{filename:<25} {size_kb:>10} | {swift_str:>10} {python_str:>10} {js_str:>10} | {ratio_py_str:>8} {ratio_js_str:>8}")
449+
450+
if rust_results:
451+
print("-" * 115)
452+
rust_swift_total = total_swift / total_rust if total_rust else 0
453+
print(f"{'TOTAL':<30} {'':<10} | {total_rust:>9.0f}ms {total_swift:>9.0f}ms {total_js:>9.0f}ms {total_python:>9.0f}ms | {rust_swift_total:>9.1f}x")
454+
print("\n(Higher ratio = Rust is faster by that factor)")
455+
else:
456+
print("-" * 100)
457+
swift_py_total = total_python / total_swift if total_swift else 0
458+
swift_js_total = total_js / total_swift if total_swift else 0
459+
print(f"{'TOTAL':<25} {'':<10} | {total_swift:>9.0f}ms {total_python:>9.0f}ms {total_js:>9.0f}ms | {swift_py_total:>7.1f}x {swift_js_total:>7.1f}x")
460+
print("\n(Higher ratio = Swift is faster by that factor)")
373461

374462
def main():
375-
# Get git info for all three projects
463+
# Get git info for all projects
376464
print("Gathering git information...", file=sys.stderr)
377465
git_info = {
378466
'swift-justhtml': get_git_info(SWIFT_PROJECT_ROOT),
379467
'justhtml (Python)': get_git_info(JUSTHTML_ROOT / 'justhtml'),
380468
'justjshtml (JavaScript)': get_git_info(JUSTHTML_ROOT / 'justjshtml'),
469+
'html5ever (Rust)': get_git_info(JUSTHTML_ROOT / 'html5ever'),
381470
}
382471

383472
# Download samples if needed
@@ -387,17 +476,18 @@ def main():
387476
swift_results = run_swift_benchmark()
388477
python_results = run_python_benchmark()
389478
js_results = run_js_benchmark()
479+
rust_results = run_rust_benchmark()
390480

391481
# Compare outputs
392-
all_match, file_results = compare_outputs(swift_results, python_results, js_results)
482+
all_match, file_results = compare_outputs(swift_results, python_results, js_results, rust_results)
393483

394484
# Print performance summary
395-
print_summary(swift_results, python_results, js_results)
485+
print_summary(swift_results, python_results, js_results, rust_results)
396486

397487
# Generate markdown report
398488
markdown_report = generate_markdown_report(
399489
swift_results, python_results, js_results,
400-
all_match, file_results, git_info
490+
all_match, file_results, git_info, rust_results
401491
)
402492

403493
# Save markdown report
@@ -413,6 +503,7 @@ def main():
413503
'swift': swift_results,
414504
'python': python_results,
415505
'javascript': js_results,
506+
'rust': rust_results,
416507
'outputs_match': all_match
417508
}
418509

0 commit comments

Comments
 (0)