From b487f9108789b9495ea497938fe718a0003f132a Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Fri, 3 Jul 2026 11:21:28 +0900 Subject: [PATCH] wip --- .github/scripts/chrome.mts | 74 ++++++++++++++++++- .github/scripts/frontend-browser-report.mts | 6 ++ .../measure-frontend-browser-comparison.mts | 3 +- .github/scripts/utility.mts | 2 +- 4 files changed, 82 insertions(+), 3 deletions(-) diff --git a/.github/scripts/chrome.mts b/.github/scripts/chrome.mts index ec0c625924..ac5466ff3e 100644 --- a/.github/scripts/chrome.mts +++ b/.github/scripts/chrome.mts @@ -42,8 +42,23 @@ export type NetworkRequest = { errorText?: string; }; +export type WebSocketConnection = { + requestId: string; + url: string; + createdAt: number; + handshakeRequestHeaders?: Record; + handshakeResponseStatus?: number; + handshakeResponseStatusText?: string; + handshakeResponseHeaders?: Record; + closedAt?: number; + sentFrameCount: number; + receivedFrameCount: number; + errorCount: number; +}; + export type NetworkSummary = { requestCount: number; + webSocketConnectionCount: number; finishedRequestCount: number; failedRequestCount: number; cachedRequestCount: number; @@ -332,6 +347,7 @@ export class Chrome { private handle: ChromeHandle; public cdp: CdpClient; public networkRequests: NetworkRequest[] = []; + public webSocketConnections: WebSocketConnection[] = []; private scenarioTimeoutMs: number; private pendingNetworkDetailReads: Promise[] = []; @@ -369,6 +385,7 @@ export class Chrome { public async enableNetworkTracking() { const requests = new Map(); + const webSockets = new Map(); const readRequestBody = (row: NetworkRequest) => { if (!row.hasRequestBody || row.requestBody != null) return; @@ -405,6 +422,58 @@ export class Chrome { this.networkRequests.push(row); }); + this.cdp.on('Network.webSocketCreated', params => { + if (params.requestId == null || params.url == null) return; + const row: WebSocketConnection = { + requestId: params.requestId, + url: params.url, + createdAt: params.timestamp ?? 0, + sentFrameCount: 0, + receivedFrameCount: 0, + errorCount: 0, + }; + webSockets.set(params.requestId, row); + this.webSocketConnections.push(row); + }); + + this.cdp.on('Network.webSocketWillSendHandshakeRequest', params => { + const row = webSockets.get(params.requestId); + if (row == null) return; + row.handshakeRequestHeaders = normalizeHeaders(params.request?.headers); + }); + + this.cdp.on('Network.webSocketHandshakeResponseReceived', params => { + const row = webSockets.get(params.requestId); + if (row == null) return; + row.handshakeResponseStatus = params.response?.status; + row.handshakeResponseStatusText = params.response?.statusText; + row.handshakeResponseHeaders = normalizeHeaders(params.response?.headers); + }); + + this.cdp.on('Network.webSocketFrameSent', params => { + const row = webSockets.get(params.requestId); + if (row == null) return; + row.sentFrameCount += 1; + }); + + this.cdp.on('Network.webSocketFrameReceived', params => { + const row = webSockets.get(params.requestId); + if (row == null) return; + row.receivedFrameCount += 1; + }); + + this.cdp.on('Network.webSocketFrameError', params => { + const row = webSockets.get(params.requestId); + if (row == null) return; + row.errorCount += 1; + }); + + this.cdp.on('Network.webSocketClosed', params => { + const row = webSockets.get(params.requestId); + if (row == null) return; + row.closedAt = params.timestamp ?? 0; + }); + this.cdp.on('Network.responseReceived', params => { const row = requests.get(params.requestId); if (row == null) return; @@ -609,7 +678,7 @@ function isMeasurableRequest(row: NetworkRequest) { return !row.url.startsWith('data:') && !row.url.startsWith('blob:') && !row.url.startsWith('devtools:'); } -export function summarizeNetwork(requestRows: NetworkRequest[], baseUrl: string): NetworkSummary { +export function summarizeNetwork(requestRows: NetworkRequest[], baseUrl: string, webSocketRows?: WebSocketConnection[]): NetworkSummary { const origin = new URL(baseUrl).origin; const rows = requestRows.filter(isMeasurableRequest); const byResourceType = {} as NetworkSummary['byResourceType']; @@ -636,6 +705,9 @@ export function summarizeNetwork(requestRows: NetworkRequest[], baseUrl: string) return { requestCount: rows.length, + webSocketConnectionCount: webSocketRows == null + ? rows.filter(row => row.resourceType === 'WebSocket').length + : webSocketRows.length, finishedRequestCount: rows.filter(row => row.finished).length, failedRequestCount: rows.filter(row => row.failed).length, cachedRequestCount: rows.filter(row => row.fromDiskCache).length, diff --git a/.github/scripts/frontend-browser-report.mts b/.github/scripts/frontend-browser-report.mts index 20d1643378..fb046fb438 100644 --- a/.github/scripts/frontend-browser-report.mts +++ b/.github/scripts/frontend-browser-report.mts @@ -18,6 +18,7 @@ export type BrowserMeasurement = { durationMs: number; network: { requestCount: number; + webSocketConnectionCount?: number; finishedRequestCount: number; failedRequestCount: number; cachedRequestCount: number; @@ -157,6 +158,10 @@ function resourceTypeSampleBytes(sample: BrowserMeasurementSample, resourceTypes return resourceTypeBytes(sample, resourceTypes); } +function webSocketConnectionCount(report: BrowserMeasurement) { + return report.network.webSocketConnectionCount ?? report.network.byResourceType.WebSocket?.requests ?? 0; +} + function getMetric(report: BrowserMeasurement, key: string) { return report.performance.cdpMetrics[key]; } @@ -165,6 +170,7 @@ function renderSummaryTable(base: BrowserMetricsReport, head: BrowserMetricsRepo const rows = [ //metricRow('Scenario duration', base, head, summary => summary.durationMs, sample => sample.durationMs, formatMs), metricRow('Requests', base, head, summary => summary.network.requestCount, sample => sample.network.requestCount, util.formatNumber), + metricRow('WebSocket connections', base, head, webSocketConnectionCount, webSocketConnectionCount, util.formatNumber), //metricRow('Failed requests', base, head, summary => summary.network.failedRequestCount, sample => sample.network.failedRequestCount, util.formatNumber), metricRow('Encoded network', base, head, summary => summary.network.totalEncodedBytes, sample => sample.network.totalEncodedBytes, util.formatBytes, 10000), metricRow('Decoded body', base, head, summary => summary.network.totalDecodedBodyBytes, sample => sample.network.totalDecodedBodyBytes, util.formatBytes, 10000), diff --git a/.github/scripts/measure-frontend-browser-comparison.mts b/.github/scripts/measure-frontend-browser-comparison.mts index 65678b3894..18f2a87874 100644 --- a/.github/scripts/measure-frontend-browser-comparison.mts +++ b/.github/scripts/measure-frontend-browser-comparison.mts @@ -132,6 +132,7 @@ function summarizeNetworkSamples(samples: BrowserMeasurementSample[]): NetworkSu return { requestCount: finiteMedian(samples.map(sample => sample.network.requestCount)), + webSocketConnectionCount: finiteMedian(samples.map(sample => sample.network.webSocketConnectionCount)), finishedRequestCount: finiteMedian(samples.map(sample => sample.network.finishedRequestCount)), failedRequestCount: finiteMedian(samples.map(sample => sample.network.failedRequestCount)), cachedRequestCount: finiteMedian(samples.map(sample => sample.network.cachedRequestCount)), @@ -242,7 +243,7 @@ async function measureSample(label: 'base' | 'head', round: number, heapSnapshot url: baseUrl, scenario: 'fresh browser signup, first timeline note, after the note becomes visible', durationMs, - network: summarizeNetwork(chrome.networkRequests, baseUrl), + network: summarizeNetwork(chrome.networkRequests, baseUrl, chrome.webSocketConnections), networkRequests: chrome.networkRequests, performance, heapSnapshot, diff --git a/.github/scripts/utility.mts b/.github/scripts/utility.mts index de97c1c7be..caec5555ef 100644 --- a/.github/scripts/utility.mts +++ b/.github/scripts/utility.mts @@ -5,7 +5,7 @@ // NOTE: このファイルはworkflow上でバックエンドからも参照されるため、side effectがあってはならない -import { ChildProcessWithoutNullStreams, spawn, spawnSync } from 'node:child_process'; +import { spawn, spawnSync, type ChildProcessWithoutNullStreams } from 'node:child_process'; import { promises as fs } from 'node:fs'; import path from 'node:path';