Precise DOM morphing
morphing typescript dom
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at cb64636eae73e4012cc0d75bc052d43c45192f19 1033 lines 28 kB view raw
1<!doctype html> 2<html lang="en"> 3 <head> 4 <meta charset="UTF-8" /> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 <title>Morphlex Benchmark Suite</title> 7 <style> 8 :root { 9 --primary: #6366f1; 10 --success: #10b981; 11 --warning: #f59e0b; 12 --danger: #ef4444; 13 --dark: #1f2937; 14 --light: #f3f4f6; 15 --white: #ffffff; 16 } 17 18 * { 19 margin: 0; 20 padding: 0; 21 box-sizing: border-box; 22 } 23 24 body { 25 font-family: 26 system-ui, 27 -apple-system, 28 BlinkMacSystemFont, 29 "Segoe UI", 30 Roboto, 31 Oxygen, 32 Ubuntu, 33 sans-serif; 34 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 35 min-height: 100vh; 36 padding: 2rem; 37 } 38 39 .container { 40 max-width: 1200px; 41 margin: 0 auto; 42 } 43 44 header { 45 text-align: center; 46 color: var(--white); 47 margin-bottom: 2rem; 48 } 49 50 h1 { 51 font-size: 3rem; 52 margin-bottom: 0.5rem; 53 text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2); 54 } 55 56 .subtitle { 57 font-size: 1.2rem; 58 opacity: 0.95; 59 } 60 61 .controls { 62 background: var(--white); 63 border-radius: 12px; 64 padding: 1.5rem; 65 margin-bottom: 2rem; 66 box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); 67 } 68 69 .control-group { 70 display: flex; 71 gap: 1rem; 72 align-items: center; 73 flex-wrap: wrap; 74 } 75 76 label { 77 font-weight: 600; 78 color: var(--dark); 79 } 80 81 input[type="number"] { 82 padding: 0.5rem; 83 border: 2px solid var(--light); 84 border-radius: 6px; 85 font-size: 1rem; 86 width: 120px; 87 } 88 89 button { 90 padding: 0.75rem 2rem; 91 background: var(--primary); 92 color: var(--white); 93 border: none; 94 border-radius: 6px; 95 font-size: 1rem; 96 font-weight: 600; 97 cursor: pointer; 98 transition: 99 transform 0.2s, 100 box-shadow 0.2s; 101 } 102 103 button:hover:not(:disabled) { 104 transform: translateY(-2px); 105 box-shadow: 0 5px 15px rgba(99, 102, 241, 0.4); 106 } 107 108 button:disabled { 109 background: var(--light); 110 color: #9ca3af; 111 cursor: not-allowed; 112 } 113 114 button.stop { 115 background: var(--danger); 116 } 117 118 .status { 119 margin-left: auto; 120 padding: 0.5rem 1rem; 121 border-radius: 20px; 122 background: var(--light); 123 font-weight: 600; 124 color: var(--dark); 125 } 126 127 .status.running { 128 background: var(--warning); 129 color: var(--white); 130 animation: pulse 1s infinite; 131 } 132 133 .status.complete { 134 background: var(--success); 135 color: var(--white); 136 } 137 138 @keyframes pulse { 139 0%, 140 100% { 141 opacity: 1; 142 } 143 50% { 144 opacity: 0.7; 145 } 146 } 147 148 .results { 149 background: var(--white); 150 border-radius: 12px; 151 padding: 1.5rem; 152 box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); 153 } 154 155 .test-case { 156 margin-bottom: 2rem; 157 padding: 1.5rem; 158 background: var(--light); 159 border-radius: 8px; 160 } 161 162 .test-case h3 { 163 color: var(--primary); 164 margin-bottom: 0.5rem; 165 } 166 167 .test-description { 168 color: #6b7280; 169 margin-bottom: 1rem; 170 } 171 172 .library-result { 173 display: flex; 174 justify-content: space-between; 175 align-items: center; 176 padding: 0.75rem; 177 margin: 0.5rem 0; 178 background: var(--white); 179 border-radius: 6px; 180 transition: transform 0.2s; 181 } 182 183 .library-result:hover { 184 transform: translateX(5px); 185 box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); 186 } 187 188 .library-name { 189 font-weight: 600; 190 min-width: 150px; 191 } 192 193 .metrics { 194 display: flex; 195 gap: 2rem; 196 flex: 1; 197 justify-content: center; 198 } 199 200 .metric { 201 text-align: center; 202 } 203 204 .metric-value { 205 font-size: 1.2rem; 206 font-weight: 700; 207 color: var(--primary); 208 } 209 210 .metric-label { 211 font-size: 0.8rem; 212 color: #9ca3af; 213 text-transform: uppercase; 214 letter-spacing: 0.5px; 215 } 216 217 .rank { 218 min-width: 50px; 219 text-align: center; 220 font-weight: 700; 221 } 222 223 .rank-1 { 224 color: #fbbf24; 225 font-size: 1.5rem; 226 } 227 228 .rank-2 { 229 color: #9ca3af; 230 font-size: 1.3rem; 231 } 232 233 .rank-3 { 234 color: #cd7f32; 235 font-size: 1.2rem; 236 } 237 238 .summary { 239 margin-top: 2rem; 240 padding: 2rem; 241 background: linear-gradient(135deg, var(--primary), #8b5cf6); 242 border-radius: 12px; 243 color: var(--white); 244 } 245 246 .summary h2 { 247 margin-bottom: 1.5rem; 248 text-align: center; 249 } 250 251 .summary-grid { 252 display: grid; 253 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 254 gap: 1.5rem; 255 } 256 257 .summary-card { 258 background: rgba(255, 255, 255, 0.15); 259 backdrop-filter: blur(10px); 260 padding: 1.5rem; 261 border-radius: 8px; 262 text-align: center; 263 } 264 265 .summary-library { 266 font-size: 1.2rem; 267 font-weight: 700; 268 margin-bottom: 0.5rem; 269 } 270 271 .summary-score { 272 font-size: 2rem; 273 font-weight: 900; 274 } 275 276 .progress-bar { 277 width: 100%; 278 height: 6px; 279 background: var(--light); 280 border-radius: 3px; 281 overflow: hidden; 282 margin-top: 1rem; 283 } 284 285 .progress-fill { 286 height: 100%; 287 background: var(--success); 288 border-radius: 3px; 289 transition: width 0.3s ease; 290 } 291 292 #sandbox { 293 display: none; 294 } 295 296 .error { 297 background: #fee; 298 border: 2px solid var(--danger); 299 color: var(--danger); 300 padding: 1rem; 301 border-radius: 8px; 302 margin: 1rem 0; 303 } 304 305 .chart-container { 306 margin: 2rem 0; 307 padding: 1.5rem; 308 background: var(--white); 309 border-radius: 8px; 310 } 311 312 .bar-chart { 313 display: flex; 314 flex-direction: column; 315 gap: 0.5rem; 316 } 317 318 .bar-row { 319 display: flex; 320 align-items: center; 321 gap: 1rem; 322 } 323 324 .bar-label { 325 min-width: 120px; 326 font-weight: 600; 327 } 328 329 .bar { 330 height: 30px; 331 background: linear-gradient(90deg, var(--primary), #8b5cf6); 332 border-radius: 4px; 333 display: flex; 334 align-items: center; 335 padding: 0 0.5rem; 336 color: var(--white); 337 font-weight: 600; 338 font-size: 0.9rem; 339 } 340 </style> 341 </head> 342 <body> 343 <div class="container"> 344 <header> 345 <h1>⚡ Morphlex Benchmark Suite</h1> 346 <p class="subtitle">Performance comparison of DOM morphing libraries</p> 347 </header> 348 349 <div class="controls"> 350 <div class="control-group"> 351 <label for="iterations">Iterations:</label> 352 <input type="number" id="iterations" value="5000" min="1000" max="50000" step="1000" /> 353 354 <label for="warmup">Warmup:</label> 355 <input type="number" id="warmup" value="500" min="100" max="5000" step="100" /> 356 357 <button id="runBtn">🚀 Run Benchmark</button> 358 <button id="stopBtn" class="stop" style="display: none">⏹ Stop</button> 359 360 <div class="status" id="status">Ready</div> 361 </div> 362 <div class="progress-bar"> 363 <div class="progress-fill" id="progress" style="width: 0%"></div> 364 </div> 365 </div> 366 367 <div class="results" id="results"> 368 <p style="text-align: center; color: #9ca3af; padding: 2rem">Click "Run Benchmark" to start testing...</p> 369 </div> 370 </div> 371 372 <div id="sandbox"></div> 373 374 <script type="module"> 375 // Using local morphlex build 376 // To run this benchmark: 377 // 1. Run 'bun run build' to build morphlex 378 // 2. Start a local server: 'bun run --bun vite' or 'python -m http.server' in the morphlex root 379 // 3. Open http://localhost:5173/benchmark/ (or appropriate port) 380 import { morph as morphlex } from "../dist/morphlex.min.js" 381 import { morph as dataMorph } from "../dist/data-morph.min.js" 382 import { Idiomorph } from "https://unpkg.com/idiomorph@0.7.4/dist/idiomorph.esm.js" 383 import morphdom from "https://unpkg.com/morphdom@2.7.7/dist/morphdom-esm.js" 384 // Try loading nanomorph from jsdelivr with ESM 385 import nanomorph from "https://cdn.jsdelivr.net/npm/nanomorph@5.4.3/+esm" 386 // Load Alpine.js and Alpine morph 387 import Alpine from "https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/module.esm.js" 388 import Morph from "https://cdn.jsdelivr.net/npm/@alpinejs/morph@3.x.x/dist/module.esm.js" 389 390 // Initialize Alpine with morph plugin 391 Alpine.plugin(Morph) 392 window.Alpine = Alpine 393 Alpine.start() 394 395 const sandbox = document.getElementById("sandbox") 396 let isRunning = false 397 let stopRequested = false 398 399 // DOM Operation Counter using MutationObserver 400 class DOMOperationCounter { 401 constructor() { 402 this.reset() 403 } 404 405 reset() { 406 this.counts = { 407 childList: 0, 408 attributes: 0, 409 characterData: 0, 410 addedNodes: 0, 411 removedNodes: 0, 412 total: 0, 413 } 414 this.attributeNames = new Map() // Map of attribute name -> count 415 } 416 417 start(targetNode) { 418 this.reset() 419 this.observer = new MutationObserver((mutations) => { 420 this.processMutations(mutations) 421 }) 422 423 this.observer.observe(targetNode, { 424 childList: true, 425 attributes: true, 426 characterData: true, 427 subtree: true, 428 attributeOldValue: false, 429 characterDataOldValue: false, 430 }) 431 } 432 433 processMutations(mutations) { 434 for (const mutation of mutations) { 435 if (mutation.type === "childList") { 436 this.counts.childList++ 437 this.counts.addedNodes += mutation.addedNodes.length 438 this.counts.removedNodes += mutation.removedNodes.length 439 } else if (mutation.type === "attributes") { 440 this.counts.attributes++ 441 // Track the attribute name 442 if (mutation.attributeName) { 443 const count = this.attributeNames.get(mutation.attributeName) || 0 444 this.attributeNames.set(mutation.attributeName, count + 1) 445 } 446 } else if (mutation.type === "characterData") { 447 this.counts.characterData++ 448 } 449 this.counts.total++ 450 } 451 } 452 453 stop() { 454 if (this.observer) { 455 // Process any pending mutations that haven't been delivered yet 456 const pendingMutations = this.observer.takeRecords() 457 this.processMutations(pendingMutations) 458 this.observer.disconnect() 459 this.observer = null 460 } 461 return { 462 ...this.counts, 463 attributeNames: Array.from(this.attributeNames.entries()).sort((a, b) => b[1] - a[1]), 464 } 465 } 466 467 getCounts() { 468 return { ...this.counts } 469 } 470 } 471 472 class BrowserBenchmark { 473 constructor(iterations = 1000, warmupIterations = 100) { 474 this.iterations = iterations 475 this.warmupIterations = warmupIterations 476 this.results = [] 477 } 478 479 async runSingleBenchmark(library, testCase, morphFn) { 480 // Warmup 481 for (let i = 0; i < this.warmupIterations; i++) { 482 const { from, to } = testCase.setup() 483 sandbox.appendChild(from) 484 morphFn(from, to) 485 sandbox.innerHTML = "" 486 } 487 488 // Allow browser to settle 489 await new Promise((resolve) => setTimeout(resolve, 10)) 490 491 // First, do a quick test to see how fast the operation is 492 const testRun = testCase.setup() 493 sandbox.appendChild(testRun.from) 494 const testStart = performance.now() 495 morphFn(testRun.from, testRun.to) 496 const testEnd = performance.now() 497 sandbox.innerHTML = "" 498 const singleOpTime = testEnd - testStart 499 500 // If operation is very fast (< 0.1ms), batch multiple operations per measurement 501 const batchSize = singleOpTime < 0.1 ? Math.min(100, Math.ceil(0.1 / Math.max(singleOpTime, 0.001))) : 1 502 503 // Actual benchmark - use higher precision timing with batching 504 const times = [] 505 const actualIterations = Math.ceil(this.iterations / batchSize) 506 507 // Measure DOM operations for a single operation (not batched) 508 const domCounter = new DOMOperationCounter() 509 const domTestSetup = testCase.setup() 510 sandbox.appendChild(domTestSetup.from) 511 domCounter.start(domTestSetup.from) 512 morphFn(domTestSetup.from, domTestSetup.to) 513 const domOperations = domCounter.stop() 514 sandbox.innerHTML = "" 515 516 for (let i = 0; i < actualIterations; i++) { 517 // Prepare batch 518 const batch = [] 519 for (let j = 0; j < batchSize; j++) { 520 batch.push(testCase.setup()) 521 } 522 523 // Measure batch execution 524 const start = performance.now() 525 for (let j = 0; j < batchSize; j++) { 526 sandbox.appendChild(batch[j].from) 527 morphFn(batch[j].from, batch[j].to) 528 sandbox.innerHTML = "" 529 } 530 const end = performance.now() 531 532 const elapsed = (end - start) / batchSize // Average per operation 533 times.push(elapsed) 534 } 535 536 // Calculate statistics with better precision 537 const totalTime = times.reduce((a, b) => a + b, 0) 538 const averageTime = totalTime / times.length 539 const sorted = [...times].sort((a, b) => a - b) 540 const median = sorted[Math.floor(sorted.length / 2)] 541 const min = sorted[0] 542 const max = sorted[sorted.length - 1] 543 // Prevent infinity and provide more precision 544 const opsPerSecond = averageTime > 0 ? 1000 / averageTime : 0 545 546 return { 547 library, 548 testName: testCase.name, 549 iterations: actualIterations * batchSize, 550 totalTime: totalTime, 551 averageTime, 552 median, 553 min, 554 max, 555 opsPerSecond, 556 domOperations, 557 } 558 } 559 560 async runTestCase(testCase, onProgress) { 561 const results = [] 562 const libraries = [ 563 { name: "morphlex", fn: (from, to) => morphlex(from, to) }, 564 { name: "data-morph", fn: (from, to) => dataMorph(from, to.cloneNode(true), "outer") }, 565 { name: "idiomorph", fn: (from, to) => Idiomorph.morph(from, to) }, 566 { name: "morphdom", fn: (from, to) => morphdom(from, to.cloneNode(true)) }, 567 { name: "nanomorph", fn: (from, to) => nanomorph(from, to.cloneNode(true)) }, 568 { name: "alpine-morph", fn: (from, to) => Alpine.morph(from, to) }, 569 ] 570 571 for (const lib of libraries) { 572 if (stopRequested) break 573 574 try { 575 const result = await this.runSingleBenchmark(lib.name, testCase, lib.fn) 576 results.push(result) 577 this.results.push(result) 578 if (onProgress) onProgress(result) 579 } catch (error) { 580 console.error(`Error running ${lib.name}:`, error) 581 results.push({ 582 library: lib.name, 583 testName: testCase.name, 584 error: error.message, 585 }) 586 } 587 588 // Small delay between libraries 589 await new Promise((resolve) => setTimeout(resolve, 50)) 590 } 591 592 return results 593 } 594 } 595 596 // Test cases 597 const testCases = [ 598 { 599 name: "Simple Text Update", 600 description: "Morphing a single text node change", 601 setup: () => { 602 const from = document.createElement("div") 603 from.innerHTML = "<p>Hello World</p>" 604 const to = document.createElement("div") 605 to.innerHTML = "<p>Hello Morphlex</p>" 606 return { from: from.firstElementChild, to: to.firstElementChild } 607 }, 608 }, 609 { 610 name: "Attribute Changes", 611 description: "Updating multiple attributes on elements", 612 setup: () => { 613 const from = document.createElement("div") 614 from.innerHTML = ` 615 <div class="old-class" data-value="1"> 616 <span id="test" title="old">Content</span> 617 </div> 618 ` 619 const to = document.createElement("div") 620 to.innerHTML = ` 621 <div class="new-class" data-value="2" data-new="true"> 622 <span id="test" title="new" aria-label="label">Content</span> 623 </div> 624 ` 625 return { from: from.firstElementChild, to: to.firstElementChild } 626 }, 627 }, 628 { 629 name: "List Reordering", 630 description: "Reordering items in a list", 631 setup: () => { 632 const from = document.createElement("ul") 633 from.innerHTML = ` 634 <li>First</li> 635 <li>Second</li> 636 <li>Third</li> 637 <li>Fourth</li> 638 <li>Fifth</li> 639 ` 640 const to = document.createElement("ul") 641 to.innerHTML = ` 642 <li>Third</li> 643 <li>First</li> 644 <li>Fifth</li> 645 <li>Second</li> 646 <li>Fourth</li> 647 ` 648 return { from, to } 649 }, 650 }, 651 { 652 name: "Large List", 653 description: "Morphing a list with 50 items", 654 setup: () => { 655 const from = document.createElement("ul") 656 for (let i = 1; i <= 50; i++) { 657 const li = document.createElement("li") 658 li.textContent = `Item ${i}` 659 from.appendChild(li) 660 } 661 const to = document.createElement("ul") 662 const indices = Array.from({ length: 50 }, (_, i) => i + 1) 663 // Simple shuffle 664 for (let i = indices.length - 1; i > 0; i--) { 665 const j = Math.floor(Math.random() * (i + 1)) 666 ;[indices[i], indices[j]] = [indices[j], indices[i]] 667 } 668 for (const i of indices.slice(0, 45)) { 669 const li = document.createElement("li") 670 li.textContent = i % 3 === 0 ? `Modified Item ${i}` : `Item ${i}` 671 to.appendChild(li) 672 } 673 return { from, to } 674 }, 675 }, 676 { 677 name: "Large List - Add One Item", 678 description: "Adding a single item to a list of 100 items", 679 setup: () => { 680 const from = document.createElement("ul") 681 for (let i = 1; i <= 100; i++) { 682 const li = document.createElement("li") 683 li.textContent = `Item ${i}` 684 from.appendChild(li) 685 } 686 const to = document.createElement("ul") 687 for (let i = 1; i <= 100; i++) { 688 const li = document.createElement("li") 689 li.textContent = `Item ${i}` 690 to.appendChild(li) 691 } 692 // Add new item at position 50 693 const newLi = document.createElement("li") 694 newLi.textContent = "New Item" 695 to.insertBefore(newLi, to.children[50]) 696 return { from, to } 697 }, 698 }, 699 { 700 name: "Large List - Remove One Item", 701 description: "Removing a single item from a list of 100 items", 702 setup: () => { 703 const from = document.createElement("ul") 704 for (let i = 1; i <= 100; i++) { 705 const li = document.createElement("li") 706 li.textContent = `Item ${i}` 707 from.appendChild(li) 708 } 709 const to = document.createElement("ul") 710 for (let i = 1; i <= 100; i++) { 711 if (i === 50) continue // Skip item 50 712 const li = document.createElement("li") 713 li.textContent = `Item ${i}` 714 to.appendChild(li) 715 } 716 return { from, to } 717 }, 718 }, 719 { 720 name: "Large List - Resort All Items", 721 description: "Resorting all items in a list of 100 items", 722 setup: () => { 723 const from = document.createElement("ul") 724 for (let i = 1; i <= 100; i++) { 725 const li = document.createElement("li") 726 li.textContent = `Item ${i}` 727 from.appendChild(li) 728 } 729 const to = document.createElement("ul") 730 // Reverse the order 731 for (let i = 100; i >= 1; i--) { 732 const li = document.createElement("li") 733 li.textContent = `Item ${i}` 734 to.appendChild(li) 735 } 736 return { from, to } 737 }, 738 }, 739 { 740 name: "Large List - Partial Reorder", 741 description: "Reordering some items in a list of 100 items while keeping many in place", 742 setup: () => { 743 const from = document.createElement("ul") 744 for (let i = 1; i <= 100; i++) { 745 const li = document.createElement("li") 746 li.id = `item-${i}` 747 li.textContent = `Item ${i}` 748 from.appendChild(li) 749 } 750 const to = document.createElement("ul") 751 // Partial reorder: move every 5th item to a different position 752 // [1,2,3,4,5,6,7,8,9,10,...] → [5,1,2,3,4,10,6,7,8,9,15,11,12,13,14,20,...] 753 // This keeps most items in order (good for LIS) while shuffling some 754 const items = [] 755 for (let i = 1; i <= 100; i++) { 756 items.push(i) 757 } 758 const reordered = [] 759 for (let i = 0; i < items.length; i += 5) { 760 if (i + 4 < items.length) { 761 // Move 5th item to front of group 762 reordered.push(items[i + 4]) 763 reordered.push(items[i]) 764 reordered.push(items[i + 1]) 765 reordered.push(items[i + 2]) 766 reordered.push(items[i + 3]) 767 } else { 768 // Handle remaining items 769 for (let j = i; j < items.length; j++) { 770 reordered.push(items[j]) 771 } 772 } 773 } 774 for (const num of reordered) { 775 const li = document.createElement("li") 776 li.id = `item-${num}` 777 li.textContent = `Item ${num}` 778 to.appendChild(li) 779 } 780 return { from, to } 781 }, 782 }, 783 { 784 name: "Deep Nesting", 785 description: "Morphing deeply nested structures", 786 setup: () => { 787 const from = document.createElement("div") 788 from.innerHTML = ` 789 <div> 790 <section> 791 <article> 792 <header> 793 <h1>Title</h1> 794 <p>Subtitle</p> 795 </header> 796 <div> 797 <p>Paragraph 1</p> 798 <p>Paragraph 2</p> 799 </div> 800 </article> 801 </section> 802 </div> 803 ` 804 const to = document.createElement("div") 805 to.innerHTML = ` 806 <div> 807 <section> 808 <article> 809 <header> 810 <h1>New Title</h1> 811 <p>New Subtitle</p> 812 </header> 813 <div> 814 <p>Modified Paragraph 1</p> 815 <p>Paragraph 2</p> 816 <p>Paragraph 3</p> 817 </div> 818 </article> 819 </section> 820 </div> 821 ` 822 return { from: from.firstElementChild, to: to.firstElementChild } 823 }, 824 }, 825 ] 826 827 // UI Functions 828 function updateProgress(current, total) { 829 const percent = (current / total) * 100 830 document.getElementById("progress").style.width = `${percent}%` 831 } 832 833 function displayResults(allResults) { 834 const resultsEl = document.getElementById("results") 835 resultsEl.innerHTML = "" 836 837 // Group results by test case 838 const testGroups = {} 839 allResults.forEach((result) => { 840 if (!testGroups[result.testName]) { 841 testGroups[result.testName] = [] 842 } 843 testGroups[result.testName].push(result) 844 }) 845 846 // Calculate overall stats first for summary 847 const libraryStats = {} 848 allResults.forEach((result) => { 849 if (!result.error) { 850 if (!libraryStats[result.library]) { 851 libraryStats[result.library] = { 852 totalTime: 0, 853 count: 0, 854 wins: 0, 855 } 856 } 857 libraryStats[result.library].totalTime += result.averageTime 858 libraryStats[result.library].count++ 859 } 860 }) 861 862 // Count wins 863 Object.keys(testGroups).forEach((testName) => { 864 const testResults = allResults.filter((r) => r.testName === testName && !r.error) 865 if (testResults.length > 0) { 866 const winner = testResults.reduce((min, r) => (r.averageTime < min.averageTime ? r : min)) 867 if (libraryStats[winner.library]) { 868 libraryStats[winner.library].wins++ 869 } 870 } 871 }) 872 873 const summaryData = Object.entries(libraryStats) 874 .map(([library, stats]) => ({ 875 library, 876 avgTime: stats.totalTime / stats.count, 877 wins: stats.wins, 878 totalTests: stats.count, 879 })) 880 .sort((a, b) => a.avgTime - b.avgTime) 881 882 // Create summary section 883 const summary = document.createElement("div") 884 summary.className = "summary" 885 summary.innerHTML = "<h2>🏆 Overall Results</h2>" 886 887 const summaryGrid = document.createElement("div") 888 summaryGrid.className = "summary-grid" 889 890 summaryData.forEach((data, index) => { 891 const card = document.createElement("div") 892 card.className = "summary-card" 893 const medal = index === 0 ? "🥇" : index === 1 ? "🥈" : index === 2 ? "🥉" : `#${index + 1}` 894 const avgTime = data.avgTime < 0.001 ? data.avgTime.toExponential(2) : data.avgTime.toFixed(4) 895 896 card.innerHTML = ` 897 <div style="font-size: 2rem; margin-bottom: 0.5rem">${medal}</div> 898 <div class="summary-library">${data.library}</div> 899 <div class="summary-score">${avgTime} ms</div> 900 <div style="margin-top: 0.5rem; font-size: 0.9rem"> 901 ${data.wins} wins / ${data.totalTests} tests 902 </div> 903 ` 904 summaryGrid.appendChild(card) 905 }) 906 907 summary.appendChild(summaryGrid) 908 resultsEl.appendChild(summary) 909 910 // Display each test case 911 Object.entries(testGroups).forEach(([testName, results]) => { 912 const testCase = document.createElement("div") 913 testCase.className = "test-case" 914 915 const testDesc = testCases.find((t) => t.name === testName) 916 testCase.innerHTML = ` 917 <h3>${testName}</h3> 918 <p class="test-description">${testDesc ? testDesc.description : ""}</p> 919 ` 920 921 // Sort by performance 922 const sorted = [...results].sort((a, b) => { 923 if (a.error) return 1 924 if (b.error) return -1 925 return a.averageTime - b.averageTime 926 }) 927 928 // Create visual results for each library 929 sorted.forEach((result, index) => { 930 const libResult = document.createElement("div") 931 libResult.className = "library-result" 932 933 if (result.error) { 934 libResult.innerHTML = ` 935 <div class="library-name">${result.library}</div> 936 <div class="error">ERROR: ${result.error}</div> 937 ` 938 } else { 939 const msOp = result.averageTime < 0.001 ? result.averageTime.toExponential(2) : result.averageTime.toFixed(4) 940 const opsPerSec = 941 result.opsPerSecond > 1000000 942 ? result.opsPerSecond.toExponential(2) 943 : Math.round(result.opsPerSecond).toLocaleString() 944 945 const dom = result.domOperations 946 const rankBadge = index === 0 ? "🥇" : index === 1 ? "🥈" : index === 2 ? "🥉" : "" 947 948 libResult.innerHTML = ` 949 <div class="library-name">${result.library}</div> 950 <div class="metrics"> 951 <div class="metric"> 952 <div class="metric-value">${msOp}</div> 953 <div class="metric-label">ms/op</div> 954 </div> 955 <div class="metric"> 956 <div class="metric-value">${opsPerSec}</div> 957 <div class="metric-label">ops/sec</div> 958 </div> 959 <div class="metric"> 960 <div class="metric-value">${dom ? dom.total : "N/A"}</div> 961 <div class="metric-label">DOM ops</div> 962 </div> 963 <div class="metric"> 964 <div class="metric-value">${dom ? dom.addedNodes + dom.removedNodes : "N/A"}</div> 965 <div class="metric-label">nodes +/-</div> 966 </div> 967 <div class="metric"> 968 <div class="metric-value">${dom ? dom.attributes : "N/A"}</div> 969 <div class="metric-label">attrs</div> 970 </div> 971 </div> 972 <div class="rank rank-${index + 1}">${rankBadge}</div> 973 ` 974 } 975 976 testCase.appendChild(libResult) 977 }) 978 979 resultsEl.appendChild(testCase) 980 }) 981 } 982 983 // Event handlers 984 document.getElementById("runBtn").addEventListener("click", async () => { 985 if (isRunning) return 986 987 isRunning = true 988 stopRequested = false 989 const runBtn = document.getElementById("runBtn") 990 const stopBtn = document.getElementById("stopBtn") 991 const statusEl = document.getElementById("status") 992 const resultsEl = document.getElementById("results") 993 994 runBtn.style.display = "none" 995 stopBtn.style.display = "inline-block" 996 statusEl.textContent = "Running..." 997 statusEl.className = "status running" 998 resultsEl.innerHTML = "" 999 1000 const iterations = parseInt(document.getElementById("iterations").value) 1001 const warmup = parseInt(document.getElementById("warmup").value) 1002 1003 const benchmark = new BrowserBenchmark(iterations, warmup) 1004 const totalTests = testCases.length * 4 // 4 libraries per test 1005 let completedTests = 0 1006 1007 for (const testCase of testCases) { 1008 if (stopRequested) break 1009 1010 await benchmark.runTestCase(testCase, (result) => { 1011 completedTests++ 1012 updateProgress(completedTests, totalTests) 1013 }) 1014 } 1015 1016 if (!stopRequested && benchmark.results.length > 0) { 1017 displayResults(benchmark.results) 1018 } 1019 1020 isRunning = false 1021 runBtn.style.display = "inline-block" 1022 stopBtn.style.display = "none" 1023 statusEl.textContent = stopRequested ? "Stopped" : "Complete" 1024 statusEl.className = stopRequested ? "status" : "status complete" 1025 updateProgress(100, 100) 1026 }) 1027 1028 document.getElementById("stopBtn").addEventListener("click", () => { 1029 stopRequested = true 1030 }) 1031 </script> 1032 </body> 1033</html>