UNPKG

chrome-devtools-frontend

Version:
527 lines (470 loc) • 22 kB
// Copyright 2024 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import {TraceLoader} from '../../../../testing/TraceLoader.js'; import * as Trace from '../../trace.js'; import * as Lantern from '../lantern.js'; import {runTrace, toLanternTrace} from '../testing/testing.js'; const {NetworkAnalyzer} = Lantern.Core; async function createRequests(context: Mocha.Suite|Mocha.Context, trace: Lantern.Types.Trace) { const parsedTrace = await runTrace(context, trace); return Trace.LanternComputationData.createNetworkRequests(trace, parsedTrace); } describe('NetworkAnalyzer', () => { let trace: Lantern.Types.Trace; let traceWithRedirect: Lantern.Types.Trace; before(async function() { trace = toLanternTrace(await TraceLoader.rawEvents(this, 'lantern/paul/trace.json.gz')); traceWithRedirect = toLanternTrace(await TraceLoader.rawEvents(this, 'lantern/redirect/trace.json.gz')); }); let recordId = 1; function createRecord(opts: { // Real request ids are strings but we take a number here to make test // setup easier. requestId?: number, connectionId?: number, connectionReused?: boolean, url?: string, networkRequestTime?: number, networkEndTime?: number, protocol?: string, transferSize?: number, resourceType?: string, timing?: {connectStart?: number, connectEnd?: number, sendStart?: number, receiveHeadersEnd?: number}, }): Trace.Lantern.Types.NetworkRequest { const url = opts.url || 'https://example.com'; if (opts.networkRequestTime) { opts.networkRequestTime *= 1000; } if (opts.networkEndTime) { opts.networkEndTime *= 1000; } const requestId = opts.requestId ? String(opts.requestId) : String(recordId++); delete opts.requestId; return Object.assign( { url, requestId, connectionId: 0, connectionReused: false, networkRequestTime: 10, networkEndTime: 10, transferSize: 10000, protocol: opts.protocol || 'http/1.1', parsedURL: {scheme: url.match(/https?/)?.[0], securityOrigin: url.match(/.*\.com/)?.[0]}, timing: opts.timing || null, }, opts, ) as unknown as Trace.Lantern.Types.NetworkRequest; } beforeEach(() => { recordId = 1; }); function assertCloseEnough(valueA: number, valueB: number, threshold = 1) { const message = `${valueA} was not close enough to ${valueB}`; assert.isOk(Math.abs(valueA - valueB) < threshold, message); } describe('#estimateIfConnectionWasReused', function() { it('should use built-in value when trustworthy', () => { const records = [ createRecord({requestId: 1, connectionId: 1, connectionReused: false}), createRecord({requestId: 2, connectionId: 1, connectionReused: true}), createRecord({requestId: 3, connectionId: 2, connectionReused: false}), createRecord({requestId: 4, connectionId: 3, connectionReused: false}), createRecord({requestId: 5, connectionId: 2, connectionReused: true}), ]; // the `records` are not "full" NetworkRequest items but they are good enough for this test. const result = NetworkAnalyzer.estimateIfConnectionWasReused(records as unknown as Trace.Lantern.Types.NetworkRequest[]); const expected = new Map([['1', false], ['2', true], ['3', false], ['4', false], ['5', true]]); assert.deepEqual(result, expected); }); it('should estimate values when not trustworthy (duplicate IDs)', () => { const records = [ createRecord({requestId: 1, networkRequestTime: 0, networkEndTime: 15}), createRecord({requestId: 2, networkRequestTime: 10, networkEndTime: 25}), createRecord({requestId: 3, networkRequestTime: 20, networkEndTime: 40}), createRecord({requestId: 4, networkRequestTime: 30, networkEndTime: 40}), ]; const result = NetworkAnalyzer.estimateIfConnectionWasReused(records as unknown as Trace.Lantern.Types.NetworkRequest[]); const expected = new Map([['1', false], ['2', false], ['3', true], ['4', true]]); assert.deepEqual(result, expected); }); it('should estimate values when not trustworthy (connectionReused nonsense)', () => { const records = [ createRecord({ requestId: 1, connectionId: 1, connectionReused: true, networkRequestTime: 0, networkEndTime: 15, }), createRecord({ requestId: 2, connectionId: 1, connectionReused: true, networkRequestTime: 10, networkEndTime: 25, }), createRecord({ requestId: 3, connectionId: 1, connectionReused: true, networkRequestTime: 20, networkEndTime: 40, }), createRecord({ requestId: 4, connectionId: 2, connectionReused: false, networkRequestTime: 30, networkEndTime: 40, }), ]; const result = NetworkAnalyzer.estimateIfConnectionWasReused(records as unknown as Trace.Lantern.Types.NetworkRequest[]); const expected = new Map([['1', false], ['2', false], ['3', true], ['4', true]]); assert.deepEqual(result, expected); }); it('should estimate with earliest allowed reuse', () => { const records = [ createRecord({requestId: 1, networkRequestTime: 0, networkEndTime: 40}), createRecord({requestId: 2, networkRequestTime: 10, networkEndTime: 15}), createRecord({requestId: 3, networkRequestTime: 20, networkEndTime: 30}), createRecord({requestId: 4, networkRequestTime: 35, networkEndTime: 40}), ]; const result = NetworkAnalyzer.estimateIfConnectionWasReused(records as unknown as Trace.Lantern.Types.NetworkRequest[]); const expected = new Map([['1', false], ['2', false], ['3', true], ['4', true]]); assert.deepEqual(result, expected); }); it('should work on a real trace', async () => { const requests = await createRequests(this, trace); const result = NetworkAnalyzer.estimateIfConnectionWasReused(requests); const distinctConnections = Array.from(result.values()).filter(item => !item).length; assert.strictEqual(result.size, 24); assert.strictEqual(distinctConnections, 8); }); }); describe('#estimateRTTByOrigin', function() { it('should infer from tcp timing when available', () => { const timing = {connectStart: 0, connectEnd: 99}; const request = createRecord({networkRequestTime: 0, networkEndTime: 1, timing}); const result = NetworkAnalyzer.estimateRTTByOrigin([request]); const expected = {min: 99, max: 99, avg: 99, median: 99}; assert.deepEqual(result.get('https://example.com'), expected); }); it('should infer only one estimate if tcp and ssl start times are equal', () => { const timing = {connectStart: 0, connectEnd: 99, sslStart: 0, sslEnd: 99}; const request = createRecord({networkRequestTime: 0, networkEndTime: 1, timing}); const result = NetworkAnalyzer.estimateRTTByOrigin([request]); const expected = {min: 99, max: 99, avg: 99, median: 99}; assert.deepEqual(result.get('https://example.com'), expected); }); it('should infer from tcp and ssl timing when available', () => { const timing = {connectStart: 0, connectEnd: 99, sslStart: 50, sslEnd: 99}; const request = createRecord({networkRequestTime: 0, networkEndTime: 1, timing}); const result = NetworkAnalyzer.estimateRTTByOrigin([request]); const expected = {min: 49, max: 50, avg: 49.5, median: 49.5}; assert.deepEqual(result.get('https://example.com'), expected); }); it('should infer from connection timing when available for h3 (one estimate)', () => { const timing = {connectStart: 0, connectEnd: 99, sslStart: 1, sslEnd: 99}; const request = createRecord({networkRequestTime: 0, networkEndTime: 1, timing, protocol: 'h3'}); const result = NetworkAnalyzer.estimateRTTByOrigin([request]); const expected = {min: 99, max: 99, avg: 99, median: 99}; assert.deepEqual(result.get('https://example.com'), expected); }); it('should infer from sendStart when available', () => { const timing = {sendStart: 150}; // this request took 150ms before Chrome could send the request // i.e. DNS (maybe) + queuing (maybe) + TCP handshake took ~100ms // 150ms / 3 round trips ~= 50ms RTT const request = createRecord({networkRequestTime: 0, networkEndTime: 1, timing}); const result = NetworkAnalyzer.estimateRTTByOrigin([request], {coarseEstimateMultiplier: 1}); const expected = {min: 50, max: 50, avg: 50, median: 50}; assert.deepEqual(result.get('https://example.com'), expected); }); it('should infer from download timing when available', () => { const timing = {receiveHeadersEnd: 100}; // this request took 1000ms after the first byte was received to download the payload // i.e. it took at least one full additional roundtrip after first byte to download the rest // 1000ms / 1 round trip ~= 1000ms RTT const request = createRecord({networkRequestTime: 0, networkEndTime: 1.1, transferSize: 28 * 1024, timing}); const result = NetworkAnalyzer.estimateRTTByOrigin([request], { coarseEstimateMultiplier: 1, useHeadersEndEstimates: false, }); const expected = {min: 1000, max: 1000, avg: 1000, median: 1000}; assert.deepEqual(result.get('https://example.com'), expected); }); it('should infer from TTFB when available', () => { const timing = {receiveHeadersEnd: 1000}; const request = createRecord({networkRequestTime: 0, networkEndTime: 1, timing, resourceType: 'Other'}); const result = NetworkAnalyzer.estimateRTTByOrigin([request], { coarseEstimateMultiplier: 1, }); // this request's TTFB was 1000ms, it used SSL and was a fresh connection requiring a handshake // which needs ~4 RTs. We don't know its resource type so it'll be assumed that 40% of it was // server response time. // 600 ms / 4 = 150ms const expected = {min: 150, max: 150, avg: 150, median: 150}; assert.deepEqual(result.get('https://example.com'), expected); }); it('should use coarse estimates on a per-origin basis', () => { const records = [ createRecord({url: 'https://example.com', timing: {connectStart: 1, connectEnd: 100, sendStart: 150}}), createRecord({url: 'https://example2.com', timing: {sendStart: 150}}), ]; const result = NetworkAnalyzer.estimateRTTByOrigin(records); assert.deepEqual(result.get('https://example.com'), {min: 99, max: 99, avg: 99, median: 99}); assert.deepEqual(result.get('https://example2.com'), {min: 15, max: 15, avg: 15, median: 15}); }); it('should handle untrustworthy connection information', () => { const timing = {sendStart: 150}; const recordA = createRecord({networkRequestTime: 0, networkEndTime: 1, timing, connectionReused: true}); const recordB = createRecord({ networkRequestTime: 0, networkEndTime: 1, timing, connectionId: 2, connectionReused: true, }); const result = NetworkAnalyzer.estimateRTTByOrigin([recordA, recordB], { coarseEstimateMultiplier: 1, }); const expected = {min: 50, max: 50, avg: 50, median: 50}; assert.deepEqual(result.get('https://example.com'), expected); }); it('should work on a real trace', async () => { const requests = await createRequests(this, trace); const result = NetworkAnalyzer.estimateRTTByOrigin(requests); assertCloseEnough(result.get('https://www.paulirish.com')?.min ?? 0, 10); assertCloseEnough(result.get('https://www.googletagmanager.com')?.min ?? 0, 17); assertCloseEnough(result.get('https://www.google-analytics.com')?.min ?? 0, 10); }); it('should approximate well with either method', async () => { const requests = await createRequests(this, trace); const result = NetworkAnalyzer.estimateRTTByOrigin(requests).get(NetworkAnalyzer.summary); const resultApprox = NetworkAnalyzer .estimateRTTByOrigin(requests, { forceCoarseEstimates: true, }) .get(NetworkAnalyzer.summary); assert.isOk(result); assert.isOk(resultApprox); assertCloseEnough(result.min, resultApprox.min, 20); assertCloseEnough(result.avg, resultApprox.avg, 30); assertCloseEnough(result.median, resultApprox.median, 30); }); }); describe('#estimateServerResponseTimeByOrigin', function() { it('should estimate server response time using ttfb times', () => { const timing = {sendEnd: 100, receiveHeadersEnd: 200}; const request = createRecord({networkRequestTime: 0, networkEndTime: 1, timing}); const rttByOrigin = new Map([[NetworkAnalyzer.summary, 0]]); const result = NetworkAnalyzer.estimateServerResponseTimeByOrigin([request], {rttByOrigin}); const expected = {min: 100, max: 100, avg: 100, median: 100}; assert.deepEqual(result.get('https://example.com'), expected); }); it('should subtract out rtt', () => { const timing = {sendEnd: 100, receiveHeadersEnd: 200}; const request = createRecord({networkRequestTime: 0, networkEndTime: 1, timing}); const rttByOrigin = new Map([[NetworkAnalyzer.summary, 50]]); const result = NetworkAnalyzer.estimateServerResponseTimeByOrigin([request], {rttByOrigin}); const expected = {min: 50, max: 50, avg: 50, median: 50}; assert.deepEqual(result.get('https://example.com'), expected); }); it('should compute rtts when not provided', () => { const timing = {connectStart: 5, connectEnd: 55, sendEnd: 100, receiveHeadersEnd: 200}; const request = createRecord({networkRequestTime: 0, networkEndTime: 1, timing}); const result = NetworkAnalyzer.estimateServerResponseTimeByOrigin([request]); const expected = {min: 50, max: 50, avg: 50, median: 50}; assert.deepEqual(result.get('https://example.com'), expected); }); it('should work on a real trace', async () => { const requests = await createRequests(this, trace); const result = NetworkAnalyzer.estimateServerResponseTimeByOrigin(requests); assertCloseEnough(result.get('https://www.paulirish.com')?.avg ?? 0, 35); assertCloseEnough(result.get('https://www.googletagmanager.com')?.avg ?? 0, 8); assertCloseEnough(result.get('https://www.google-analytics.com')?.avg ?? 0, 8); }); it('should approximate well with either method', async () => { const requests = await createRequests(this, trace); const result = NetworkAnalyzer.estimateServerResponseTimeByOrigin(requests).get( NetworkAnalyzer.summary, ); const resultApprox = NetworkAnalyzer .estimateServerResponseTimeByOrigin(requests, { forceCoarseEstimates: true, }) .get(NetworkAnalyzer.summary); assert.isOk(result); assert.isOk(resultApprox); assertCloseEnough(result.min, resultApprox.min, 20); assertCloseEnough(result.avg, resultApprox.avg, 30); assertCloseEnough(result.median, resultApprox.median, 30); }); }); describe('#estimateThroughput', () => { const estimateThroughput = NetworkAnalyzer.estimateThroughput; function createThroughputRecord(responseHeadersEndTimeInS: number, networkEndTimeInS: number, extras: object = {}): Trace.Lantern.Types.NetworkRequest { return Object.assign( { responseHeadersEndTime: responseHeadersEndTimeInS * 1000, networkEndTime: networkEndTimeInS * 1000, transferSize: 1000, finished: true, failed: false, statusCode: 200, url: 'https://google.com/logo.png', parsedURL: {scheme: 'https'}, }, extras, ) as unknown as Trace.Lantern.Types.NetworkRequest; } it('should return null for no/missing records', () => { assert.isNull(estimateThroughput([])); assert.isNull(estimateThroughput([createThroughputRecord(0, 0, {finished: false})])); }); it('should compute correctly for a basic waterfall', () => { const result = estimateThroughput([ createThroughputRecord(0, 1), createThroughputRecord(1, 2), createThroughputRecord(2, 6), ]); assert.strictEqual(result, 500 * 8); }); it('should compute correctly for concurrent requests', () => { const result = estimateThroughput([ createThroughputRecord(0, 1), createThroughputRecord(0.5, 1), ]); assert.strictEqual(result, 2000 * 8); }); it('should compute correctly for gaps', () => { const result = estimateThroughput([ createThroughputRecord(0, 1), createThroughputRecord(3, 4), ]); assert.strictEqual(result, 1000 * 8); }); it('should compute correctly for partially overlapping requests', () => { const result = estimateThroughput([ createThroughputRecord(0, 1), createThroughputRecord(0.5, 1.5), createThroughputRecord(1.25, 3), createThroughputRecord(1.4, 4), createThroughputRecord(5, 9), ]); assert.strictEqual(result, 625 * 8); }); it('should exclude failed records', () => { const result = estimateThroughput([ createThroughputRecord(0, 2), createThroughputRecord(3, 4, {failed: true}), ]); assert.strictEqual(result, 500 * 8); }); it('should exclude cached records', () => { const result = estimateThroughput([ createThroughputRecord(0, 2), createThroughputRecord(3, 4, {statusCode: 304}), ]); assert.strictEqual(result, 500 * 8); }); it('should exclude unfinished records', () => { const result = estimateThroughput([ createThroughputRecord(0, 2), createThroughputRecord(3, 4, {finished: false}), ]); assert.strictEqual(result, 500 * 8); }); it('should exclude data URIs', () => { const result = estimateThroughput([ createThroughputRecord(0, 2), createThroughputRecord(3, 4, {parsedURL: {scheme: 'data'}}), ]); assert.strictEqual(result, 500 * 8); }); }); describe('#computeRTTAndServerResponseTime', function() { it('should work', async () => { const requests = await createRequests(this, trace); const result = NetworkAnalyzer.computeRTTAndServerResponseTime(requests); expect(result.rtt).to.be.closeTo(0.082, 0.001); assert.deepEqual([...result.additionalRttByOrigin.entries()], [ [ 'https://www.paulirish.com', 9.788999999999994, ], [ 'https://www.googletagmanager.com', 17.21999999999999, ], [ 'https://fonts.googleapis.com', 16.816000000000003, ], [ 'https://fonts.gstatic.com', 1.6889999999999998, ], [ 'https://www.google-analytics.com', 9.924999999999997, ], [ 'https://paulirish.disqus.com', 9.000999999999998, ], [ 'https://firebaseinstallations.googleapis.com', 0, ], [ 'https://firebaseremoteconfig.googleapis.com', 0.1823, ], [ '__SUMMARY__', 0, ], ]); }); }); describe('#findMainDocument', function() { it('should find the main document', async () => { const requests = await createRequests(this, trace); const mainDocument = NetworkAnalyzer.findResourceForUrl(requests, 'https://www.paulirish.com/'); assert.isOk(mainDocument); assert.strictEqual(mainDocument.url, 'https://www.paulirish.com/'); }); it('should find the main document if the URL includes a fragment', async () => { const requests = await createRequests(this, trace); const mainDocument = NetworkAnalyzer.findResourceForUrl(requests, 'https://www.paulirish.com/#info'); assert.isOk(mainDocument); assert.strictEqual(mainDocument.url, 'https://www.paulirish.com/'); }); }); describe('#resolveRedirects', function() { it('should resolve to the same document when no redirect', async () => { const requests = await createRequests(this, trace); const mainDocument = NetworkAnalyzer.findResourceForUrl(requests, 'https://www.paulirish.com/'); assert.isOk(mainDocument); const finalDocument = NetworkAnalyzer.resolveRedirects(mainDocument); assert.strictEqual(mainDocument.url, finalDocument.url); assert.strictEqual(finalDocument.url, 'https://www.paulirish.com/'); }); it('should resolve to the final document with redirects', async () => { const requests = await createRequests(this, traceWithRedirect); const mainDocument = NetworkAnalyzer.findResourceForUrl(requests, 'http://www.vkontakte.ru/'); assert.isOk(mainDocument); const finalDocument = NetworkAnalyzer.resolveRedirects(mainDocument); assert.notEqual(mainDocument.url, finalDocument.url); assert.strictEqual(finalDocument.url, 'https://m.vk.com/'); }); }); });