lightpanda (#646)

* lightpanda

* lightpanda benchmarks

* improvements

* fixes

* improvements
This commit is contained in:
Chris Tate
2026-03-06 11:16:37 -06:00
committed by GitHub
parent 36c2e06f89
commit 0da54c7038
25 changed files with 2190 additions and 74 deletions
+317
View File
@@ -0,0 +1,317 @@
import type { BenchmarkCommand, Scenario } from "./scenarios.js";
// ---------------------------------------------------------------------------
// HTML generators for realistic pages with complex DOM structures
// ---------------------------------------------------------------------------
function generateArticlePage(): string {
const paragraphs = Array.from({ length: 30 }, (_, i) => {
const words = Array.from(
{ length: 40 + (i % 5) * 10 },
(_, w) => ["the", "quick", "browser", "engine", "renders", "content", "across", "multiple", "layout", "passes", "while", "handling", "style", "recalculations", "and", "DOM", "mutations"][w % 17],
).join(" ");
return `<p class="article-p">${words}</p>`;
});
const comments = Array.from(
{ length: 40 },
(_, i) =>
`<div class="comment" data-id="${i}">` +
`<div class="comment-header"><span class="author">User ${i}</span><time>2025-01-${String(i % 28 + 1).padStart(2, "0")}</time></div>` +
`<div class="comment-body"><p>This is comment number ${i + 1} with some discussion text.</p></div>` +
`<div class="comment-actions"><button class="reply-btn">Reply</button><button class="like-btn">Like</button></div>` +
`</div>`,
);
const sidebar = Array.from(
{ length: 20 },
(_, i) =>
`<li class="sidebar-item"><a href="#section-${i}">Related Article ${i + 1}: A Longer Title Here</a></li>`,
);
return [
"<html><head><title>Benchmark Article</title>",
"<style>",
"body{font-family:system-ui;margin:0;padding:0;display:grid;grid-template-columns:1fr 300px;gap:20px;max-width:1200px;margin:0 auto}",
".article{padding:20px}.sidebar{padding:20px;border-left:1px solid #ddd}",
".comment{border:1px solid #eee;padding:12px;margin:8px 0;border-radius:4px}",
".comment-header{display:flex;justify-content:space-between;font-size:14px;color:#666}",
".nav{display:flex;gap:16px;padding:12px 20px;background:#f5f5f5;grid-column:1/-1}",
".tag{display:inline-block;padding:2px 8px;background:#e0e7ff;border-radius:12px;font-size:12px;margin:2px}",
"</style></head><body>",
`<nav class="nav">${Array.from({ length: 8 }, (_, i) => `<a href="#nav-${i}">Section ${i + 1}</a>`).join("")}</nav>`,
'<div class="article">',
"<h1>Understanding Modern Browser Engine Architecture</h1>",
'<div class="meta"><span class="author">Dr. Smith</span> | <time>2025-03-15</time> | <span>15 min read</span></div>',
`<div class="tags">${Array.from({ length: 6 }, (_, i) => `<span class="tag">tag-${i + 1}</span>`).join("")}</div>`,
"<h2>Introduction</h2>",
...paragraphs.slice(0, 5),
"<h2>Core Concepts</h2>",
...paragraphs.slice(5, 12),
'<blockquote>"Performance is not just about speed, it is about efficiency." - Anonymous</blockquote>',
"<h2>Implementation Details</h2>",
...paragraphs.slice(12, 20),
"<h3>Subsection A</h3>",
...paragraphs.slice(20, 25),
"<h3>Subsection B</h3>",
...paragraphs.slice(25),
"<h2>Comments</h2>",
'<div class="comments">',
...comments,
"</div></div>",
'<div class="sidebar">',
"<h3>Related Articles</h3>",
`<ul>${sidebar.join("")}</ul>`,
"<h3>Archives</h3>",
`<ul>${Array.from({ length: 12 }, (_, i) => `<li><a href="#month-${i}">Month ${i + 1}, 2025</a></li>`).join("")}</ul>`,
"</div>",
"</body></html>",
].join("");
}
function generateDataTablePage(): string {
const headerCells = [
"ID", "Name", "Email", "Department", "Role", "Status", "Joined", "Last Active",
];
const header = `<tr>${headerCells.map((h) => `<th>${h}</th>`).join("")}</tr>`;
const rows = Array.from({ length: 200 }, (_, i) => {
const dept = ["Engineering", "Design", "Marketing", "Sales", "Support"][i % 5];
const role = ["Admin", "Manager", "Member", "Viewer"][i % 4];
const status = ["Active", "Inactive", "Pending"][i % 3];
return (
`<tr data-row="${i}">` +
`<td>${i + 1}</td>` +
`<td><a href="#user-${i}">User ${i + 1}</a></td>` +
`<td>user${i + 1}@example.com</td>` +
`<td>${dept}</td>` +
`<td><span class="badge badge-${role.toLowerCase()}">${role}</span></td>` +
`<td><span class="status status-${status.toLowerCase()}">${status}</span></td>` +
`<td>2024-${String((i % 12) + 1).padStart(2, "0")}-${String((i % 28) + 1).padStart(2, "0")}</td>` +
`<td>${i % 3 === 0 ? "Today" : i % 3 === 1 ? "Yesterday" : "Last week"}</td>` +
`</tr>`
);
});
return [
"<html><head><title>Benchmark Table</title>",
"<style>",
"body{font-family:system-ui;margin:20px}",
"table{width:100%;border-collapse:collapse}",
"th,td{padding:8px 12px;border:1px solid #ddd;text-align:left}",
"th{background:#f5f5f5;font-weight:600;position:sticky;top:0}",
"tr:nth-child(even){background:#fafafa}",
".badge{padding:2px 8px;border-radius:4px;font-size:12px}",
".toolbar{display:flex;gap:12px;margin-bottom:16px;align-items:center}",
"input,select,button{padding:6px 12px;border:1px solid #ccc;border-radius:4px}",
"</style></head><body>",
"<h1>User Management Dashboard</h1>",
'<div class="toolbar">',
'<input id="search" type="text" placeholder="Search users...">',
'<select id="dept-filter"><option value="">All Departments</option><option value="eng">Engineering</option><option value="des">Design</option></select>',
'<select id="status-filter"><option value="">All Statuses</option><option value="active">Active</option><option value="inactive">Inactive</option></select>',
'<button id="add-user">Add User</button>',
'<span id="count">Showing 200 users</span>',
"</div>",
`<table><thead>${header}</thead><tbody>${rows.join("")}</tbody></table>`,
'<div class="pagination">',
...Array.from({ length: 10 }, (_, i) => `<button class="page-btn" data-page="${i + 1}">${i + 1}</button>`),
"</div>",
"</body></html>",
].join("");
}
function generateNestedPage(): string {
function nest(depth: number, breadth: number, prefix: string): string {
if (depth === 0) {
return `<span class="leaf" data-path="${prefix}">Leaf node at ${prefix}</span>`;
}
const children = Array.from(
{ length: breadth },
(_, i) =>
`<div class="node depth-${depth}" data-depth="${depth}" data-idx="${i}">` +
`<div class="node-header"><strong>Section ${prefix}.${i + 1}</strong> <em>(depth ${depth})</em></div>` +
`<div class="node-content">${nest(depth - 1, Math.max(2, breadth - 1), `${prefix}.${i + 1}`)}</div>` +
`</div>`,
);
return children.join("");
}
return [
"<html><head><title>Benchmark Nested</title>",
"<style>",
"body{font-family:system-ui;margin:20px}",
".node{border-left:2px solid #ddd;padding-left:16px;margin:4px 0}",
".node-header{padding:4px 0;cursor:pointer}",
".leaf{display:block;padding:2px 8px;background:#f0f9ff;margin:2px 0;border-radius:2px}",
"</style></head><body>",
"<h1>Deeply Nested Document Structure</h1>",
nest(7, 3, "root"),
"</body></html>",
].join("");
}
function generateDashboardPage(): string {
const cards = Array.from(
{ length: 12 },
(_, i) =>
`<div class="card" data-card="${i}">` +
`<div class="card-title">Metric ${i + 1}</div>` +
`<div class="card-value">${Math.floor(Math.random() * 10000)}</div>` +
`<div class="card-trend ${i % 2 === 0 ? "up" : "down"}">${i % 2 === 0 ? "+" : "-"}${(Math.random() * 20).toFixed(1)}%</div>` +
`</div>`,
);
const chartBars = Array.from(
{ length: 24 },
(_, i) => {
const h = 20 + (i * 7 + 13) % 80;
return `<div class="bar" style="height:${h}%" data-hour="${i}"><span class="bar-label">${String(i).padStart(2, "0")}:00</span></div>`;
},
);
const logRows = Array.from(
{ length: 100 },
(_, i) => {
const level = ["INFO", "WARN", "ERROR", "DEBUG"][i % 4];
return (
`<tr class="log-${level.toLowerCase()}" data-log="${i}">` +
`<td>${new Date(2025, 0, 1, i % 24, i % 60).toISOString()}</td>` +
`<td><span class="level level-${level.toLowerCase()}">${level}</span></td>` +
`<td>Service ${["auth", "api", "worker", "cache", "db"][i % 5]}</td>` +
`<td>Log message number ${i + 1}: operation completed in ${(Math.random() * 1000).toFixed(0)}ms</td>` +
`</tr>`
);
},
);
return [
"<html><head><title>Benchmark Dashboard</title>",
"<style>",
"body{font-family:system-ui;margin:0;background:#f5f5f5}",
".header{background:#1a1a2e;color:white;padding:12px 24px;display:flex;justify-content:space-between;align-items:center}",
".grid{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;padding:24px}",
".card{background:white;padding:20px;border-radius:8px;box-shadow:0 1px 3px rgba(0,0,0,.1)}",
".card-value{font-size:28px;font-weight:700;margin:8px 0}",
".card-trend.up{color:#16a34a}.card-trend.down{color:#dc2626}",
".chart-area{background:white;margin:0 24px;padding:20px;border-radius:8px;box-shadow:0 1px 3px rgba(0,0,0,.1)}",
".bars{display:flex;align-items:flex-end;gap:4px;height:200px}",
".bar{background:#3b82f6;flex:1;border-radius:2px 2px 0 0;position:relative;min-width:8px}",
".log-table{margin:24px;background:white;border-radius:8px;box-shadow:0 1px 3px rgba(0,0,0,.1);overflow:hidden}",
"table{width:100%;border-collapse:collapse;font-size:13px}",
"th,td{padding:6px 12px;border-bottom:1px solid #eee;text-align:left}",
"th{background:#f9fafb;font-weight:600}",
".tabs{display:flex;gap:0;margin:24px 24px 0}",
".tab{padding:8px 20px;background:#e5e7eb;cursor:pointer;border-radius:6px 6px 0 0}",
".tab.active{background:white}",
"</style></head><body>",
'<div class="header"><h1>Operations Dashboard</h1><div><input id="dash-search" placeholder="Search..." type="text"><button id="refresh">Refresh</button></div></div>',
`<div class="grid">${cards.join("")}</div>`,
'<div class="tabs"><div class="tab active">Hourly</div><div class="tab">Daily</div><div class="tab">Weekly</div></div>',
`<div class="chart-area"><h3>Request Volume</h3><div class="bars">${chartBars.join("")}</div></div>`,
'<div class="log-table">',
"<h3 style='padding:16px 12px 0'>Recent Logs</h3>",
`<table><thead><tr><th>Timestamp</th><th>Level</th><th>Service</th><th>Message</th></tr></thead><tbody>${logRows.join("")}</tbody></table>`,
"</div>",
"</body></html>",
].join("");
}
// ---------------------------------------------------------------------------
// Pre-build HTML strings and injection commands
// ---------------------------------------------------------------------------
const ARTICLE_HTML = generateArticlePage();
const TABLE_HTML = generateDataTablePage();
const NESTED_HTML = generateNestedPage();
const DASHBOARD_HTML = generateDashboardPage();
function injectCmd(id: string, html: string): BenchmarkCommand {
return {
id,
action: "evaluate",
script: `document.open(); document.write(${JSON.stringify(html)}); document.close(); 'ok'`,
};
}
function setupPage(html: string, tag: string): BenchmarkCommand[] {
return [
{ id: `${tag}-nav`, action: "navigate", url: "about:blank", waitUntil: "domcontentloaded" },
injectCmd(`${tag}-inject`, html),
];
}
// ---------------------------------------------------------------------------
// Engine-specific scenarios: complex pages that stress real-world workloads
// ---------------------------------------------------------------------------
export const engineScenarios: Scenario[] = [
{
name: "article-snapshot",
description: "Snapshot a realistic article page (~800 DOM nodes, 30 paragraphs, 40 comments)",
setup: setupPage(ARTICLE_HTML, "art"),
commands: [{ id: "snap", action: "snapshot" }],
},
{
name: "table-snapshot",
description: "Snapshot a data table with 200 rows and 8 columns",
setup: setupPage(TABLE_HTML, "tbl"),
commands: [{ id: "snap", action: "snapshot" }],
},
{
name: "nested-snapshot",
description: "Snapshot a deeply nested DOM tree (7 levels, ~3000 nodes)",
setup: setupPage(NESTED_HTML, "nest"),
commands: [{ id: "snap", action: "snapshot" }],
},
{
name: "dashboard-snap",
description: "Snapshot an operations dashboard with cards, chart, and 100 log rows",
setup: setupPage(DASHBOARD_HTML, "dash"),
commands: [{ id: "snap", action: "snapshot" }],
},
{
name: "article-inject",
description: "Write a full article page into the DOM (measures parse + layout)",
setup: [
{ id: "ai-nav", action: "navigate", url: "about:blank", waitUntil: "domcontentloaded" },
],
commands: [injectCmd("ai-write", ARTICLE_HTML)],
},
{
name: "table-query",
description: "Evaluate a querySelectorAll across a large table",
setup: setupPage(TABLE_HTML, "tq"),
commands: [
{
id: "query",
action: "evaluate",
script: "document.querySelectorAll('tr[data-row]').length + ' rows, ' + document.querySelectorAll('td').length + ' cells'",
},
],
},
{
name: "dashboard-workflow",
description: "Full agent workflow on complex dashboard: snapshot, click, fill, eval, screenshot",
setup: setupPage(DASHBOARD_HTML, "dw"),
commands: [
{ id: "dw-snap", action: "snapshot" },
{ id: "dw-fill", action: "fill", selector: "#dash-search", value: "error logs" },
{ id: "dw-click", action: "click", selector: "#refresh" },
{ id: "dw-eval", action: "evaluate", script: "document.querySelectorAll('.card').length + ' cards'" },
{ id: "dw-ss", action: "screenshot" },
],
},
{
name: "nested-eval",
description: "Recursive DOM traversal via evaluate on deeply nested tree",
setup: setupPage(NESTED_HTML, "ne"),
commands: [
{
id: "walk",
action: "evaluate",
script: "(function(){let c=0;const w=n=>{c++;for(const ch of n.children)w(ch);};w(document.body);return c+' nodes';})()",
},
],
},
];
+253
View File
@@ -0,0 +1,253 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Understanding Modern Browser Engine Architecture</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #1a1a2e; background: #fff; }
.nav { display: flex; align-items: center; gap: 24px; padding: 12px 24px; background: #1a1a2e; color: #fff; position: sticky; top: 0; z-index: 100; }
.nav a { color: #94a3b8; text-decoration: none; font-size: 14px; transition: color 0.2s; }
.nav a:hover { color: #fff; }
.layout { display: grid; grid-template-columns: 1fr 320px; gap: 40px; max-width: 1200px; margin: 0 auto; padding: 40px 24px; }
.article { min-width: 0; }
.article h1 { font-size: 2.2rem; line-height: 1.2; margin-bottom: 16px; }
.meta { display: flex; gap: 16px; color: #64748b; font-size: 14px; margin-bottom: 24px; padding-bottom: 24px; border-bottom: 1px solid #e2e8f0; }
.tags { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 32px; }
.tag { display: inline-block; padding: 4px 12px; background: #e0e7ff; color: #3730a3; border-radius: 16px; font-size: 12px; font-weight: 500; }
.article h2 { font-size: 1.5rem; margin: 32px 0 16px; padding-top: 24px; border-top: 1px solid #f1f5f9; }
.article h3 { font-size: 1.2rem; margin: 24px 0 12px; }
.article p { margin-bottom: 16px; color: #374151; }
.article blockquote { margin: 24px 0; padding: 16px 24px; border-left: 4px solid #6366f1; background: #f8fafc; font-style: italic; color: #475569; border-radius: 0 8px 8px 0; }
.article pre { background: #1e293b; color: #e2e8f0; padding: 16px 20px; border-radius: 8px; overflow-x: auto; margin: 16px 0; font-size: 14px; line-height: 1.5; }
.article code { font-family: 'SF Mono', 'Fira Code', monospace; }
.article img { max-width: 100%; height: auto; border-radius: 8px; margin: 16px 0; }
.figure { margin: 24px 0; text-align: center; }
.figure figcaption { font-size: 13px; color: #64748b; margin-top: 8px; }
.comments { margin-top: 40px; }
.comments h2 { border-top: 2px solid #e2e8f0; }
.comment { padding: 16px; margin: 12px 0; border: 1px solid #e2e8f0; border-radius: 8px; transition: box-shadow 0.2s; }
.comment:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.06); }
.comment-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
.comment-author { font-weight: 600; font-size: 14px; }
.comment-date { font-size: 12px; color: #94a3b8; }
.comment-body { font-size: 14px; color: #475569; }
.comment-actions { display: flex; gap: 12px; margin-top: 8px; }
.comment-actions button { background: none; border: none; color: #6366f1; font-size: 13px; cursor: pointer; padding: 2px 0; }
.sidebar { position: sticky; top: 72px; align-self: start; }
.sidebar section { margin-bottom: 32px; }
.sidebar h3 { font-size: 14px; text-transform: uppercase; letter-spacing: 0.05em; color: #64748b; margin-bottom: 12px; }
.sidebar ul { list-style: none; }
.sidebar li { margin-bottom: 8px; }
.sidebar a { color: #3730a3; text-decoration: none; font-size: 14px; }
.sidebar a:hover { text-decoration: underline; }
.toc a { display: block; padding: 4px 0; border-left: 2px solid transparent; padding-left: 12px; }
.toc a:hover { border-left-color: #6366f1; }
.sidebar .widget { background: #f8fafc; padding: 16px; border-radius: 8px; }
@media (max-width: 768px) { .layout { grid-template-columns: 1fr; } .sidebar { display: none; } }
</style>
</head>
<body>
<nav class="nav">
<strong style="font-size:18px;color:#fff">TechBlog</strong>
<a href="#">Home</a><a href="#">Articles</a><a href="#">Tutorials</a><a href="#">About</a>
<a href="#">Open Source</a><a href="#">Newsletter</a><a href="#">Contact</a>
<div style="flex:1"></div>
<a href="#">Sign In</a>
</nav>
<div class="layout">
<main class="article">
<h1>Understanding Modern Browser Engine Architecture</h1>
<div class="meta">
<span>By <strong>Dr. Alexandra Chen</strong></span>
<span>March 15, 2025</span>
<span>18 min read</span>
<span>2,847 views</span>
</div>
<div class="tags">
<span class="tag">Browser Engines</span><span class="tag">Performance</span>
<span class="tag">Web Standards</span><span class="tag">Rendering</span>
<span class="tag">Architecture</span><span class="tag">Open Source</span>
</div>
<p>Modern browser engines are among the most complex pieces of software ever created. They must parse HTML, CSS, and JavaScript, construct a DOM tree, compute styles, perform layout calculations, paint pixels, and composite layers -- all within milliseconds to maintain 60fps rendering.</p>
<p>This article explores the architecture of modern browser engines, examining how they process web content from raw bytes to rendered pixels on screen. We will trace the critical rendering path, examine optimization strategies, and understand why certain patterns lead to better performance.</p>
<h2>The Critical Rendering Path</h2>
<p>When a browser receives an HTML document, it begins a multi-stage pipeline known as the critical rendering path. Each stage transforms the document into progressively more structured representations until pixels are painted on screen.</p>
<p>The first stage involves parsing the HTML into a Document Object Model (DOM). The parser processes tokens sequentially, building a tree structure that represents the document's hierarchy. During this phase, the parser may encounter external resources like stylesheets and scripts that can block further processing.</p>
<p>CSS parsing happens in parallel where possible. The browser constructs the CSS Object Model (CSSOM), which represents all the style rules that apply to the document. This includes user-agent styles, author styles, and any inline styles specified directly on elements.</p>
<p>Once both the DOM and CSSOM are available, the browser combines them into a render tree. This tree contains only the elements that will be visible on screen -- elements with <code>display: none</code> are excluded, while pseudo-elements like <code>::before</code> and <code>::after</code> are added.</p>
<p>Layout (also called reflow) is the process of calculating the exact position and size of each element in the render tree. This is one of the most computationally expensive operations in the rendering pipeline, as changes to one element can cascade through the entire tree.</p>
<blockquote>"The fastest code is code that doesn't run. The fastest layout is layout that doesn't need to happen." -- Chrome DevTools Team</blockquote>
<h2>DOM Construction and Tree Building</h2>
<p>The DOM is a tree-structured representation of the HTML document. Each node in the tree corresponds to an element, text node, comment, or other construct in the HTML. The tree preserves the hierarchical relationships between elements, allowing efficient traversal and manipulation.</p>
<p>Modern parsers handle malformed HTML gracefully through error recovery algorithms specified in the HTML5 standard. This includes automatic closing of unclosed tags, adoption of misplaced elements, and reconstruction of the formatting element list.</p>
<p>Shadow DOM introduces additional complexity by creating encapsulated subtrees that can have their own scoped styles and behavior. Custom elements use shadow roots to attach shadow trees, which are rendered in place of the element's regular children.</p>
<h3>Incremental DOM Updates</h3>
<p>When JavaScript modifies the DOM, the browser must determine which parts of the rendering pipeline need to be re-executed. Modern engines use fine-grained invalidation to minimize the work required. A change to an element's text content, for example, may only require a repaint, while changing its width could trigger a full relayout of its subtree.</p>
<p>Mutation observers provide a way for JavaScript to respond to DOM changes without polling. The browser batches mutations and delivers them asynchronously, allowing multiple changes to be processed efficiently in a single callback.</p>
<h3>Memory Management</h3>
<p>DOM nodes are reference-counted objects that are garbage collected when no longer reachable. However, detached DOM trees -- subtrees that have been removed from the document but are still referenced by JavaScript -- represent a common source of memory leaks in web applications.</p>
<p>Browser engines use various strategies to minimize memory overhead: string interning for attribute names and common values, node pools for rapid allocation, and lazy initialization of rarely-accessed properties.</p>
<h2>Style Resolution and Cascade</h2>
<p>CSS style resolution involves matching each element against all applicable style rules and computing the final value for every CSS property. With thousands of rules and millions of elements on complex pages, this process must be highly optimized.</p>
<p>Modern engines use Bloom filters to quickly eliminate rules that cannot match an element, reducing the number of full selector matches required. Selector matching proceeds right-to-left, starting from the key selector (the rightmost part) and working backwards through ancestors.</p>
<p>The cascade algorithm resolves conflicts between competing declarations by considering origin, specificity, and source order. Custom properties (CSS variables) add another layer of complexity, as they must be resolved during the cascade before they can be used in property values.</p>
<p>Style sharing is an optimization where elements with identical computed styles share a single style data structure rather than each maintaining their own copy. This is particularly effective on pages with repetitive structures like lists and tables.</p>
<pre><code>/* Example: These list items can share computed styles */
.data-grid tr:nth-child(even) td {
background-color: #f8fafc;
padding: 8px 12px;
font-size: 14px;
border-bottom: 1px solid #e2e8f0;
}</code></pre>
<h2>Layout Algorithms</h2>
<p>Layout is the process of converting the styled render tree into a set of positioned boxes with concrete pixel dimensions. Different layout modes (block, inline, flex, grid, table) each have their own algorithm for determining element sizes and positions.</p>
<p>Flexbox layout involves multiple passes: first computing the flex basis of each item, then distributing free space according to flex-grow and flex-shrink factors, and finally positioning items along the cross axis. This multi-pass nature makes flex layout more expensive than simple block layout.</p>
<p>Grid layout is even more complex, supporting both explicit and implicit grid definitions, named areas, auto-placement, and spanning. The grid placement algorithm must resolve conflicts between explicitly-placed and auto-placed items while respecting sizing constraints.</p>
<p>Containing block queries are a frequent operation during layout. An element's containing block determines its available width for percentage calculations and establishes the coordinate system for positioned descendants. Finding the correct containing block requires walking up the tree, checking for elements that establish new containing blocks.</p>
<p>Fragmentation handles content that must be split across multiple pages or columns. The fragmentation algorithm inserts breaks at legal break points, avoiding orphans and widows while respecting the <code>break-before</code>, <code>break-after</code>, and <code>break-inside</code> properties.</p>
<h2>Paint and Compositing</h2>
<p>After layout, the browser must paint the visual representation of each element. This involves drawing backgrounds, borders, text, images, shadows, and other visual effects in the correct stacking order defined by the z-index property and stacking context rules.</p>
<p>Modern browsers use a layered compositing architecture. Elements that change frequently (animations, scrolling regions, video) are promoted to their own compositing layers. These layers can be updated independently and composited together on the GPU, avoiding expensive repaints of the entire page.</p>
<p>The compositor thread operates independently from the main thread, allowing smooth scrolling and animations even when JavaScript is executing. Touch events and scroll gestures are handled directly by the compositor, with the main thread notified asynchronously.</p>
<p>Paint operations are recorded into display lists -- serialized sequences of drawing commands. These display lists can be rasterized by worker threads on the CPU or directly by the GPU, depending on the content and the platform's capabilities.</p>
<p>Subpixel antialiasing, font hinting, and text shaping add complexity to text rendering. Each glyph must be positioned with fractional pixel precision, and the rendering must account for kerning pairs, ligatures, and complex scripts like Arabic and Devanagari that require contextual glyph substitution.</p>
<h2>JavaScript Engine Integration</h2>
<p>The JavaScript engine is tightly integrated with the browser's rendering pipeline. Script execution can trigger style recalculation, layout, and paint through DOM manipulation and CSSOM access. The browser must balance responsive script execution with maintaining smooth rendering.</p>
<p>Modern engines use just-in-time (JIT) compilation to achieve near-native performance for hot code paths. The compilation pipeline typically includes an interpreter for initial execution, a baseline compiler for warm functions, and an optimizing compiler for hot functions. Deoptimization handles cases where optimistic assumptions are invalidated.</p>
<p>Web Workers provide true parallelism by running JavaScript in separate threads with their own heap and message-passing communication. SharedArrayBuffer enables shared memory between workers, but requires careful synchronization to avoid data races.</p>
<p>The event loop orchestrates the interleaving of script execution, rendering, and I/O callbacks. Microtasks (promises, mutation observers) are processed between macrotasks, and rendering updates are synchronized with the display's refresh rate through requestAnimationFrame.</p>
<h2>Conclusion</h2>
<p>Browser engines represent decades of engineering effort to make the web fast, secure, and compatible. Understanding their architecture helps web developers write code that works with the browser rather than against it, leading to better performance and user experience.</p>
<p>As the web platform continues to evolve with new APIs, layout modes, and rendering capabilities, browser engines must adapt while maintaining backwards compatibility with billions of existing web pages. This tension between innovation and compatibility remains one of the greatest challenges in software engineering.</p>
<div class="comments">
<h2>Comments (50)</h2>
<script>
(function() {
var container = document.querySelector('.comments');
var names = ['Alex Morgan', 'Jamie Rivera', 'Sam Patel', 'Taylor Kim', 'Jordan Lee',
'Casey Wu', 'Riley Chen', 'Morgan Davis', 'Avery Singh', 'Quinn Zhao'];
for (var i = 0; i < 50; i++) {
var div = document.createElement('div');
div.className = 'comment';
div.dataset.id = i;
var d = new Date(2025, 2, 15 - Math.floor(i / 5));
div.innerHTML = '<div class="comment-header"><span class="comment-author">' + names[i % 10] +
'</span><span class="comment-date">' + d.toLocaleDateString() + '</span></div>' +
'<div class="comment-body"><p>' + (i % 3 === 0 ?
'Great article! The section on compositing layers was particularly insightful. I have been struggling with janky scroll performance and this explains why promoting elements to their own layer helps.' :
i % 3 === 1 ?
'Thanks for the detailed breakdown. One thing I would add is that the style invalidation strategy varies significantly between engines. Blink uses a different approach from WebKit for descendant invalidation.' :
'This is exactly the kind of deep dive I was looking for. The paint and compositing section cleared up several misconceptions I had about how GPU acceleration works in practice.') +
'</p></div><div class="comment-actions"><button>Reply</button><button>Like (' + (Math.floor(Math.random() * 30)) + ')</button></div>';
container.appendChild(div);
}
})();
</script>
</div>
</main>
<aside class="sidebar">
<section>
<h3>Table of Contents</h3>
<ul class="toc">
<li><a href="#crp">The Critical Rendering Path</a></li>
<li><a href="#dom">DOM Construction and Tree Building</a></li>
<li><a href="#styles">Style Resolution and Cascade</a></li>
<li><a href="#layout">Layout Algorithms</a></li>
<li><a href="#paint">Paint and Compositing</a></li>
<li><a href="#js">JavaScript Engine Integration</a></li>
<li><a href="#conclusion">Conclusion</a></li>
</ul>
</section>
<section>
<h3>Related Articles</h3>
<ul>
<li><a href="#">How V8 Optimizes JavaScript Execution</a></li>
<li><a href="#">CSS Grid Layout: A Complete Guide</a></li>
<li><a href="#">Web Performance Metrics That Matter</a></li>
<li><a href="#">Understanding the Event Loop</a></li>
<li><a href="#">Debugging Layout Thrashing</a></li>
<li><a href="#">Service Workers and Caching Strategies</a></li>
<li><a href="#">WebAssembly: Beyond JavaScript</a></li>
<li><a href="#">Rendering Performance Case Studies</a></li>
<li><a href="#">Accessibility Tree Deep Dive</a></li>
<li><a href="#">Cross-Browser Compatibility Patterns</a></li>
<li><a href="#">Progressive Web Apps in 2025</a></li>
<li><a href="#">HTTP/3 and QUIC Explained</a></li>
<li><a href="#">Container Queries Guide</a></li>
<li><a href="#">CSS Houdini: Low-Level APIs</a></li>
<li><a href="#">Browser DevTools Advanced Tips</a></li>
</ul>
</section>
<section>
<h3>Archives</h3>
<ul>
<li><a href="#">March 2025</a></li><li><a href="#">February 2025</a></li>
<li><a href="#">January 2025</a></li><li><a href="#">December 2024</a></li>
<li><a href="#">November 2024</a></li><li><a href="#">October 2024</a></li>
<li><a href="#">September 2024</a></li><li><a href="#">August 2024</a></li>
<li><a href="#">July 2024</a></li><li><a href="#">June 2024</a></li>
<li><a href="#">May 2024</a></li><li><a href="#">April 2024</a></li>
</ul>
</section>
<section class="widget">
<h3>Newsletter</h3>
<p style="font-size:13px;color:#475569;margin-bottom:8px">Get weekly browser engineering insights</p>
<input type="email" placeholder="you@example.com" style="width:100%;padding:8px;border:1px solid #d1d5db;border-radius:4px;margin-bottom:8px">
<button style="width:100%;padding:8px;background:#6366f1;color:#fff;border:none;border-radius:4px;cursor:pointer">Subscribe</button>
</section>
</aside>
</div>
</body>
</html>
+248
View File
@@ -0,0 +1,248 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Operations Dashboard</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f1f5f9; color: #0f172a; }
.header { background: #0f172a; color: #fff; padding: 12px 24px; display: flex; justify-content: space-between; align-items: center; }
.header h1 { font-size: 18px; }
.header-actions { display: flex; gap: 12px; align-items: center; }
.header input { padding: 6px 12px; border: 1px solid #334155; background: #1e293b; color: #fff; border-radius: 6px; font-size: 13px; width: 200px; }
.header button { padding: 6px 16px; background: #6366f1; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; }
.grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; padding: 24px; }
.card { background: #fff; padding: 20px; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); }
.card-label { font-size: 13px; color: #64748b; text-transform: uppercase; letter-spacing: 0.05em; }
.card-value { font-size: 32px; font-weight: 700; margin: 8px 0 4px; }
.card-trend { font-size: 14px; font-weight: 500; }
.card-trend.up { color: #16a34a; }
.card-trend.down { color: #dc2626; }
.card-sparkline { height: 40px; display: flex; align-items: flex-end; gap: 2px; margin-top: 8px; }
.card-sparkline .bar { flex: 1; background: #e0e7ff; border-radius: 2px; min-width: 3px; transition: background 0.2s; }
.card-sparkline .bar:last-child { background: #6366f1; }
.section { margin: 0 24px 24px; background: #fff; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); overflow: hidden; }
.section-header { padding: 16px 20px; border-bottom: 1px solid #f1f5f9; display: flex; justify-content: space-between; align-items: center; }
.section-header h2 { font-size: 16px; }
.tabs { display: flex; gap: 0; }
.tab { padding: 6px 16px; font-size: 13px; border: 1px solid #e2e8f0; background: #fff; cursor: pointer; }
.tab:first-child { border-radius: 6px 0 0 6px; }
.tab:last-child { border-radius: 0 6px 6px 0; }
.tab.active { background: #6366f1; color: #fff; border-color: #6366f1; }
.chart { padding: 20px; height: 240px; display: flex; align-items: flex-end; gap: 4px; }
.chart .bar { flex: 1; background: #6366f1; border-radius: 4px 4px 0 0; position: relative; min-width: 6px; transition: opacity 0.2s; }
.chart .bar:hover { opacity: 0.8; }
.chart .bar-label { position: absolute; bottom: -20px; left: 50%; transform: translateX(-50%); font-size: 10px; color: #94a3b8; white-space: nowrap; }
table { width: 100%; border-collapse: collapse; font-size: 13px; }
thead th { background: #f8fafc; padding: 10px 16px; text-align: left; font-weight: 600; color: #475569; border-bottom: 1px solid #e2e8f0; position: sticky; top: 0; }
tbody td { padding: 8px 16px; border-bottom: 1px solid #f1f5f9; }
tbody tr:hover { background: #f8fafc; }
.badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; }
.badge-info { background: #dbeafe; color: #1d4ed8; }
.badge-warn { background: #fef3c7; color: #b45309; }
.badge-error { background: #fee2e2; color: #dc2626; }
.badge-debug { background: #f1f5f9; color: #475569; }
.badge-active { background: #dcfce7; color: #166534; }
.badge-inactive { background: #f1f5f9; color: #64748b; }
.badge-pending { background: #fef3c7; color: #b45309; }
.pagination { display: flex; justify-content: center; gap: 4px; padding: 16px; }
.pagination button { width: 32px; height: 32px; border: 1px solid #e2e8f0; background: #fff; border-radius: 6px; cursor: pointer; font-size: 13px; }
.pagination button.active { background: #6366f1; color: #fff; border-color: #6366f1; }
.two-col { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; padding: 0 24px 24px; }
@media (max-width: 1024px) { .grid { grid-template-columns: repeat(2, 1fr); } .two-col { grid-template-columns: 1fr; } }
</style>
</head>
<body>
<div class="header">
<h1>Operations Dashboard</h1>
<div class="header-actions">
<input id="search" type="text" placeholder="Search...">
<button id="refresh">Refresh</button>
<button style="background:#334155">Export</button>
</div>
</div>
<div class="grid" id="metrics-grid"></div>
<div class="section">
<div class="section-header">
<h2>Request Volume</h2>
<div class="tabs">
<div class="tab active">Hourly</div>
<div class="tab">Daily</div>
<div class="tab">Weekly</div>
</div>
</div>
<div class="chart" id="chart"></div>
</div>
<div class="two-col">
<div class="section" style="margin:0">
<div class="section-header"><h2>Top Endpoints</h2></div>
<table>
<thead><tr><th>Endpoint</th><th>Requests</th><th>Avg Latency</th><th>Error Rate</th></tr></thead>
<tbody id="endpoints-table"></tbody>
</table>
</div>
<div class="section" style="margin:0">
<div class="section-header"><h2>Active Alerts</h2></div>
<table>
<thead><tr><th>Alert</th><th>Severity</th><th>Service</th><th>Since</th></tr></thead>
<tbody id="alerts-table"></tbody>
</table>
</div>
</div>
<div class="section">
<div class="section-header">
<h2>Recent Logs</h2>
<div class="tabs">
<div class="tab active">All</div>
<div class="tab">Errors</div>
<div class="tab">Warnings</div>
</div>
</div>
<table>
<thead><tr><th>Timestamp</th><th>Level</th><th>Service</th><th>Message</th><th>Duration</th></tr></thead>
<tbody id="logs-table"></tbody>
</table>
<div class="pagination" id="pagination"></div>
</div>
<div class="section">
<div class="section-header"><h2>Service Status</h2></div>
<table>
<thead><tr><th>Service</th><th>Status</th><th>Uptime</th><th>CPU</th><th>Memory</th><th>Requests/min</th><th>Error Rate</th><th>Last Deploy</th></tr></thead>
<tbody id="services-table"></tbody>
</table>
</div>
<script>
(function() {
// Metric cards
var metrics = [
{ label: 'Total Requests', value: '1,284,392', trend: '+12.5%', up: true },
{ label: 'Avg Response Time', value: '142ms', trend: '-8.3%', up: true },
{ label: 'Error Rate', value: '0.42%', trend: '+0.12%', up: false },
{ label: 'Active Users', value: '3,847', trend: '+5.1%', up: true },
{ label: 'Throughput', value: '892/s', trend: '+3.7%', up: true },
{ label: 'P99 Latency', value: '487ms', trend: '+15ms', up: false },
{ label: 'CPU Usage', value: '67%', trend: '-2.4%', up: true },
{ label: 'Memory Usage', value: '4.2GB', trend: '+180MB', up: false },
{ label: 'Cache Hit Rate', value: '94.7%', trend: '+1.2%', up: true },
{ label: 'Queue Depth', value: '234', trend: '-45', up: true },
{ label: 'Open Connections', value: '12,483', trend: '+892', up: false },
{ label: 'Deployments Today', value: '7', trend: '+2', up: true },
];
var grid = document.getElementById('metrics-grid');
metrics.forEach(function(m) {
var sparkBars = '';
for (var s = 0; s < 12; s++) {
var h = 20 + Math.floor(Math.random() * 80);
sparkBars += '<div class="bar" style="height:' + h + '%"></div>';
}
grid.innerHTML += '<div class="card"><div class="card-label">' + m.label +
'</div><div class="card-value">' + m.value +
'</div><div class="card-trend ' + (m.up ? 'up' : 'down') + '">' +
(m.up ? '+' : '') + m.trend +
'</div><div class="card-sparkline">' + sparkBars + '</div></div>';
});
// Chart bars
var chart = document.getElementById('chart');
for (var h = 0; h < 24; h++) {
var height = 15 + ((h * 17 + 7) % 85);
var bar = document.createElement('div');
bar.className = 'bar';
bar.style.height = height + '%';
bar.innerHTML = '<span class="bar-label">' + String(h).padStart(2, '0') + ':00</span>';
chart.appendChild(bar);
}
// Endpoints table
var endpoints = document.getElementById('endpoints-table');
var paths = ['/api/users', '/api/auth', '/api/products', '/api/orders', '/api/search',
'/api/analytics', '/api/notifications', '/api/payments', '/api/inventory', '/api/reports',
'/api/settings', '/api/uploads', '/api/webhooks', '/api/health', '/api/metrics'];
paths.forEach(function(p, i) {
endpoints.innerHTML += '<tr><td><code>' + p + '</code></td><td>' +
(50000 - i * 3000) + '</td><td>' + (45 + i * 12) + 'ms</td><td>' +
(0.1 + i * 0.08).toFixed(2) + '%</td></tr>';
});
// Alerts table
var alerts = document.getElementById('alerts-table');
var alertData = [
['High error rate on /api/payments', 'error', 'payments'],
['Memory usage above 85%', 'warn', 'api-gateway'],
['Slow queries detected', 'warn', 'database'],
['SSL certificate expiring in 7 days', 'info', 'infrastructure'],
['Disk usage above 80%', 'warn', 'storage'],
['Connection pool exhaustion', 'error', 'database'],
['Rate limit threshold reached', 'warn', 'api-gateway'],
['Deployment rollback detected', 'info', 'ci-cd'],
];
alertData.forEach(function(a, i) {
var badge = a[1] === 'error' ? 'badge-error' : a[1] === 'warn' ? 'badge-warn' : 'badge-info';
alerts.innerHTML += '<tr><td>' + a[0] + '</td><td><span class="badge ' + badge + '">' +
a[1].toUpperCase() + '</span></td><td>' + a[2] + '</td><td>' + (i * 15 + 5) + 'm ago</td></tr>';
});
// Logs table
var logs = document.getElementById('logs-table');
var services = ['auth', 'api-gateway', 'worker', 'cache', 'database', 'payments', 'search', 'notifications'];
var levels = ['INFO', 'WARN', 'ERROR', 'DEBUG'];
var messages = [
'Request processed successfully',
'Connection pool running low',
'Failed to connect to upstream service',
'Cache miss for key session:',
'Query execution exceeded threshold',
'Payment webhook received',
'Search index rebuild started',
'Rate limit applied to client',
'Health check passed',
'Background job completed',
];
for (var i = 0; i < 200; i++) {
var level = levels[i % 4];
var badge = level === 'ERROR' ? 'badge-error' : level === 'WARN' ? 'badge-warn' :
level === 'DEBUG' ? 'badge-debug' : 'badge-info';
var ts = new Date(2025, 2, 15, 23 - Math.floor(i / 8), 59 - (i % 60));
logs.innerHTML += '<tr><td style="white-space:nowrap">' + ts.toISOString().replace('T', ' ').substring(0, 19) +
'</td><td><span class="badge ' + badge + '">' + level +
'</span></td><td>' + services[i % 8] +
'</td><td>' + messages[i % 10] + ' #' + (i + 1) +
'</td><td>' + (Math.floor(Math.random() * 500) + 10) + 'ms</td></tr>';
}
// Pagination
var pag = document.getElementById('pagination');
for (var p = 1; p <= 10; p++) {
pag.innerHTML += '<button class="' + (p === 1 ? 'active' : '') + '">' + p + '</button>';
}
// Services table
var svcTable = document.getElementById('services-table');
var svcNames = ['api-gateway', 'auth-service', 'payment-processor', 'search-engine',
'notification-hub', 'analytics-pipeline', 'cache-layer', 'worker-pool',
'storage-service', 'cdn-origin', 'queue-processor', 'ml-inference'];
svcNames.forEach(function(name, i) {
var status = i < 9 ? 'active' : i === 9 ? 'pending' : 'inactive';
var badge = status === 'active' ? 'badge-active' : status === 'pending' ? 'badge-pending' : 'badge-inactive';
svcTable.innerHTML += '<tr><td><strong>' + name + '</strong></td>' +
'<td><span class="badge ' + badge + '">' + status + '</span></td>' +
'<td>' + (99.9 - i * 0.05).toFixed(2) + '%</td>' +
'<td>' + (30 + i * 5) + '%</td>' +
'<td>' + (512 + i * 128) + 'MB</td>' +
'<td>' + (2000 - i * 150) + '</td>' +
'<td>' + (0.1 + i * 0.04).toFixed(2) + '%</td>' +
'<td>' + (i + 1) + 'h ago</td></tr>';
});
})();
</script>
</body>
</html>
+179
View File
@@ -0,0 +1,179 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>TechStore - Electronics & Gadgets</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #fff; color: #0f172a; }
.topbar { background: #0f172a; color: #94a3b8; font-size: 12px; padding: 6px 24px; display: flex; justify-content: space-between; }
.navbar { display: flex; align-items: center; gap: 24px; padding: 12px 24px; border-bottom: 1px solid #e2e8f0; position: sticky; top: 0; background: #fff; z-index: 100; }
.navbar .logo { font-size: 22px; font-weight: 800; color: #6366f1; }
.navbar .search { flex: 1; max-width: 500px; position: relative; }
.navbar .search input { width: 100%; padding: 10px 16px; border: 2px solid #e2e8f0; border-radius: 8px; font-size: 14px; }
.navbar .search input:focus { border-color: #6366f1; outline: none; }
.nav-links { display: flex; gap: 20px; }
.nav-links a { text-decoration: none; color: #475569; font-size: 14px; }
.nav-actions { display: flex; gap: 16px; align-items: center; }
.nav-actions button { background: none; border: none; font-size: 14px; cursor: pointer; color: #475569; }
.cart-badge { background: #6366f1; color: #fff; font-size: 11px; padding: 2px 6px; border-radius: 10px; margin-left: 4px; }
.categories { display: flex; gap: 0; padding: 0 24px; border-bottom: 1px solid #f1f5f9; overflow-x: auto; }
.categories a { padding: 10px 16px; font-size: 13px; color: #64748b; text-decoration: none; white-space: nowrap; border-bottom: 2px solid transparent; }
.categories a:hover, .categories a.active { color: #6366f1; border-bottom-color: #6366f1; }
.hero { background: linear-gradient(135deg, #312e81, #6366f1); color: #fff; padding: 60px 24px; text-align: center; }
.hero h2 { font-size: 2.5rem; margin-bottom: 12px; }
.hero p { font-size: 18px; opacity: 0.9; margin-bottom: 24px; }
.hero button { padding: 12px 32px; background: #fff; color: #6366f1; border: none; border-radius: 8px; font-size: 16px; font-weight: 600; cursor: pointer; }
.container { max-width: 1280px; margin: 0 auto; padding: 0 24px; }
.section-title { font-size: 1.5rem; font-weight: 700; margin: 32px 0 16px; display: flex; justify-content: space-between; align-items: center; }
.section-title a { font-size: 14px; color: #6366f1; text-decoration: none; }
.product-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; margin-bottom: 32px; }
.product { border: 1px solid #e2e8f0; border-radius: 12px; overflow: hidden; transition: box-shadow 0.2s, transform 0.2s; }
.product:hover { box-shadow: 0 4px 16px rgba(0,0,0,0.1); transform: translateY(-2px); }
.product-img { height: 200px; display: flex; align-items: center; justify-content: center; font-size: 48px; }
.product-info { padding: 16px; }
.product-brand { font-size: 12px; color: #64748b; text-transform: uppercase; letter-spacing: 0.05em; }
.product-name { font-size: 15px; font-weight: 600; margin: 4px 0 8px; line-height: 1.3; }
.product-price { font-size: 20px; font-weight: 700; color: #0f172a; }
.product-original { font-size: 14px; color: #94a3b8; text-decoration: line-through; margin-left: 8px; }
.product-rating { display: flex; align-items: center; gap: 4px; margin-top: 8px; font-size: 13px; color: #64748b; }
.stars { color: #f59e0b; }
.product-actions { display: flex; gap: 8px; margin-top: 12px; }
.product-actions button { flex: 1; padding: 8px; border: none; border-radius: 6px; font-size: 13px; cursor: pointer; }
.btn-primary { background: #6366f1; color: #fff; }
.btn-secondary { background: #f1f5f9; color: #475569; }
.filters { display: flex; gap: 12px; margin-bottom: 20px; flex-wrap: wrap; }
.filter { padding: 6px 16px; border: 1px solid #e2e8f0; border-radius: 20px; font-size: 13px; background: #fff; cursor: pointer; }
.filter.active { background: #6366f1; color: #fff; border-color: #6366f1; }
.deals-banner { background: #fef3c7; border: 1px solid #fbbf24; border-radius: 12px; padding: 20px 24px; margin: 24px 0; display: flex; justify-content: space-between; align-items: center; }
.deals-banner h3 { color: #b45309; }
.reviews { margin: 24px 0; }
.review { padding: 16px; border-bottom: 1px solid #f1f5f9; }
.review-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
.review-author { font-weight: 600; font-size: 14px; }
.review-date { font-size: 12px; color: #94a3b8; }
.review-body { font-size: 14px; color: #475569; }
.footer { background: #0f172a; color: #94a3b8; padding: 48px 24px; margin-top: 48px; }
.footer-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 32px; max-width: 1280px; margin: 0 auto; }
.footer h4 { color: #fff; margin-bottom: 16px; font-size: 14px; }
.footer a { display: block; color: #94a3b8; text-decoration: none; font-size: 13px; margin-bottom: 8px; }
.footer-bottom { border-top: 1px solid #1e293b; padding-top: 24px; margin-top: 32px; text-align: center; font-size: 13px; max-width: 1280px; margin-left: auto; margin-right: auto; }
</style>
</head>
<body>
<div class="topbar">
<span>Free shipping on orders over $99</span>
<span>Customer Service: 1-800-TECH | Track Order | Help</span>
</div>
<nav class="navbar">
<div class="logo">TechStore</div>
<div class="search"><input type="text" placeholder="Search products, brands, categories..."></div>
<div class="nav-actions">
<button>Account</button>
<button>Wishlist</button>
<button>Cart <span class="cart-badge">3</span></button>
</div>
</nav>
<div class="categories">
<a href="#" class="active">All</a><a href="#">Laptops</a><a href="#">Phones</a>
<a href="#">Tablets</a><a href="#">Audio</a><a href="#">Cameras</a>
<a href="#">Monitors</a><a href="#">Storage</a><a href="#">Networking</a>
<a href="#">Accessories</a><a href="#">Deals</a><a href="#">New Arrivals</a>
</div>
<div class="hero">
<h2>Spring Tech Sale</h2>
<p>Up to 40% off on selected electronics. Limited time offer.</p>
<button>Shop Now</button>
</div>
<div class="container">
<div class="deals-banner">
<div><h3>Flash Deals - Ends in 04:32:17</h3><p style="font-size:13px;color:#92400e">Extra 15% off with code SPRING15</p></div>
<button class="btn-primary" style="padding:10px 24px;border-radius:8px;border:none;cursor:pointer">View All Deals</button>
</div>
<div class="section-title"><span>Featured Products</span><a href="#">View All</a></div>
<div class="filters">
<span class="filter active">All</span><span class="filter">Under $100</span>
<span class="filter">$100 - $500</span><span class="filter">$500+</span>
<span class="filter">Top Rated</span><span class="filter">New</span>
</div>
<div class="product-grid" id="featured-grid"></div>
<div class="section-title"><span>Best Sellers</span><a href="#">View All</a></div>
<div class="product-grid" id="bestsellers-grid"></div>
<div class="section-title"><span>New Arrivals</span><a href="#">View All</a></div>
<div class="product-grid" id="newarrivals-grid"></div>
<div class="section-title"><span>Customer Reviews</span></div>
<div class="reviews" id="reviews"></div>
</div>
<footer class="footer">
<div class="footer-grid">
<div><h4>Shop</h4><a href="#">Laptops</a><a href="#">Phones</a><a href="#">Tablets</a><a href="#">Audio</a><a href="#">Cameras</a><a href="#">Monitors</a><a href="#">Accessories</a></div>
<div><h4>Support</h4><a href="#">Help Center</a><a href="#">Returns</a><a href="#">Warranty</a><a href="#">Contact Us</a><a href="#">Track Order</a><a href="#">Shipping Info</a></div>
<div><h4>Company</h4><a href="#">About Us</a><a href="#">Careers</a><a href="#">Press</a><a href="#">Blog</a><a href="#">Sustainability</a><a href="#">Investor Relations</a></div>
<div><h4>Connect</h4><a href="#">Newsletter</a><a href="#">Social Media</a><a href="#">Affiliate Program</a><a href="#">Partner With Us</a><a href="#">Developer API</a></div>
</div>
<div class="footer-bottom">2025 TechStore Inc. All rights reserved. | Privacy Policy | Terms of Service | Cookie Settings</div>
</footer>
<script>
(function() {
var brands = ['Apple', 'Samsung', 'Sony', 'Bose', 'Dell', 'Lenovo', 'LG', 'ASUS', 'Logitech', 'Canon', 'Nikon', 'JBL'];
var categories = ['Laptop', 'Phone', 'Tablet', 'Headphones', 'Camera', 'Monitor', 'Speaker', 'Keyboard'];
var colors = ['#dbeafe', '#fce7f3', '#d1fae5', '#fef3c7', '#e0e7ff', '#f1f5f9', '#fef2f2', '#f0fdf4'];
function makeProduct(i) {
var brand = brands[i % brands.length];
var cat = categories[i % categories.length];
var price = 49 + (i * 73) % 1500;
var original = Math.round(price * 1.25);
var rating = (3.5 + (i % 15) * 0.1).toFixed(1);
var reviews = 50 + (i * 37) % 2000;
var stars = '';
for (var s = 0; s < 5; s++) stars += s < Math.round(parseFloat(rating)) ? '*' : ' ';
return '<div class="product"><div class="product-img" style="background:' + colors[i % 8] + '">' +
cat.charAt(0).toUpperCase() + '</div><div class="product-info">' +
'<div class="product-brand">' + brand + '</div>' +
'<div class="product-name">' + brand + ' ' + cat + ' Pro ' + (2024 + (i % 3)) + ' Edition</div>' +
'<div><span class="product-price">$' + price + '</span><span class="product-original">$' + original + '</span></div>' +
'<div class="product-rating"><span class="stars">' + stars + '</span> ' + rating + ' (' + reviews + ')</div>' +
'<div class="product-actions"><button class="btn-primary">Add to Cart</button><button class="btn-secondary">Compare</button></div>' +
'</div></div>';
}
var featured = document.getElementById('featured-grid');
for (var i = 0; i < 16; i++) featured.innerHTML += makeProduct(i);
var bestsellers = document.getElementById('bestsellers-grid');
for (var i = 16; i < 32; i++) bestsellers.innerHTML += makeProduct(i);
var newarrivals = document.getElementById('newarrivals-grid');
for (var i = 32; i < 48; i++) newarrivals.innerHTML += makeProduct(i);
var reviewsEl = document.getElementById('reviews');
var names = ['Alice M.', 'Bob K.', 'Carol S.', 'David L.', 'Eva R.', 'Frank W.', 'Grace H.', 'Henry P.'];
var reviewTexts = [
'Excellent product, arrived faster than expected. Build quality is outstanding.',
'Good value for money. Would recommend to anyone looking for a reliable device.',
'Decent product but the battery life could be better. Works well otherwise.',
'Amazing quality! This is my third purchase from this brand and they never disappoint.',
];
for (var i = 0; i < 20; i++) {
reviewsEl.innerHTML += '<div class="review"><div class="review-header"><div><span class="review-author">' +
names[i % 8] + '</span></div><span class="review-date">March ' + (15 - i % 15) + ', 2025</span></div>' +
'<div class="review-body">' + reviewTexts[i % 4] + '</div></div>';
}
})();
</script>
</body>
</html>
+516 -51
View File
@@ -1,14 +1,128 @@
import { spawn, ChildProcess } from "child_process";
import * as http from "http";
import * as net from "net";
import * as os from "os";
import * as path from "path";
import * as fs from "fs";
import { fileURLToPath } from "url";
import { scenarios, type BenchmarkCommand, type Scenario } from "./scenarios.js";
import { engineScenarios } from "./engine-scenarios.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// ---------------------------------------------------------------------------
// Static file server for HTTP-served benchmarks
// ---------------------------------------------------------------------------
const PAGES_DIR = path.join(__dirname, "pages");
const MIME_TYPES: Record<string, string> = {
".html": "text/html",
".css": "text/css",
".js": "application/javascript",
".json": "application/json",
".png": "image/png",
".jpg": "image/jpeg",
".svg": "image/svg+xml",
};
function startFileServer(): Promise<{ server: http.Server; port: number }> {
return new Promise((resolve, reject) => {
const server = http.createServer((req, res) => {
const url = new URL(req.url || "/", `http://localhost`);
let filePath = path.join(PAGES_DIR, url.pathname === "/" ? "article.html" : url.pathname);
if (!filePath.startsWith(PAGES_DIR)) {
res.writeHead(403);
res.end();
return;
}
if (fs.existsSync(filePath) && fs.statSync(filePath).isDirectory()) {
filePath = path.join(filePath, "index.html");
}
try {
const content = fs.readFileSync(filePath);
const ext = path.extname(filePath);
res.writeHead(200, { "Content-Type": MIME_TYPES[ext] || "application/octet-stream" });
res.end(content);
} catch {
res.writeHead(404);
res.end("Not found");
}
});
server.listen(0, "127.0.0.1", () => {
const addr = server.address();
if (!addr || typeof addr === "string") {
reject(new Error("Failed to get server address"));
return;
}
resolve({ server, port: addr.port });
});
server.on("error", reject);
});
}
function stopFileServer(server: http.Server): Promise<void> {
return new Promise((resolve) => {
server.close(() => resolve());
});
}
// ---------------------------------------------------------------------------
// Memory measurement via /proc or ps
// ---------------------------------------------------------------------------
function getProcessMemoryKB(pid: number): number | null {
if (process.platform === "linux") {
try {
const status = fs.readFileSync(`/proc/${pid}/status`, "utf-8");
const match = status.match(/VmRSS:\s+(\d+)\s+kB/);
if (match) return parseInt(match[1], 10);
} catch { /* */ }
}
try {
const { execSync } = require("child_process");
const output = execSync(`ps -o rss= -p ${pid}`, { encoding: "utf-8", timeout: 2000 });
const kb = parseInt(output.trim(), 10);
if (!isNaN(kb)) return kb;
} catch { /* */ }
return null;
}
function sampleMemory(pids: number[], intervalMs: number): { stop: () => number } {
let peakKB = 0;
const timer = setInterval(() => {
for (const pid of pids) {
const kb = getProcessMemoryKB(pid);
if (kb && kb > peakKB) peakKB = kb;
}
}, intervalMs);
return {
stop() {
clearInterval(timer);
for (const pid of pids) {
const kb = getProcessMemoryKB(pid);
if (kb && kb > peakKB) peakKB = kb;
}
return peakKB;
},
};
}
function formatMemory(kb: number): string {
if (kb >= 1024 * 1024) return `${(kb / 1024 / 1024).toFixed(1)}GB`;
if (kb >= 1024) return `${(kb / 1024).toFixed(1)}MB`;
return `${kb}KB`;
}
// ---------------------------------------------------------------------------
// Socket / daemon helpers
// ---------------------------------------------------------------------------
@@ -162,23 +276,29 @@ function spawnNodeDaemon(session: string): DaemonHandle {
return { session, process: child };
}
function spawnNativeDaemon(session: string): DaemonHandle {
function spawnNativeDaemon(session: string, engine?: string): DaemonHandle {
const binaryPath = getNativeBinaryPath();
const env: Record<string, string> = {
...process.env as Record<string, string>,
AGENT_BROWSER_DAEMON: "1",
AGENT_BROWSER_SESSION: session,
};
if (engine) {
env.AGENT_BROWSER_ENGINE = engine;
}
const child = spawn(binaryPath, [], {
env: {
...process.env,
AGENT_BROWSER_DAEMON: "1",
AGENT_BROWSER_SESSION: session,
},
env,
stdio: ["ignore", "ignore", "pipe"],
detached: true,
});
const label = engine ? `native-${engine}` : "native-daemon";
child.stderr?.on("data", (chunk) => {
const msg = chunk.toString().trim();
if (msg && process.env.BENCH_DEBUG) {
process.stderr.write(`[native-daemon] ${msg}\n`);
process.stderr.write(`[${label}] ${msg}\n`);
}
});
@@ -200,7 +320,12 @@ async function closeDaemon(handle: DaemonHandle): Promise<void> {
}
function cleanupSockets(): void {
for (const session of ["bench-node", "bench-native"]) {
for (const session of [
"bench-node",
"bench-native",
"bench-chrome",
"bench-lightpanda",
]) {
const sockPath = getSocketPath(session);
const pidPath = sockPath.replace(/\.sock$/, ".pid");
try {
@@ -272,6 +397,8 @@ interface ScenarioResult {
name: string;
nodeStats: Stats | null;
nativeStats: Stats | null;
chromeStats: Stats | null;
lightpandaStats: Stats | null;
}
async function runScenario(
@@ -284,6 +411,8 @@ async function runScenario(
name: scenario.name,
nodeStats: null,
nativeStats: null,
chromeStats: null,
lightpandaStats: null,
};
for (const [label, session] of Object.entries(sessions)) {
@@ -308,7 +437,60 @@ async function runScenario(
const stats = computeStats(timings);
if (label === "node") result.nodeStats = stats;
else result.nativeStats = stats;
else if (label === "native") result.nativeStats = stats;
else if (label === "chrome") result.chromeStats = stats;
else if (label === "lightpanda") result.lightpandaStats = stats;
}
return result;
}
async function runScenarioWithErrorTolerance(
scenario: Scenario,
sessions: Record<string, string>,
iterations: number,
warmup: number,
): Promise<ScenarioResult> {
const result: ScenarioResult = {
name: scenario.name,
nodeStats: null,
nativeStats: null,
chromeStats: null,
lightpandaStats: null,
};
for (const [label, session] of Object.entries(sessions)) {
if (!session) continue;
try {
if (scenario.setup) {
await runCommands(session, scenario.setup);
}
for (let i = 0; i < warmup; i++) {
await timeCommands(session, scenario.commands);
}
const timings: number[] = [];
for (let i = 0; i < iterations; i++) {
timings.push(await timeCommands(session, scenario.commands));
}
if (scenario.teardown) {
await runCommands(session, scenario.teardown);
}
const stats = computeStats(timings);
if (label === "chrome") result.chromeStats = stats;
else if (label === "lightpanda") result.lightpandaStats = stats;
else if (label === "node") result.nodeStats = stats;
else if (label === "native") result.nativeStats = stats;
} catch (err) {
if (process.env.BENCH_DEBUG) {
const msg = err instanceof Error ? err.message : String(err);
process.stderr.write(` [${label}] scenario '${scenario.name}' failed: ${msg}\n`);
}
}
}
return result;
@@ -326,17 +508,30 @@ function rpad(s: string, len: number): string {
return s.padStart(len);
}
function formatSpeedup(nodeUs: number, nativeUs: number): string {
if (nativeUs === 0 && nodeUs === 0) return " --";
if (nativeUs === 0) return " >>>";
const ratio = nodeUs / nativeUs;
function formatSpeedup(baselineUs: number, candidateUs: number): string {
if (candidateUs === 0 && baselineUs === 0) return " --";
if (candidateUs === 0) return " >>>";
const ratio = baselineUs / candidateUs;
return `${ratio.toFixed(1)}x`;
}
function printResults(results: ScenarioResult[], iterations: number, warmup: number): void {
type BenchmarkMode = "daemon" | "engine";
function printResults(
results: ScenarioResult[],
iterations: number,
warmup: number,
mode: BenchmarkMode = "daemon",
): void {
console.log("");
if (mode === "engine") {
printEngineResults(results, iterations, warmup);
return;
}
const bothPaths = results[0].nodeStats !== null && results[0].nativeStats !== null;
console.log("");
const header = bothPaths
? `agent-browser benchmark: node vs native (${iterations} iterations, ${warmup} warmup)`
: `agent-browser benchmark (${iterations} iterations, ${warmup} warmup)`;
@@ -423,33 +618,100 @@ function printResults(results: ScenarioResult[], iterations: number, warmup: num
console.log("");
}
function writeJsonResults(results: ScenarioResult[], outputPath: string): void {
function printEngineResults(
results: ScenarioResult[],
iterations: number,
warmup: number,
): void {
const header = `agent-browser benchmark: chrome vs lightpanda (${iterations} iterations, ${warmup} warmup)`;
console.log(header);
console.log("=".repeat(header.length));
console.log("");
const nameW = 22;
const colW = 18;
console.log(
pad("Scenario", nameW) +
rpad("Chrome (avg)", colW) +
rpad("Lightpanda (avg)", colW) +
rpad("Speedup", 10),
);
console.log("-".repeat(nameW + colW * 2 + 10));
let totalChromeUs = 0;
let totalLightpandaUs = 0;
let comparableCount = 0;
for (const r of results) {
const chromeAvg = r.chromeStats ? formatDuration(r.chromeStats.avgUs) : "N/A";
const lpAvg = r.lightpandaStats ? formatDuration(r.lightpandaStats.avgUs) : "N/A";
let speedup = " --";
if (r.chromeStats && r.lightpandaStats) {
totalChromeUs += r.chromeStats.avgUs;
totalLightpandaUs += r.lightpandaStats.avgUs;
comparableCount++;
speedup = formatSpeedup(r.chromeStats.avgUs, r.lightpandaStats.avgUs);
}
console.log(
pad(r.name, nameW) +
rpad(chromeAvg, colW) +
rpad(lpAvg, colW) +
rpad(speedup, 10),
);
}
console.log("-".repeat(nameW + colW * 2 + 10));
if (comparableCount > 0 && totalLightpandaUs > 0) {
const ratio = totalChromeUs / totalLightpandaUs;
const winner = ratio >= 1.0
? `lightpanda ${ratio.toFixed(1)}x faster`
: `chrome ${(1 / ratio).toFixed(1)}x faster`;
console.log(`Overall: ${winner}`);
}
console.log("");
}
function writeJsonResults(
results: ScenarioResult[],
outputPath: string,
mode: BenchmarkMode = "daemon",
): void {
const toMs = (us: number) => +(us / 1000).toFixed(2);
const json = results.map((r) => ({
scenario: r.name,
node: r.nodeStats
? {
avg_ms: toMs(r.nodeStats.avgUs),
min_ms: toMs(r.nodeStats.minUs),
max_ms: toMs(r.nodeStats.maxUs),
p50_ms: toMs(r.nodeStats.p50Us),
p95_ms: toMs(r.nodeStats.p95Us),
}
: null,
native: r.nativeStats
? {
avg_ms: toMs(r.nativeStats.avgUs),
min_ms: toMs(r.nativeStats.minUs),
max_ms: toMs(r.nativeStats.maxUs),
p50_ms: toMs(r.nativeStats.p50Us),
p95_ms: toMs(r.nativeStats.p95Us),
}
: null,
speedup:
r.nodeStats && r.nativeStats && r.nativeStats.avgUs > 0
? +(r.nodeStats.avgUs / r.nativeStats.avgUs).toFixed(2)
: null,
}));
const statsToJson = (s: Stats) => ({
avg_ms: toMs(s.avgUs),
min_ms: toMs(s.minUs),
max_ms: toMs(s.maxUs),
p50_ms: toMs(s.p50Us),
p95_ms: toMs(s.p95Us),
});
const json = results.map((r) => {
if (mode === "engine") {
return {
scenario: r.name,
chrome: r.chromeStats ? statsToJson(r.chromeStats) : null,
lightpanda: r.lightpandaStats ? statsToJson(r.lightpandaStats) : null,
speedup:
r.chromeStats && r.lightpandaStats && r.lightpandaStats.avgUs > 0
? +(r.chromeStats.avgUs / r.lightpandaStats.avgUs).toFixed(2)
: null,
};
}
return {
scenario: r.name,
node: r.nodeStats ? statsToJson(r.nodeStats) : null,
native: r.nativeStats ? statsToJson(r.nativeStats) : null,
speedup:
r.nodeStats && r.nativeStats && r.nativeStats.avgUs > 0
? +(r.nodeStats.avgUs / r.nativeStats.avgUs).toFixed(2)
: null,
};
});
fs.writeFileSync(outputPath, JSON.stringify(json, null, 2) + "\n");
console.log(`JSON results written to ${outputPath}`);
}
@@ -463,6 +725,7 @@ interface CliArgs {
warmup: number;
nodeOnly: boolean;
nativeOnly: boolean;
engineMode: boolean;
json: boolean;
}
@@ -473,6 +736,7 @@ function parseArgs(): CliArgs {
warmup: 3,
nodeOnly: false,
nativeOnly: false,
engineMode: false,
json: false,
};
@@ -490,6 +754,9 @@ function parseArgs(): CliArgs {
case "--native-only":
result.nativeOnly = true;
break;
case "--engine":
result.engineMode = true;
break;
case "--json":
result.json = true;
break;
@@ -506,13 +773,10 @@ function parseArgs(): CliArgs {
// Main
// ---------------------------------------------------------------------------
async function main(): Promise<void> {
const args = parseArgs();
async function runDaemonBenchmark(args: CliArgs): Promise<void> {
const runNode = !args.nativeOnly;
const runNative = !args.nodeOnly;
cleanupSockets();
console.log("Starting benchmark daemons...");
let nodeHandle: DaemonHandle | undefined;
@@ -535,7 +799,6 @@ async function main(): Promise<void> {
if (runNode) sessions.node = "bench-node";
if (runNative) sessions.native = "bench-native";
// Launch browsers on both daemons
for (const session of Object.values(sessions)) {
const resp = await sendCommand(session, {
id: "launch",
@@ -549,7 +812,6 @@ async function main(): Promise<void> {
console.log(" Browsers launched");
console.log("");
// Run all scenarios
const results: ScenarioResult[] = [];
for (const scenario of scenarios) {
process.stdout.write(` Running: ${scenario.name}...`);
@@ -567,20 +829,22 @@ async function main(): Promise<void> {
}
}
printResults(results, args.iterations, args.warmup);
printResults(results, args.iterations, args.warmup, "daemon");
if (args.json) {
writeJsonResults(results, path.join(getProjectRoot(), "test/benchmarks/results.json"));
writeJsonResults(
results,
path.join(getProjectRoot(), "test/benchmarks/results.json"),
"daemon",
);
}
// Close browsers
for (const session of Object.values(sessions)) {
await sendCommand(session, { id: "close", action: "close" }).catch(() => {});
}
await sleep(300);
// CI gate: exit 1 if native is slower overall (total avg across all scenarios)
if (runNode && runNative) {
let totalNodeUs = 0;
let totalNativeUs = 0;
@@ -597,6 +861,207 @@ async function main(): Promise<void> {
} finally {
if (nodeHandle) await closeDaemon(nodeHandle);
if (nativeHandle) await closeDaemon(nativeHandle);
}
}
function buildHttpScenarios(baseUrl: string): Scenario[] {
const pages = ["article.html", "dashboard.html", "ecommerce.html"];
const httpScenarios: Scenario[] = [];
for (const page of pages) {
const label = page.replace(".html", "");
httpScenarios.push({
name: `http-${label}`,
description: `Navigate to ${label} page over HTTP (full fetch + parse + layout)`,
commands: [
{ id: "nav", action: "navigate", url: `${baseUrl}/${page}`, waitUntil: "load" },
],
});
}
httpScenarios.push({
name: "http-nav+snap",
description: "Navigate to article over HTTP then snapshot",
commands: [
{ id: "nav", action: "navigate", url: `${baseUrl}/article.html`, waitUntil: "load" },
{ id: "snap", action: "snapshot" },
],
});
// Multi-page throughput: cycle through all pages N times
const multiPageCmds: BenchmarkCommand[] = [];
for (let round = 0; round < 5; round++) {
for (const page of pages) {
multiPageCmds.push({
id: `nav-${round}-${page}`,
action: "navigate",
url: `${baseUrl}/${page}`,
waitUntil: "load",
});
}
}
httpScenarios.push({
name: "http-multi-15pg",
description: "Navigate 15 pages in sequence (5 rounds x 3 pages)",
commands: multiPageCmds,
});
// Bulk navigation: 50 page loads of the article (closest to Lightpanda's 100-page benchmark)
const bulkCmds: BenchmarkCommand[] = [];
for (let i = 0; i < 50; i++) {
bulkCmds.push({
id: `bulk-${i}`,
action: "navigate",
url: `${baseUrl}/${pages[i % pages.length]}`,
waitUntil: "load",
});
}
httpScenarios.push({
name: "http-bulk-50pg",
description: "Navigate 50 pages sequentially (throughput test)",
commands: bulkCmds,
});
return httpScenarios;
}
async function runEngineBenchmark(args: CliArgs): Promise<void> {
console.log("Starting local file server...");
const { server, port } = await startFileServer();
const baseUrl = `http://127.0.0.1:${port}`;
console.log(` Serving pages at ${baseUrl}`);
console.log("Starting engine benchmark daemons...");
let chromeHandle: DaemonHandle | undefined;
let lightpandaHandle: DaemonHandle | undefined;
try {
chromeHandle = spawnNativeDaemon("bench-chrome", "chrome");
await waitForSocket("bench-chrome");
console.log(" Chrome daemon ready");
lightpandaHandle = spawnNativeDaemon("bench-lightpanda", "lightpanda");
await waitForSocket("bench-lightpanda");
console.log(" Lightpanda daemon ready");
const sessions: Record<string, string> = {
chrome: "bench-chrome",
lightpanda: "bench-lightpanda",
};
for (const [label, session] of Object.entries(sessions)) {
const resp = await sendCommand(session, {
id: "launch",
action: "launch",
headless: true,
});
if (!(resp as { success?: boolean }).success) {
throw new Error(
`Failed to launch ${label} browser on ${session}: ${JSON.stringify(resp)}`,
);
}
}
console.log(" Browsers launched");
// Collect PIDs for memory sampling
const chromePid = chromeHandle.process.pid;
const lpPid = lightpandaHandle.process.pid;
const pidsToSample: number[] = [];
if (chromePid) pidsToSample.push(chromePid);
if (lpPid) pidsToSample.push(lpPid);
const memSampler = pidsToSample.length > 0
? sampleMemory(pidsToSample, 500)
: null;
// Measure per-engine peak memory during the heavy scenarios
const chromeMemPids = chromePid ? [chromePid] : [];
const lpMemPids = lpPid ? [lpPid] : [];
console.log("");
const httpScenarios = buildHttpScenarios(baseUrl);
const allScenarios = [...scenarios, ...engineScenarios, ...httpScenarios];
const results: ScenarioResult[] = [];
for (const scenario of allScenarios) {
process.stdout.write(` Running: ${scenario.name}...`);
const result = await runScenarioWithErrorTolerance(
scenario,
sessions,
args.iterations,
args.warmup,
);
results.push(result);
const chromeAvg = result.chromeStats
? formatDuration(result.chromeStats.avgUs)
: "N/A";
const lpAvg = result.lightpandaStats
? formatDuration(result.lightpandaStats.avgUs)
: "N/A";
if (result.chromeStats && result.lightpandaStats) {
const speedup = formatSpeedup(
result.chromeStats.avgUs,
result.lightpandaStats.avgUs,
);
process.stdout.write(` chrome=${chromeAvg} lightpanda=${lpAvg} (${speedup})\n`);
} else {
process.stdout.write(` chrome=${chromeAvg} lightpanda=${lpAvg}\n`);
}
}
// Final memory snapshot
const chromeMemKB = chromeMemPids.length > 0 ? getProcessMemoryKB(chromeMemPids[0]) : null;
const lpMemKB = lpMemPids.length > 0 ? getProcessMemoryKB(lpMemPids[0]) : null;
if (memSampler) memSampler.stop();
printResults(results, args.iterations, args.warmup, "engine");
if (chromeMemKB || lpMemKB) {
console.log("Memory (daemon RSS after benchmarks):");
if (chromeMemKB) console.log(` Chrome daemon: ${formatMemory(chromeMemKB)}`);
if (lpMemKB) console.log(` Lightpanda daemon: ${formatMemory(lpMemKB)}`);
if (chromeMemKB && lpMemKB && lpMemKB > 0) {
const memRatio = chromeMemKB / lpMemKB;
console.log(` Ratio: chrome uses ${memRatio.toFixed(1)}x more memory`);
}
console.log("");
}
if (args.json) {
writeJsonResults(
results,
path.join(getProjectRoot(), "test/benchmarks/results-engine.json"),
"engine",
);
}
for (const session of Object.values(sessions)) {
await sendCommand(session, { id: "close", action: "close" }).catch(() => {});
}
await sleep(300);
} finally {
if (chromeHandle) await closeDaemon(chromeHandle);
if (lightpandaHandle) await closeDaemon(lightpandaHandle);
await stopFileServer(server);
}
}
async function main(): Promise<void> {
const args = parseArgs();
cleanupSockets();
try {
if (args.engineMode) {
await runEngineBenchmark(args);
} else {
await runDaemonBenchmark(args);
}
} finally {
cleanupSockets();
}
}