mirror of
https://github.com/DeNNiiInc/Web-Page-Performance-Test.git
synced 2026-04-17 20:05:58 +00:00
Phase B.1 - Add Connection View with socket grouping
This commit is contained in:
@@ -25,6 +25,10 @@ function parseHAR(lighthouseResult) {
|
|||||||
protocol: request.protocol,
|
protocol: request.protocol,
|
||||||
priority: request.priority,
|
priority: request.priority,
|
||||||
|
|
||||||
|
// Connection/Socket information for connection view
|
||||||
|
socket: request.connectionId || request.socket || null,
|
||||||
|
connectionReused: request.connectionReused || false,
|
||||||
|
|
||||||
// Timing breakdown (in milliseconds)
|
// Timing breakdown (in milliseconds)
|
||||||
timing: {
|
timing: {
|
||||||
dns: timing.dns,
|
dns: timing.dns,
|
||||||
|
|||||||
@@ -298,13 +298,61 @@
|
|||||||
.details-table th.sorted .sort-icon {
|
.details-table th.sorted .sort-icon {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Connection View Styles */
|
||||||
|
.connection-group {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
border: 2px solid var(--color-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-header {
|
||||||
|
background: var(--color-bg-secondary);
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
border-bottom: 2px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-badge {
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
background: var(--color-bg-tertiary);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-count {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-requests {
|
||||||
|
background: var(--color-bg-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-row {
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="waterfall-container">
|
<div class="waterfall-container">
|
||||||
<h1>Request Waterfall</h1>
|
<h1>Request Waterfall</h1>
|
||||||
|
|
||||||
<div style="display: flex; gap: 2rem; margin-bottom: 1.5rem; align-items: center;">
|
<div style="display: flex; gap: 2rem; margin-bottom: 1.5rem; align-items: center; flex-wrap: wrap;">
|
||||||
<div class="filter-controls" id="filterControls">
|
<div class="filter-controls" id="filterControls">
|
||||||
<button class="filter-btn active" data-type="all">All</button>
|
<button class="filter-btn active" data-type="all">All</button>
|
||||||
<button class="filter-btn" data-type="Document">HTML</button>
|
<button class="filter-btn" data-type="Document">HTML</button>
|
||||||
@@ -314,6 +362,14 @@
|
|||||||
<button class="filter-btn" data-type="Font">Fonts</button>
|
<button class="filter-btn" data-type="Font">Fonts</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
||||||
|
<label for="viewMode" style="color: var(--color-text-secondary); font-size: 0.9rem;">View:</label>
|
||||||
|
<select id="viewMode" style="padding: 0.5rem; border-radius: 6px; background: var(--color-bg-secondary); color: var(--color-text-primary); border: 2px solid var(--color-border); cursor: pointer;">
|
||||||
|
<option value="waterfall">Waterfall</option>
|
||||||
|
<option value="connection">Connection</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
||||||
<label for="sortSelect" style="color: var(--color-text-secondary); font-size: 0.9rem;">Sort:</label>
|
<label for="sortSelect" style="color: var(--color-text-secondary); font-size: 0.9rem;">Sort:</label>
|
||||||
<select id="sortSelect" style="padding: 0.5rem; border-radius: 6px; background: var(--color-bg-secondary); color: var(--color-text-primary); border: 2px solid var(--color-border); cursor: pointer;">
|
<select id="sortSelect" style="padding: 0.5rem; border-radius: 6px; background: var(--color-bg-secondary); color: var(--color-text-primary); border: 2px solid var(--color-border); cursor: pointer;">
|
||||||
|
|||||||
122
waterfall.js
122
waterfall.js
@@ -6,6 +6,7 @@
|
|||||||
let currentHarData = null;
|
let currentHarData = null;
|
||||||
let currentFilter = 'all';
|
let currentFilter = 'all';
|
||||||
let currentSort = 'time-desc'; // Default: slowest to fastest
|
let currentSort = 'time-desc'; // Default: slowest to fastest
|
||||||
|
let currentViewMode = 'waterfall'; // waterfall or connection
|
||||||
|
|
||||||
// Load HAR data from URL parameter
|
// Load HAR data from URL parameter
|
||||||
async function init() {
|
async function init() {
|
||||||
@@ -40,6 +41,12 @@ function renderWaterfall() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (currentViewMode === 'connection') {
|
||||||
|
renderConnectionView(entries);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Original waterfall view
|
||||||
// Calculate timeline scale
|
// Calculate timeline scale
|
||||||
const maxTime = Math.max(...entries.map(e => e.timing.endTime));
|
const maxTime = Math.max(...entries.map(e => e.timing.endTime));
|
||||||
const scale = 1000 / maxTime; // pixels per second
|
const scale = 1000 / maxTime; // pixels per second
|
||||||
@@ -98,6 +105,112 @@ function renderWaterfall() {
|
|||||||
renderDetailsTable(filteredEntries);
|
renderDetailsTable(filteredEntries);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderConnectionView(entries) {
|
||||||
|
const canvas = document.getElementById('waterfallCanvas');
|
||||||
|
|
||||||
|
// Group requests by connection/socket
|
||||||
|
const connections = groupByConnection(entries);
|
||||||
|
|
||||||
|
if (connections.length === 0) {
|
||||||
|
canvas.innerHTML = '<p>No connection information available</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate timeline scale
|
||||||
|
const maxTime = Math.max(...entries.map(e => e.timing.endTime));
|
||||||
|
const scale = 1000 / maxTime;
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
|
||||||
|
// Add time scale header
|
||||||
|
html += '<div class="waterfall-timescale">';
|
||||||
|
for (let sec = 0; sec <= Math.ceil(maxTime); sec++) {
|
||||||
|
const pos = sec * scale;
|
||||||
|
html += `<div class="time-marker" style="left: ${300 + pos}px">${sec}s</div>`;
|
||||||
|
html += `<div class="grid-line" style="left: ${300 + pos}px"></div>`;
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
// Render each connection as a group
|
||||||
|
connections.forEach((conn, connIndex) => {
|
||||||
|
const domain = conn.domain || 'Unknown';
|
||||||
|
const connLabel = `Connection ${connIndex + 1}: ${domain}`;
|
||||||
|
const reqCount = conn.requests.length;
|
||||||
|
const reused = conn.reused ? '♻️ Reused' : '🆕 New';
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<div class="connection-group">
|
||||||
|
<div class="connection-header">
|
||||||
|
<span class="connection-label">${connLabel}</span>
|
||||||
|
<span class="connection-badge">${reused}</span>
|
||||||
|
<span class="connection-count">${reqCount} requests</span>
|
||||||
|
</div>
|
||||||
|
<div class="connection-requests">
|
||||||
|
`;
|
||||||
|
|
||||||
|
conn.requests.forEach(entry => {
|
||||||
|
const label = truncateUrl(entry.url, 25);
|
||||||
|
const timingBars = renderTimingBars(entry.timing, scale);
|
||||||
|
const statusColor = getStatusColor(entry.status);
|
||||||
|
const typeBadge = getResourceTypeBadge(entry.resourceType);
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<div class="waterfall-row connection-row" data-request-id="${entry.requestId}">
|
||||||
|
<div class="request-number">${entry.requestId}</div>
|
||||||
|
<div class="status-badge" style="background: ${statusColor}">${entry.status}</div>
|
||||||
|
<div class="type-badge">${typeBadge}</div>
|
||||||
|
<div class="request-label" title="${entry.url}">${label}</div>
|
||||||
|
<div class="timeline">${timingBars}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
html += `
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
canvas.innerHTML = html;
|
||||||
|
|
||||||
|
// Attach click handlers
|
||||||
|
document.querySelectorAll('.waterfall-row').forEach(row => {
|
||||||
|
row.addEventListener('click', () => {
|
||||||
|
const requestId = parseInt(row.dataset.requestId);
|
||||||
|
showRequestDetails(requestId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Render details table with all requests
|
||||||
|
const allRequests = connections.flatMap(c => c.requests);
|
||||||
|
renderDetailsTable(allRequests);
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupByConnection(entries) {
|
||||||
|
const connectionMap = new Map();
|
||||||
|
|
||||||
|
entries.forEach(entry => {
|
||||||
|
// Use socket ID or create connection based on domain + protocol
|
||||||
|
const connKey = entry.socket || `${entry.domain}-${entry.protocol || 'http'}`;
|
||||||
|
|
||||||
|
if (!connectionMap.has(connKey)) {
|
||||||
|
connectionMap.set(connKey, {
|
||||||
|
id: connKey,
|
||||||
|
domain: entry.domain,
|
||||||
|
protocol: entry.protocol,
|
||||||
|
reused: entry.connectionReused,
|
||||||
|
requests: []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
connectionMap.get(connKey).requests.push(entry);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Convert to array and sort by first request time
|
||||||
|
return Array.from(connectionMap.values())
|
||||||
|
.sort((a, b) => a.requests[0].timing.startTime - b.requests[0].timing.startTime);
|
||||||
|
}
|
||||||
|
|
||||||
function renderDetailsTable(entries) {
|
function renderDetailsTable(entries) {
|
||||||
const container = document.getElementById('requestDetailsTable');
|
const container = document.getElementById('requestDetailsTable');
|
||||||
|
|
||||||
@@ -451,6 +564,15 @@ function setupEventListeners() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// View mode selector
|
||||||
|
document.getElementById('viewMode').addEventListener('change', (e) => {
|
||||||
|
currentViewMode = e.target.value;
|
||||||
|
// Update heading
|
||||||
|
document.querySelector('.waterfall-container h1').textContent =
|
||||||
|
currentViewMode === 'connection' ? 'Connection View' : 'Request Waterfall';
|
||||||
|
renderWaterfall();
|
||||||
|
});
|
||||||
|
|
||||||
// Sort dropdown
|
// Sort dropdown
|
||||||
document.getElementById('sortSelect').addEventListener('change', (e) => {
|
document.getElementById('sortSelect').addEventListener('change', (e) => {
|
||||||
currentSort = e.target.value;
|
currentSort = e.target.value;
|
||||||
|
|||||||
Reference in New Issue
Block a user