@@ -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
374462def 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