├── third_party ├── proto-converter │ ├── .nvmrc │ ├── .gitignore │ ├── package.json │ ├── tsconfig.json │ ├── package-lock.json │ └── proto-to-json.ts ├── timing-trace-saver-test.js ├── timing-trace-saver.js ├── pwmetrics.events.js ├── lantern-trace-saver.js └── cpu-profile-model.js ├── .gitignore ├── format-trace.mjs ├── tsconfig.json ├── evaluate-screenshots.mjs ├── events-with-arg.mjs ├── types └── chromium-trace.d.ts ├── readme.md ├── bytes-in-trace-by-cat.mjs ├── generic-trace-to-devtools-trace.mjs ├── trace-file-utils.mjs ├── user-timings-to-trace.mjs ├── extract-cpu-profile-from-trace.mjs ├── winnow-trace.mjs ├── LICENSE ├── process-traces.mjs └── extract-netlog-from-trace.mjs /third_party/proto-converter/.nvmrc: -------------------------------------------------------------------------------- 1 | v11.9.0 2 | -------------------------------------------------------------------------------- /third_party/proto-converter/.gitignore: -------------------------------------------------------------------------------- 1 | proto-to-json.js -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.cpuprofile 2 | 3 | myjansatta.json 4 | coo.f.json 5 | coo.json -------------------------------------------------------------------------------- /third_party/proto-converter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | }, 5 | "devDependencies": { 6 | "@types/node": "^11.11.4", 7 | "protobufjs": "^6.8.8", 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /third_party/proto-converter/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "commonjs", 5 | "lib": ["es6","dom"], 6 | "outDir": "lib", 7 | "rootDir": "src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "resolveJsonModule": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /format-trace.mjs: -------------------------------------------------------------------------------- 1 | 2 | // Save a "properly formatted" version of the trace (with a new filename). 3 | 4 | import path from 'node:path'; 5 | import {saveTrace, loadTraceEventsFromFile} from './trace-file-utils.mjs'; 6 | 7 | export async function resaveTrace(filename, filterEventFn) { 8 | const traceEvents = loadTraceEventsFromFile(filename); 9 | const afterFilename = `${filename}.formatted.json`; 10 | await saveTrace({traceEvents}, afterFilename); 11 | console.log(`Written: ${afterFilename}`); 12 | } 13 | 14 | // CLI direct invocation? 15 | if (import.meta.url.endsWith(process?.argv[1])) { 16 | cli(); 17 | } 18 | 19 | async function cli() { 20 | const filename = path.resolve(process.cwd(), process.argv[2]); 21 | await resaveTrace(filename); // , filterEventFn); 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": ".tmp/tsbuildinfo/", 4 | "composite": true, 5 | "emitDeclarationOnly": true, 6 | "declarationMap": true, 7 | 8 | "target": "es2020", 9 | "module": "es2020", 10 | "moduleResolution": "node", 11 | "esModuleInterop": true, 12 | 13 | "allowJs": true, 14 | "checkJs": true, 15 | "strict": true, 16 | "forceConsistentCasingInFileNames": true, 17 | 18 | 19 | "lib": ["ESNext", "dom"] 20 | 21 | // "listFiles": true, 22 | // "noErrorTruncation": true, 23 | // "extendedDiagnostics": true, 24 | 25 | // TODO(esmodules): included to support require('file.json'). Remove on the switch to ES Modules. 26 | // "resolveJsonModule": true, 27 | }, 28 | "include": [ 29 | "types/*.d.ts", 30 | "*.js", 31 | "*.mjs", 32 | // not third_party yet 33 | 34 | // "node_modules/devtools-protocol/types/protocol.d.ts", 35 | 36 | // TODO(esmodules): JSON files included via resolveJsonModule. Removable on the switch to ES Modules. 37 | "package.json" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /evaluate-screenshots.mjs: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Emit some summary numbers about the screenshots 4 | */ 5 | 6 | import fs from 'node:fs'; 7 | 8 | const passedArg = process.argv[2]; 9 | const filename = passedArg ? passedArg : './scroll-tl-viewer.json'; 10 | 11 | console.log('Parsing: ', filename); 12 | const stat = fs.statSync(filename); 13 | console.log('size:' , ( stat.size / 1_000_000).toLocaleString(), 'MB'); 14 | 15 | let trace = JSON.parse(fs.readFileSync(filename, 'utf-8')); 16 | 17 | if (trace.length) { 18 | const traceEvents = trace; 19 | trace = { 20 | traceEvents, 21 | }; 22 | } 23 | 24 | 25 | console.log('event count: ', trace.traceEvents.length.toLocaleString()) 26 | 27 | const screenshotEvts = trace.traceEvents.filter(e => { 28 | return e.cat === 'disabled-by-default-devtools.screenshot'; 29 | }).sort((a, b) => a.ts - b.ts); 30 | 31 | console.log('screenshot event count: ', screenshotEvts.length.toLocaleString()) 32 | const timeDeltas = screenshotEvts.map((evt, i) => { 33 | if (i === 0) return 0; 34 | return evt.ts - screenshotEvts[i - 1].ts 35 | }); 36 | const timeDeltaMap = new Map(); 37 | for (const delta of timeDeltas) { 38 | let sum = timeDeltaMap.get(delta) || 0; 39 | sum++; 40 | timeDeltaMap.set(delta, sum); 41 | } 42 | 43 | console.log({timeDeltaMap}) 44 | 45 | 46 | let sizeSum = 0; 47 | screenshotEvts.forEach(e => { 48 | sizeSum += (JSON.stringify(e).length); 49 | }) 50 | 51 | console.log({sizeSum: (sizeSum / 1000).toLocaleString() + ' kb'}) 52 | 53 | 54 | 55 | const duration = (screenshotEvts.at(-1).ts - screenshotEvts.at(0).ts) / 1000; 56 | console.log({duration}); 57 | 58 | console.log('bitrate (bytes per sec): ', sizeSum / (duration / 1000)); 59 | // the video is roughly 10x more byte-cost effective. (a tenth of the size) 60 | 61 | 62 | const droppedFramePct = screenshotEvts.length / (duration / 16.66666); 63 | console.log('dropped frame %:', ((1 - droppedFramePct) * 100).toLocaleString()) 64 | 65 | -------------------------------------------------------------------------------- /third_party/timing-trace-saver-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Copyright 2018 The Lighthouse Authors. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 5 | */ 6 | 7 | /* eslint-disable no-console */ 8 | 9 | import {generateTraceEvents, createTraceString} from '../../lib/timing-trace-saver.js'; 10 | 11 | const mockEntries = [{ 12 | startTime: 650, 13 | name: 'lh:init:config', 14 | duration: 210, 15 | entryType: 'measure', 16 | }, 17 | { 18 | startTime: 870, 19 | name: 'lh:runner:run', 20 | duration: 120, 21 | entryType: 'measure', 22 | }, 23 | { 24 | startTime: 990, 25 | name: 'lh:runner:auditing', 26 | duration: 750, 27 | entryType: 'measure', 28 | }, 29 | { 30 | startTime: 1010, 31 | name: 'lh:audit:is-on-https', 32 | duration: 10, 33 | entryType: 'measure', 34 | }, 35 | ]; 36 | 37 | describe('generateTraceEvents', () => { 38 | it('generates a pair of trace events', () => { 39 | const events = generateTraceEvents([mockEntries[0]]); 40 | expect(events.slice(0, 2)).toMatchSnapshot(); 41 | }); 42 | }); 43 | 44 | describe('createTraceString', () => { 45 | it('creates a real trace', () => { 46 | const jsonStr = createTraceString({ 47 | timing: { 48 | entries: mockEntries, 49 | }, 50 | }); 51 | const traceJson = JSON.parse(jsonStr); 52 | const eventsWithoutMetadata = traceJson.traceEvents.filter(e => e.cat !== '__metadata'); 53 | expect(eventsWithoutMetadata).toMatchSnapshot(); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /events-with-arg.mjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 'use strict'; 3 | 4 | 5 | import fs from 'node:fs'; 6 | 7 | const passedArg = process.argv[2]; 8 | const filename = passedArg ? passedArg : './scroll-tl-viewer.json'; 9 | 10 | console.log('Parsing: ', filename); 11 | const stat = fs.statSync(filename); 12 | console.log('size:' , ( stat.size / 1_000_000).toLocaleString(), 'MB'); 13 | console.log('first by event name + category. then by category'); 14 | 15 | let trace = JSON.parse(fs.readFileSync(filename, 'utf-8')); 16 | 17 | 18 | function cool() { 19 | const eventNames = {}; 20 | 21 | 22 | if (trace.length) { 23 | const traceEvents = trace; 24 | trace = { 25 | traceEvents, 26 | }; 27 | } 28 | console.log('event count: ', trace.traceEvents.length.toLocaleString()) 29 | 30 | trace.traceEvents.forEach(e => { 31 | let eventCats = e.cat; 32 | const frame = e.args.frame ?? e.args.data?.frame; 33 | 34 | if (e.ph === 'R' || e.ph === 'I') return; 35 | if (frame) { 36 | eventNames[`${e.cat.padEnd(50)} ${e.name} ${e.ph}`] = frame; 37 | 38 | } 39 | 40 | }); 41 | 42 | console.log(Object.keys(eventNames).sort()); 43 | 44 | const argValues = Array.from(new Set(Object.values(eventNames))); 45 | 46 | console.log(argValues.sort()); 47 | 48 | } 49 | 50 | 51 | cool(false); 52 | 53 | 54 | 55 | 56 | function groupAndOutput(traceCats, totalBytes, totalEvents) { 57 | // obj to array 58 | const traceTotals = []; 59 | Object.keys(traceCats).forEach(catname => { 60 | const cat = traceCats[catname]; 61 | traceTotals.push({name: catname, bytes: cat.bytes, events: cat.events}); 62 | }); 63 | 64 | // sort and log 65 | console.log('\n'); 66 | console.log('Bytes'.padStart(16), '\t', 'Count'.padStart(7), '\t', 'Event Name'.padStart(18)) 67 | 68 | let skipped = {bytes: 0, events: 0}; 69 | traceTotals.sort((a, b) => b.bytes - a.bytes).forEach((tot, i) => { 70 | const bytesPct = tot.bytes * 100/ totalBytes; 71 | if (bytesPct < 1) { 72 | skipped.bytes += tot.bytes; 73 | skipped.events += tot.events; 74 | return; // dont output. 75 | } 76 | 77 | console.log( 78 | tot.bytes.toLocaleString().padStart(15), 79 | `${(bytesPct).toLocaleString(undefined, {maximumFractionDigits: 1})}%`.padStart(6), 80 | '\t', 81 | tot.events.toLocaleString().padStart(9), 82 | `${(tot.events * 100/ totalEvents).toLocaleString(undefined, {maximumFractionDigits: 1})}%`.padStart(6), 83 | '\t', 84 | tot.name 85 | ); 86 | }) 87 | 88 | // skipped 89 | console.log( 90 | skipped.bytes.toLocaleString().padStart(15), 91 | `${( skipped.bytes * 100/ totalBytes).toLocaleString(undefined, {maximumFractionDigits: 1})}%`.padStart(6), 92 | '\t', 93 | skipped.events.toLocaleString().padStart(9), 94 | `${(skipped.events * 100/ totalEvents).toLocaleString(undefined, {maximumFractionDigits: 1})}%`.padStart(6), 95 | '\t', 96 | '[(Rows that were < 1% of bytes)]' 97 | ); 98 | } 99 | -------------------------------------------------------------------------------- /types/chromium-trace.d.ts: -------------------------------------------------------------------------------- 1 | // Thanks https://github.com/GoogleChrome/lighthouse/blob/8577fd6dacdfb930bd6c87f8e159ab63b43e3cd1/types/artifacts.d.ts 2 | 3 | export interface Trace { 4 | traceEvents: TraceEvent[]; 5 | metadata?: { 6 | 'cpu-family'?: number; 7 | }; 8 | [futureProps: string]: any; 9 | } 10 | 11 | /** The type of the Profile & ProfileChunk event in Chromium traces. Note that this is subtly different from Crdp.Profiler.Profile. */ 12 | export interface TraceCpuProfile { 13 | nodes?: Array<{id: number, callFrame: {functionName: string, url?: string}, parent?: number}> 14 | samples?: Array 15 | timeDeltas?: Array 16 | } 17 | 18 | /** 19 | * @see https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview 20 | */ 21 | export interface TraceEvent { 22 | name: string; 23 | cat: string; 24 | args: { 25 | fileName?: string; 26 | snapshot?: string; 27 | sync_id?: string; 28 | beginData?: { 29 | frame?: string; 30 | startLine?: number; 31 | url?: string; 32 | }; 33 | source_type?: string; 34 | data?: { 35 | frameTreeNodeId?: number; 36 | persistentIds?: boolean; 37 | frame?: string; 38 | isLoadingMainFrame?: boolean; 39 | documentLoaderURL?: string; 40 | frames?: { 41 | frame: string; 42 | parent?: string; 43 | processId?: number; 44 | }[]; 45 | page?: string; 46 | readyState?: number; 47 | requestId?: string; 48 | startTime?: number; 49 | endTime?: number; 50 | timeDeltas?: TraceCpuProfile['timeDeltas']; 51 | cpuProfile?: TraceCpuProfile; 52 | callFrame?: Required['nodes'][0]['callFrame'] 53 | /** Marker for each synthetic CPU profiler event for the range of _potential_ ts values. */ 54 | _syntheticProfilerRange?: { 55 | earliestPossibleTimestamp: number 56 | latestPossibleTimestamp: number 57 | } 58 | stackTrace?: { 59 | url: string 60 | }[]; 61 | styleSheetUrl?: string; 62 | timerId?: string; 63 | url?: string; 64 | is_main_frame?: boolean; 65 | cumulative_score?: number; 66 | id?: string; 67 | nodeId?: number; 68 | impacted_nodes?: Array<{ 69 | node_id: number, 70 | old_rect?: Array, 71 | new_rect?: Array, 72 | }>; 73 | score?: number; 74 | weighted_score_delta?: number; 75 | had_recent_input?: boolean; 76 | compositeFailed?: number; 77 | unsupportedProperties?: string[]; 78 | size?: number; 79 | finishTime?: number; 80 | encodedDataLength?: number; 81 | decodedBodyLength?: number; 82 | didFail?: boolean; 83 | 84 | }; 85 | frame?: string; 86 | name?: string; 87 | labels?: string; 88 | attribution?: Array; 89 | }; 90 | pid: number; 91 | tid: number; 92 | /** Timestamp of the event in microseconds. */ 93 | ts: number; 94 | dur?: number; // Not included on usertiming marks… 95 | ph: string; // TODO, make more specific? 'B'|'b'|'D'|'E'|'e'|'F'|'I'|'M'|'N'|'n'|'O'|'R'|'S'|'T'|'X'; 96 | s?: 't'; 97 | id?: string; 98 | id2?: { 99 | local?: string; 100 | }; 101 | } 102 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## various trace file utilities I've amassed over the years 2 | 3 | This repo is messy. 4 | 5 | 🚧 No promises that these aren't broken. Hacking encouraged! 6 | 7 | ### Some of what's here: 8 | 9 | * `bytes-in-trace-by-cat.mjs` - Emit data about what trace event names and categories take up byte size in the JSON 10 | * `format-trace.mjs` - Save a "properly formatted" version of the trace to a new file. 11 | * `user-timings-to-trace.mjs` - Take some user timings (performance.measure/mark) and generate a trace for visualization. 12 | * `winnow-trace.mjs` - Remove trace events to crop it to a timerange, exclude some category, etc. Save it to a new file. 13 | * `generic-trace-to-devtools-trace.mjs` - Take a trace captured from chrome://tracing or perfetto (but converted to json)… And convert it to a trace that DevTools can load as first-class. (not falling back to isGenericTrace handling) 14 | * `extract-netlog-from-trace.mjs` - Extract .netlog from a trace, to use in the [viewer](https://netlog-viewer.appspot.com/). 15 | * `extract-cpu-profile-from-trace.mjs` - Extract .cpuprofile from a trace. It'll create 1 or more .cpuprofiles next to the trace 16 | * `process-traces.mjs` - iterate over all traces found in a folder, run them through a trace processor to see what breaks. 17 | * `trace-file-utils.mjs` - loading, saving utilities.. matching whats in NPP & LH. 18 | 19 | 20 | ## Notes 21 | 22 | ### Trace synthesis 23 | 24 | We have a BUNCH of code that synthesizes traces out of generic data so it can be viewed in `about:tracing` or DevTools. 25 | 26 | Some of those implementations: 27 | * https://github.com/paulirish/rum-trace/blob/main/src/trace/trace.js 28 | * https://github.com/GoogleChrome/lighthouse/blob/main/core/test/create-test-trace.js 29 | * https://github.com/GoogleChrome/lighthouse/blob/main/core/lib/lantern-trace-saver.js 30 | * https://github.com/GoogleChrome/lighthouse/blob/main/core/lib/timing-trace-saver.js 31 | * https://github.com/GoogleChrome/lighthouse/blob/c9584689210c4fff8398e7a124f0819a5d91a4e8/core/lib/tracehouse/cpu-profile-model.js#L116-L134 32 | * https://github.com/GoogleChrome/lighthouse/blob/98eebdaf6daa82957cadd057b16ce680af226bc3/lighthouse-core/lib/traces/pwmetrics-events.js#L137-L164 33 | * Some of the above are local here in `./third_party` 34 | * https://github.com/paulirish/rum-trace 35 | 36 | [`how to make a sweet combo trace that's viewable in devtools.md`](https://gist.github.com/paulirish/792fdf4baaa4acc1b563d177e1ae569d) (2018, probably outdated) 37 | 38 | ## revision numbers for CDT frontend appspot 39 | 40 | See [github.com/paulirish/trace.cafe/blob/9aee52…/src/app.js#L9-L19](https://github.com/paulirish/trace.cafe/blob/9aee52bd11b0f61e31d1278da0fe0006ec0019ce/src/app.js#L9-L19) 41 | 42 | 43 | ## Types 44 | 45 | * https://github.com/ChromeDevTools/devtools-frontend/blob/main/front_end/models/trace/types/TraceEvents.ts hard to beat 46 | * https://github.com/GoogleChrome/lighthouse/blob/7d80178c37a1b600ea8f092fc0b098029799a659/types/artifacts.d.ts#L945-L1048 loosey goosey (also local here in `./types/chromium-trace.d.ts`) 47 | * https://github.com/connorjclark/chrome-trace-events-tsc brute force approach. 48 | * https://github.com/TracerBench/tracerbench/blob/master/packages/trace-event/src/types.ts nice. also tracerbench is pretty great, in general. 49 | 50 | Ephemeral Zenith Whispering Cascade 51 | Quantum Pineapple Serendipity Moonlight 52 | Fuzzy Logic Pancake Spaceship 53 | 54 | Intergalactic space badgers. 55 | syzygy flummoxed shenanigans 56 | -------------------------------------------------------------------------------- /third_party/timing-trace-saver.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Copyright 2018 The Lighthouse Authors. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 5 | */ 6 | 7 | /** 8 | * Generates a chromium trace file from user timing measures 9 | * `threadId` can be provided to separate a series of trace events into another thread, useful 10 | * if timings do not share the same timeOrigin, but should both be "left-aligned". 11 | * Adapted from https://github.com/tdresser/performance-observer-tracing 12 | * @param {LH.Artifacts.MeasureEntry[]} entries user timing entries 13 | * @param {number=} threadId 14 | */ 15 | function generateTraceEvents(entries, threadId = 0) { 16 | if (!Array.isArray(entries)) return []; 17 | 18 | /** @type {LH.TraceEvent[]} */ 19 | const currentTrace = []; 20 | entries.sort((a, b) => a.startTime - b.startTime); 21 | entries.forEach((entry, i) => { 22 | /** @type {LH.TraceEvent} */ 23 | const startEvt = { 24 | // 1) Remove 'lh:' for readability 25 | // 2) Colons in user_timing names get special handling in traceviewer we don't want. https://goo.gl/m23Vz7 26 | // Replace with a 'Modifier letter colon' ;) 27 | name: entry.name.replace('lh:', '').replace(/:/g, '\ua789'), 28 | cat: 'blink.user_timing', 29 | ts: entry.startTime * 1000, 30 | args: {}, 31 | dur: 0, 32 | pid: 0, 33 | tid: threadId, 34 | ph: 'b', 35 | id: '0x' + (i++).toString(16), 36 | }; 37 | 38 | const endEvt = JSON.parse(JSON.stringify(startEvt)); 39 | endEvt.ph = 'e'; 40 | endEvt.ts = startEvt.ts + (entry.duration * 1000); 41 | 42 | currentTrace.push(startEvt); 43 | currentTrace.push(endEvt); 44 | }); 45 | 46 | // Add labels 47 | /** @type {LH.TraceEvent} */ 48 | const metaEvtBase = { 49 | pid: 0, 50 | tid: threadId, 51 | ts: 0, 52 | dur: 0, 53 | ph: 'M', 54 | cat: '__metadata', 55 | name: 'process_labels', 56 | args: {labels: 'Default'}, 57 | }; 58 | currentTrace.push(Object.assign({}, metaEvtBase, {args: {labels: 'Lighthouse Timing'}})); 59 | 60 | // Only inject TracingStartedInBrowser once 61 | if (threadId === 0) { 62 | currentTrace.push(Object.assign({}, metaEvtBase, { 63 | 'cat': 'disabled-by-default-devtools.timeline', 64 | 'name': 'TracingStartedInBrowser', 65 | 'ph': 'I', 66 | 'args': {'data': { 67 | 'frameTreeNodeId': 1, 68 | 'persistentIds': true, 69 | 'frames': [], 70 | }}, 71 | })); 72 | } 73 | return currentTrace; 74 | } 75 | 76 | /** 77 | * Writes a trace file to disk 78 | * @param {LH.Result} lhr 79 | * @return {string} 80 | */ 81 | function createTraceString(lhr) { 82 | const gatherEntries = lhr.timing.entries.filter(entry => entry.gather); 83 | const entries = lhr.timing.entries.filter(entry => !gatherEntries.includes(entry)); 84 | 85 | const auditEvents = generateTraceEvents(entries); 86 | const gatherEvents = generateTraceEvents(gatherEntries, 10); 87 | const events = [...auditEvents, ...gatherEvents]; 88 | 89 | const jsonStr = `{"traceEvents":[ 90 | ${events.map(evt => JSON.stringify(evt)).join(',\n')} 91 | ]}`; 92 | 93 | return jsonStr; 94 | } 95 | 96 | export {generateTraceEvents, createTraceString}; 97 | -------------------------------------------------------------------------------- /bytes-in-trace-by-cat.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Emit data about what trace event names and categories take up byte size in the JSON 3 | * 4 | * Usage: 5 | * 6 | * node bytes-in-trace-by-cat.mjs path/to/trace.json (json.gz supported, too) 7 | */ 8 | 9 | import fs from 'node:fs'; 10 | 11 | import {loadTraceEventsFromFile} from './trace-file-utils.mjs'; 12 | 13 | const passedArg = process.argv[2]; 14 | const filename = passedArg ? passedArg : '/Users/paulirish/Downloads/traces/cdt-clicks-frameseq-on-evtlat.json'; 15 | 16 | console.log('Parsing: ', filename); 17 | const stat = fs.statSync(filename); 18 | console.log('size:', ( stat.size / 1_000_000).toLocaleString(), 'MB'); 19 | console.log('first by event name + category. then by category'); 20 | 21 | const traceEvents = loadTraceEventsFromFile(filename); 22 | console.log('event count: ', traceEvents.length.toLocaleString()); 23 | 24 | 25 | 26 | iterateTrace(); 27 | iterateTrace({aggregateBy: true}); 28 | 29 | 30 | function iterateTrace(opts = {aggregateBy: false}) { 31 | const traceCats = {}; 32 | const tracePhs = {}; 33 | // aggregate 34 | let totalBytes = 0; 35 | let totalEvents = 0; 36 | 37 | let outofOrder = {}; 38 | traceEvents.forEach((e, i) => { 39 | 40 | if (e.ts === undefined || e.null) { throw new Error('Invalid trace event'); } 41 | if (i > 0 && e.ts < traceEvents[i - 1].ts) { 42 | outofOrder[e.cat] = outofOrder[e.cat] || 0; 43 | outofOrder[e.cat]++; 44 | } 45 | 46 | const eventCats = e.cat; 47 | 48 | const splittedCats = opts.aggregateBy ? [eventCats] : eventCats.split(','); 49 | for (let eventId of splittedCats) { 50 | if (opts.aggregateBy) { 51 | eventId = `${e.name.padEnd(35)} (${eventId})`; 52 | } 53 | 54 | // if (e.name === 'ThreadControllerImpl::RunTask') eventId += '::::::::RunTask'; 55 | const cat = traceCats[eventId] || {bytes: 0, count: 0}; 56 | const bytes = JSON.stringify(e).length; 57 | cat.bytes += bytes; 58 | totalBytes += bytes; 59 | cat.count += 1; 60 | totalEvents += 1; 61 | traceCats[eventId] = cat; 62 | } 63 | tracePhs[e.ph] = tracePhs[e.ph] || 0; 64 | tracePhs[e.ph]++; 65 | }); 66 | 67 | reportTotals(traceCats, totalBytes, totalEvents, tracePhs, opts); 68 | console.log('outofOrder:', outofOrder); 69 | } 70 | 71 | 72 | function reportTotals(traceCats, totalBytes, totalEvents, tracePhs, opts) { 73 | // obj to array 74 | const traceTotals = []; 75 | Object.keys(traceCats).forEach(eventId => { 76 | const {bytes, count} = traceCats[eventId]; 77 | traceTotals.push({name: eventId, bytes, count}); 78 | }); 79 | 80 | // sort and log 81 | console.log(''); 82 | console.log('Bytes'.padStart(16), ' ', 'Count'.padStart(14), ' ', 83 | (opts.aggregateBy ? ('Event Name'.padEnd(35) + ' (cat)') : 'Category')); 84 | 85 | const kbfmt = new Intl.NumberFormat('en', { 86 | style: 'unit', unit: 'kilobyte', unitDisplay: 'short', 87 | minimumFractionDigits: 0, maximumFractionDigits: 0, 88 | minimumSignificantDigits: 1, maximumSignificantDigits: 3 89 | }); 90 | const toKb = bytes => kbfmt.format(bytes / 1024); 91 | 92 | const percentfmt = new Intl.NumberFormat('en', { 93 | maximumFractionDigits: 1, minimumFractionDigits: 1, 94 | }); 95 | 96 | const skipped = {bytes: 0, count: 0}; 97 | traceTotals.sort((a, b) => b.bytes - a.bytes).forEach((tot, i) => { // sort by bytes.. can change to sort by eventCount here instead. 98 | const bytesPct = tot.bytes * 100 / totalBytes; 99 | if (bytesPct < 1) { 100 | skipped.bytes += tot.bytes; 101 | skipped.count += tot.count; 102 | return; // dont output. 103 | } 104 | 105 | console.log( 106 | toKb(tot.bytes).padStart(9), 107 | `${percentfmt.format(bytesPct)}%`.padStart(6), 108 | ' ', 109 | tot.count.toLocaleString().padStart(7), 110 | `${percentfmt.format(tot.count * 100 / totalEvents)}%`.padStart(6), 111 | ' ', 112 | tot.name 113 | ); 114 | }); 115 | 116 | // skipped 117 | console.log( 118 | toKb(skipped.bytes).padStart(9), 119 | `${percentfmt.format( skipped.bytes * 100 / totalBytes)}%`.padStart(6), 120 | ' ', 121 | skipped.count.toLocaleString().padStart(7), 122 | `${percentfmt.format(skipped.count * 100 / totalEvents)}%`.padStart(6), 123 | ' ', 124 | '[(Rows that were < 1% of bytes)]' 125 | ); 126 | 127 | // phase counts 128 | // if (!opts.aggregateBy) { 129 | // console.log('\n Phases:') 130 | // Object.entries(tracePhs).sort((a, b) => b[1] - a[1]).forEach(([ph, count]) => console.log(`ph(${ph}): ${count.toLocaleString()}`) ); 131 | // // console.log({tracePhs}); 132 | // } 133 | } 134 | -------------------------------------------------------------------------------- /generic-trace-to-devtools-trace.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview 3 | * This "upgrades" a generic trace file into a DevTools-style trace file. 4 | * 5 | * "Generic" traces, like those from WebPageTest, about:tracing, or perfetto (as JSON) are 6 | * captured at the browser level, across all renderer processes. In contrast, a 7 | * trace recorded within DevTools is captured starting from the specific 8 | * renderer process of the inspected page. 9 | * 10 | * This fundamental difference leads to subtle but important distinctions in the 11 | * trace data and its presentation in the DevTools Performance panel. For example, 12 | * generic traces (see `isGenericTrace` in DevTools source) will label renderer 13 | * threads as "Renderer (####)" while DevTools traces, being renderer-specific, 14 | * can provide a cleaner view. 15 | * 16 | * This script hacks the generic trace JSON to mimic a first-class DevTools-native trace. 17 | * The goal is to provide a better, more familiar user experience for developers 18 | * analyzing WPT traces (and other generic traces) inside DevTools. 19 | * 20 | * Before/after screenshots: https://imgur.com/a/97SUkui 21 | * 22 | * Usage: 23 | * 24 | * node generic-trace-to-devtools-trace.mjs somefile.json 25 | */ 26 | 27 | 28 | import {loadTraceEventsFromFile, saveTrace} from './trace-file-utils.mjs'; 29 | 30 | const passedArg = process.argv[2]; 31 | const tracefilename = passedArg ? passedArg : './myjansatta.json'; 32 | 33 | /** @type {TraceEvent[]} */ 34 | const events = loadTraceEventsFromFile(tracefilename); 35 | console.log(events.length); 36 | 37 | let minTs = Infinity; 38 | const threads = []; 39 | const pidToThreadLookup = {}; 40 | const eventToThread = new Map(); 41 | 42 | class Thread { 43 | constructor(pid, tid, thread_name) { 44 | this.pid = pid; 45 | this.tid = tid; 46 | this.thread_name = thread_name; 47 | 48 | this.events = []; 49 | this.label; 50 | this.process_name; 51 | 52 | threads.push(this); 53 | pidToThreadLookup[pid] = pidToThreadLookup[pid] ?? {}; 54 | pidToThreadLookup[pid][tid] = this; 55 | } 56 | } 57 | 58 | // metadataevents are always at the start 59 | for (const e of events.slice(0, 1000)) { 60 | if (e.ph === 'M' && e.name === 'thread_name') { 61 | const thread = 62 | threads.find(t => t.tid === e.tid && t.pid === e.pid) ?? 63 | new Thread(e.pid, e.tid, e.args.name); 64 | eventToThread.set(e, thread); 65 | } 66 | if (e.ph === 'M' && e.name === 'process_name') { 67 | threads.filter(t => t.pid === e.pid).forEach(t => (t.process_name = e.args.name)); 68 | } 69 | } 70 | 71 | // Loop over all. assign events to threads. collect minTs 72 | for (const e of events) { 73 | if (e.ph === 'M' && e.name === 'process_labels') { 74 | const thread = pidToThreadLookup[e.pid][e.tid]; 75 | thread.label = e.args.labels; 76 | } 77 | if (e.ph === 'M') continue; // dont attempt with metadata 78 | const thread = pidToThreadLookup[e.pid][e.tid]; 79 | if (!thread){ 80 | // This happens with the `AnyPageLoading` event. Why? dunno. https://source.chromium.org/chromium/chromium/src/+/main:components/performance_manager/scenarios/browser_performance_scenarios.cc;l=41-54;drc=75a8035abe03764596f30424030465636e82aa70 81 | console.error(`no thread for ${e.pid} ${e.tid}`, e); 82 | continue; 83 | } 84 | thread.events.push(e); 85 | 86 | if (e.ts !== 0) { 87 | minTs = Math.min(minTs, e.ts); 88 | } 89 | } 90 | 91 | // Find busiest main thread and frame 92 | const rendererMains = threads.filter( 93 | t => t.thread_name === 'CrRendererMain' && t.process_name === 'Renderer' 94 | ); 95 | rendererMains.sort((aThread, bThread) => bThread.events.length - aThread.events.length); 96 | const busiestMainThread = rendererMains.at(0); 97 | 98 | const frameToCount = new Map(); 99 | busiestMainThread.events.forEach(e => { 100 | const frame = e.args.frame ?? e.args.data?.frame; 101 | if (!frame || typeof frame !== 'string') return; 102 | 103 | let count = frameToCount.get(frame) ?? 0; 104 | count++; 105 | frameToCount.set(frame, count); 106 | }); 107 | 108 | const busiestFrame = Array.from(frameToCount.entries()) 109 | .sort(([aFrame, aCount], [bBrame, bCount]) => bCount - aCount) 110 | .at(0); 111 | const busiestFrameId = busiestFrame?.at(0) ?? 'NOFRAMEIDFOUND'; 112 | 113 | 114 | /** 115 | * @return {LH.TraceEvent} 116 | */ 117 | function createFakeTracingStartedInPage() { 118 | // TODO: migrate to TracingStartedInBrowser cuz this one is old. 119 | return { 120 | pid: busiestMainThread.pid, 121 | tid: busiestMainThread.tid, 122 | cat: 'devtools.timeline', 123 | ts: minTs, 124 | ph: 'I', 125 | s: 't', 126 | cat: 'disabled-by-default-devtools.timeline', 127 | name: 'TracingStartedInPage', 128 | args: { 129 | data: { 130 | frameTreeNodeId: 1, 131 | page: busiestFrameId, 132 | persistentIds: true, 133 | frames: [ 134 | {frame: busiestFrameId, url: 'https://sdflkdsf.com', name: busiestMainThread.label, processId: busiestMainThread.pid}, 135 | ], 136 | }, 137 | }, 138 | dur: 0, 139 | }; 140 | } 141 | 142 | // startedinpage is LEGACY behavior but... i need it right now cuz the inbrowser path aint working. and im too lazy to figure out why 143 | events.push(createFakeTracingStartedInPage()); 144 | // events.push({...createFakeTracingStartedInPage(), name: 'TracingStartedInBrowser'}); 145 | // events.sort((a, b) => a.ts - b.ts); // apparently this still works even with startedinpage is at the end. 146 | 147 | await saveTrace({traceEvents: events}, `${tracefilename}.dt.json`); 148 | 149 | console.log('done'); -------------------------------------------------------------------------------- /trace-file-utils.mjs: -------------------------------------------------------------------------------- 1 | // pulled from DT FE savefileformatter.ts, which was based on lighthouse's asset-saver. 2 | 3 | import stream from 'stream'; 4 | import fs from 'fs'; 5 | import zlib from 'zlib'; 6 | import {strict as assert} from 'assert'; 7 | 8 | /** 9 | * Generates a JSON representation of an array of objects with the objects 10 | * printed one per line for a more readable (but not too verbose) version. 11 | * @param {Array} arrayOfObjects 12 | * @return {IterableIterator} 13 | */ 14 | function* arrayOfObjectsJsonGenerator(arrayOfObjects) { 15 | const ITEMS_PER_ITERATION = 10_000; 16 | 17 | // Stringify and emit items separately to avoid a giant string in memory. 18 | yield '[\n'; 19 | if (arrayOfObjects.length > 0) { 20 | const itemsIterator = arrayOfObjects[Symbol.iterator](); 21 | // Emit first item manually to avoid a trailing comma. 22 | const firstItem = itemsIterator.next().value; 23 | yield ` ${JSON.stringify(firstItem)}`; 24 | 25 | let itemsRemaining = ITEMS_PER_ITERATION; 26 | let itemsJSON = ''; 27 | for (const item of itemsIterator) { 28 | itemsJSON += `,\n ${JSON.stringify(item)}`; 29 | itemsRemaining--; 30 | if (itemsRemaining === 0) { 31 | yield itemsJSON; 32 | itemsRemaining = ITEMS_PER_ITERATION; 33 | itemsJSON = ''; 34 | } 35 | } 36 | yield itemsJSON; 37 | } 38 | yield '\n]'; 39 | } 40 | 41 | /** 42 | * Generates a JSON representation of trace line-by-line for a nicer printed 43 | * version with one trace event per line. 44 | * @param {readonly TraceEngine.Types.TraceEvents.TraceEventData[]} traceEvents 45 | * @param {Readonly|null} metadata 46 | * @return IterableIterator 47 | */ 48 | export function* traceJsonGenerator(trace) { 49 | const {traceEvents, metadata, ...rest} = trace; 50 | if (Object.keys(rest).length) throw new Error('unexpected contents in tracefile. not traceEvents or metadata! : ' + JSON.stringify(rest).slice(0, 1000)); 51 | 52 | yield '{"traceEvents": '; 53 | yield* arrayOfObjectsJsonGenerator(traceEvents); 54 | if (metadata) { 55 | yield `,\n"metadata": ${JSON.stringify(metadata, null, 2)}`; 56 | } 57 | yield '}\n'; 58 | } 59 | 60 | /** 61 | * Save a trace as JSON by streaming to disk at traceFilename. 62 | * @param {LH.Trace} trace 63 | * @param {string} traceFilename 64 | * @return {Promise} 65 | */ 66 | export async function saveTrace(trace, traceFilename) { 67 | const traceIter = traceJsonGenerator(trace); 68 | const writeStream = fs.createWriteStream(traceFilename); 69 | 70 | return stream.promises.pipeline(traceIter, writeStream); 71 | } 72 | 73 | /** 74 | * Save a devtoolsLog as JSON by streaming to disk at devtoolLogFilename. 75 | * @param {any} profile 76 | * @param {string} cpuProfileFilename 77 | * @return {Promise} 78 | */ 79 | export function saveCpuProfile(profile, cpuProfileFilename) { 80 | const writeStream = fs.createWriteStream(cpuProfileFilename); 81 | 82 | return stream.promises.pipeline(function* () { 83 | yield '{\n'; 84 | 85 | for (const [key, val] of Object.entries(profile)) { 86 | if (key === 'nodes') { // i dont know ideal formatting for samples and timeDeltas 87 | // this relies on nodes always being first.. 88 | yield `"${key}": `; 89 | yield* arrayOfObjectsJsonGenerator(val); 90 | } else { 91 | yield `,\n"${key}": `; 92 | yield JSON.stringify(val); 93 | } 94 | } 95 | 96 | yield '\n}\n'; 97 | }, writeStream); 98 | } 99 | 100 | 101 | /** 102 | * @return {Promise} 103 | * @param {string} filename 104 | */ 105 | export async function saveNetlog(netlog, filename) { 106 | const writeStream = fs.createWriteStream(filename); 107 | 108 | const {events, constants, ...rest} = netlog; 109 | if (Object.keys(rest).length) throw new Error('unexpected contents in netlog! : ' + JSON.stringify(rest).slice(0, 1000)); 110 | 111 | return stream.promises.pipeline(function* () { 112 | yield '{\n'; 113 | yield `"constants": ${JSON.stringify(constants, null, 2)}`; 114 | yield ',\n"events": '; 115 | yield* arrayOfObjectsJsonGenerator(events); 116 | yield '}\n'; 117 | }, writeStream); 118 | } 119 | 120 | 121 | 122 | /** 123 | * A simple version of LH's test-util's readJson. TBD if it needs more import.meta complexity. 124 | * 125 | * @deprecated use `loadTraceEventsFromFile` instead. 126 | * @param {string} filePath Can be an absolute or relative path. 127 | */ 128 | export function readJson(filePath) { 129 | // filePath = path.resolve(dir, filePath); 130 | return JSON.parse(fs.readFileSync(filePath, 'utf-8')); 131 | } 132 | 133 | /** 134 | * @param {string=} filename 135 | * @returns TraceEvent[] 136 | */ 137 | export function loadTraceEventsFromFile(filename) { 138 | if (!fs.existsSync(filename)) { 139 | throw new Error('File not found. ' + filename); 140 | } 141 | let fileBuf = fs.readFileSync(filename); 142 | let data; 143 | if (isGzip(fileBuf)) { 144 | data = zlib.gunzipSync(fileBuf); 145 | } else { 146 | data = fileBuf.toString('utf8'); 147 | } 148 | const json = JSON.parse(data); 149 | // clear memory 150 | fileBuf = data = ''; 151 | const traceEvents = json.traceEvents ?? json; 152 | assert.ok(Array.isArray(traceEvents) && traceEvents.length, 'No trace events array'); 153 | // TODO, do something less gross. 154 | traceEvents.metadata = json.metadata; 155 | return traceEvents; 156 | } 157 | 158 | /** 159 | * Read the first 3 bytes looking for the gzip signature in the file header 160 | * https://www.rfc-editor.org/rfc/rfc1952#page-6 161 | * @param {ArrayBuffer} ab 162 | * @returns boolean 163 | */ 164 | function isGzip(ab) { 165 | const buf = new Uint8Array(ab); 166 | if (!buf || buf.length < 3) { 167 | return false; 168 | } 169 | return buf[0] === 0x1F && buf[1] === 0x8B && buf[2] === 0x08; 170 | } 171 | 172 | -------------------------------------------------------------------------------- /third_party/proto-converter/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "proto-converter", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "devDependencies": { 8 | "@types/node": "^11.11.4", 9 | "protobufjs": "^6.8.8", 10 | "typescript": "^3.3.4000" 11 | } 12 | }, 13 | "node_modules/@protobufjs/aspromise": { 14 | "version": "1.1.2", 15 | "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", 16 | "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=", 17 | "dev": true 18 | }, 19 | "node_modules/@protobufjs/base64": { 20 | "version": "1.1.2", 21 | "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", 22 | "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", 23 | "dev": true 24 | }, 25 | "node_modules/@protobufjs/codegen": { 26 | "version": "2.0.4", 27 | "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", 28 | "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", 29 | "dev": true 30 | }, 31 | "node_modules/@protobufjs/eventemitter": { 32 | "version": "1.1.0", 33 | "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", 34 | "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=", 35 | "dev": true 36 | }, 37 | "node_modules/@protobufjs/fetch": { 38 | "version": "1.1.0", 39 | "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", 40 | "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", 41 | "dev": true, 42 | "dependencies": { 43 | "@protobufjs/aspromise": "^1.1.1", 44 | "@protobufjs/inquire": "^1.1.0" 45 | } 46 | }, 47 | "node_modules/@protobufjs/float": { 48 | "version": "1.0.2", 49 | "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", 50 | "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=", 51 | "dev": true 52 | }, 53 | "node_modules/@protobufjs/inquire": { 54 | "version": "1.1.0", 55 | "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", 56 | "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=", 57 | "dev": true 58 | }, 59 | "node_modules/@protobufjs/path": { 60 | "version": "1.1.2", 61 | "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", 62 | "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=", 63 | "dev": true 64 | }, 65 | "node_modules/@protobufjs/pool": { 66 | "version": "1.1.0", 67 | "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", 68 | "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=", 69 | "dev": true 70 | }, 71 | "node_modules/@protobufjs/utf8": { 72 | "version": "1.1.0", 73 | "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", 74 | "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=", 75 | "dev": true 76 | }, 77 | "node_modules/@types/long": { 78 | "version": "4.0.0", 79 | "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.0.tgz", 80 | "integrity": "sha512-1w52Nyx4Gq47uuu0EVcsHBxZFJgurQ+rTKS3qMHxR1GY2T8c2AJYd6vZoZ9q1rupaDjU0yT+Jc2XTyXkjeMA+Q==", 81 | "dev": true 82 | }, 83 | "node_modules/@types/node": { 84 | "version": "11.11.4", 85 | "resolved": "https://registry.npmjs.org/@types/node/-/node-11.11.4.tgz", 86 | "integrity": "sha512-02tIL+QIi/RW4E5xILdoAMjeJ9kYq5t5S2vciUdFPXv/ikFTb0zK8q9vXkg4+WAJuYXGiVT1H28AkD2C+IkXVw==", 87 | "dev": true 88 | }, 89 | "node_modules/long": { 90 | "version": "4.0.0", 91 | "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", 92 | "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", 93 | "dev": true 94 | }, 95 | "node_modules/protobufjs": { 96 | "version": "6.8.8", 97 | "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.8.8.tgz", 98 | "integrity": "sha512-AAmHtD5pXgZfi7GMpllpO3q1Xw1OYldr+dMUlAnffGTAhqkg72WdmSY71uKBF/JuyiKs8psYbtKrhi0ASCD8qw==", 99 | "dev": true, 100 | "hasInstallScript": true, 101 | "dependencies": { 102 | "@protobufjs/aspromise": "^1.1.2", 103 | "@protobufjs/base64": "^1.1.2", 104 | "@protobufjs/codegen": "^2.0.4", 105 | "@protobufjs/eventemitter": "^1.1.0", 106 | "@protobufjs/fetch": "^1.1.0", 107 | "@protobufjs/float": "^1.0.2", 108 | "@protobufjs/inquire": "^1.1.0", 109 | "@protobufjs/path": "^1.1.2", 110 | "@protobufjs/pool": "^1.1.0", 111 | "@protobufjs/utf8": "^1.1.0", 112 | "@types/long": "^4.0.0", 113 | "@types/node": "^10.1.0", 114 | "long": "^4.0.0" 115 | }, 116 | "bin": { 117 | "pbjs": "bin/pbjs", 118 | "pbts": "bin/pbts" 119 | } 120 | }, 121 | "node_modules/protobufjs/node_modules/@types/node": { 122 | "version": "10.14.1", 123 | "resolved": "https://registry.npmjs.org/@types/node/-/node-10.14.1.tgz", 124 | "integrity": "sha512-Rymt08vh1GaW4vYB6QP61/5m/CFLGnFZP++bJpWbiNxceNa6RBipDmb413jvtSf/R1gg5a/jQVl2jY4XVRscEA==", 125 | "dev": true 126 | }, 127 | "node_modules/typescript": { 128 | "version": "3.3.4000", 129 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.3.4000.tgz", 130 | "integrity": "sha512-jjOcCZvpkl2+z7JFn0yBOoLQyLoIkNZAs/fYJkUG6VKy6zLPHJGfQJYFHzibB6GJaF/8QrcECtlQ5cpvRHSMEA==", 131 | "dev": true, 132 | "bin": { 133 | "tsc": "bin/tsc", 134 | "tsserver": "bin/tsserver" 135 | }, 136 | "engines": { 137 | "node": ">=4.2.0" 138 | } 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /third_party/proto-converter/proto-to-json.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2019 the V8 project authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | // From https://chromium-review.googlesource.com/c/v8/v8/+/1535823 6 | /** 7 | * TRACE_STUFF NOTES 8 | * 9 | * 1. use node 23+ to run this .ts without compiling. https://nodejs.org/en/learn/typescript/run-natively 10 | * 1. open ui.perfetto.dev. load chrome example. hit download. 11 | * 1. node proto-to-json.ts $HOME/chromium/src/third_party/perfetto/protos/perfetto/trace/trace.proto $HOME/Downloads/chrome_example_wikipedia.perfetto_trace.gz ./out-converted.json 12 | 13 | * Something dumb about proto path resolving.. i need an edit in `node_modules/protobufjs/src/root.js`. add this line within the `fetch` function. L128: 14 | filename = filename.replace('protos/protos', 'protos') 15 | 16 | This fails on a newly captured trace, which doesn't make sense since we're refererencing ToT protos. 17 | Much of the logic is definitely outdated and it'll take work to nurse it back to something useful. 18 | 19 | * 20 | * */ 21 | 22 | import * as fs from 'fs'; 23 | import * as path from 'path'; 24 | import pkg from 'protobufjs'; 25 | const {Root} = pkg; 26 | import {saveTrace} from '../../trace-file-utils.mjs'; 27 | 28 | // Usage: node proto-to-json.ts path_to_trace.proto input_file output_file 29 | 30 | // Converts a binary proto file to a 'Trace Event Format' compatible .json file 31 | // that can be used with chrome://tracing. Documentation of this format: 32 | // https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU 33 | 34 | // Attempts to reproduce the logic of the JSONTraceWriter in V8 in terms of the 35 | // JSON fields it will include/exclude based on the data present in the trace 36 | // event. 37 | 38 | // Convert a string representing an int or uint (64 bit) to a Number or throw 39 | // if the value won't fit. 40 | function parseIntOrThrow(int: string) { 41 | if (BigInt(int) > Number.MAX_SAFE_INTEGER) { 42 | throw new Error('Loss of int precision'); 43 | } 44 | return Number(int); 45 | } 46 | 47 | function uint64AsHexString(val: string): string { 48 | return '0x' + BigInt(val).toString(16); 49 | } 50 | 51 | function parseArgValue(arg: any): any { 52 | if (arg.jsonValue) { 53 | return JSON.parse(arg.jsonValue); 54 | } 55 | if (typeof arg.stringValue !== 'undefined') { 56 | return arg.stringValue; 57 | } 58 | if (typeof arg.uintValue !== 'undefined') { 59 | return parseIntOrThrow(arg.uintValue); 60 | } 61 | if (typeof arg.intValue !== 'undefined') { 62 | return parseIntOrThrow(arg.intValue); 63 | } 64 | if (typeof arg.boolValue !== 'undefined') { 65 | return arg.boolValue; 66 | } 67 | if (typeof arg.doubleValue !== 'undefined') { 68 | // Handle [-]Infinity and NaN which protobufjs outputs as strings here. 69 | return typeof arg.doubleValue === 'string' ? arg.doubleValue : Number(arg.doubleValue); 70 | } 71 | if (typeof arg.pointerValue !== 'undefined') { 72 | return uint64AsHexString(arg.pointerValue); 73 | } 74 | } 75 | 76 | // These come from 77 | // https://cs.chromium.org/chromium/src/base/trace_event/common/trace_event_common.h 78 | const TRACE_EVENT_FLAG_HAS_ID: number = 1 << 1; 79 | const TRACE_EVENT_FLAG_FLOW_IN: number = 1 << 8; 80 | const TRACE_EVENT_FLAG_FLOW_OUT: number = 1 << 9; 81 | 82 | async function main() { 83 | const root = new Root(); 84 | const {resolvePath} = root; 85 | const numDirectoriesToStrip = 2; 86 | let initialOrigin: string | null; 87 | root.resolvePath = (origin, target) => { 88 | if (!origin) { 89 | initialOrigin = target; 90 | for (let i = 0; i <= numDirectoriesToStrip; i++) { 91 | initialOrigin = path.dirname(initialOrigin); 92 | } 93 | return resolvePath(origin, target); 94 | } 95 | return path.resolve(initialOrigin!, target); 96 | }; 97 | const traceProto = await root.load(process.argv[2]); 98 | const Trace = traceProto.lookupType('Trace'); 99 | const payload = await fs.promises.readFile(process.argv[3]); 100 | const msg = Trace.decode(payload).toJSON(); 101 | 102 | const toJSONEvent = (e: any) => { 103 | const bind_id = e.flags & (TRACE_EVENT_FLAG_FLOW_IN | TRACE_EVENT_FLAG_FLOW_OUT) ? e.bindId : undefined; 104 | const scope = e.flags & TRACE_EVENT_FLAG_HAS_ID && e.scope ? e.scope : undefined; 105 | 106 | return { 107 | pid: e.trustedPid, 108 | tid: e.trustedUid, 109 | ts: parseIntOrThrow(e.timestamp), 110 | // tts: parseIntOrThrow(e.threadTimestamp), 111 | ph: String.fromCodePoint(e.phase), 112 | cat: e.categoryGroupName, 113 | name: e.name, 114 | dur: parseIntOrThrow(e.duration), 115 | tdur: parseIntOrThrow(e.threadDuration), 116 | bind_id: bind_id, 117 | flow_in: e.flags & TRACE_EVENT_FLAG_FLOW_IN ? true : undefined, 118 | flow_out: e.flags & TRACE_EVENT_FLAG_FLOW_OUT ? true : undefined, 119 | scope: scope, 120 | id: e.flags & TRACE_EVENT_FLAG_HAS_ID ? uint64AsHexString(e.id) : undefined, 121 | args: (e.args || []).reduce((js_args: any, proto_arg: any) => { 122 | js_args[proto_arg.name] = parseArgValue(proto_arg); 123 | return js_args; 124 | }, {}), 125 | }; 126 | }; 127 | 128 | debugger; 129 | 130 | // const chromeEventsPkts = msg.packet.filter((packet: any) => !!packet.chromeEvents); 131 | const trackEventPkts = msg.packet.filter((packet: any) => !!packet.trackEvent); 132 | 133 | // TODO: do something with msg.packet.filter((packet: any) => !!packet.chromeEvents).map(e => e.chromeEvents.metadata) 134 | // TODO: maybe there's something hiding in chromeEvents.traceEvents but i think not. 135 | 136 | const traceEvents = trackEventPkts.map(toJSONEvent).flat(); 137 | 138 | const output = { 139 | traceEvents, 140 | }; 141 | 142 | await saveTrace(output, process.argv[4]); 143 | // await fs.promises.writeFile(process.argv[4], JSON.stringify(output, null, 2)); 144 | } 145 | 146 | main().catch(console.error); 147 | -------------------------------------------------------------------------------- /user-timings-to-trace.mjs: -------------------------------------------------------------------------------- 1 | // Take some user timings (performance.measure/mark) and generate a trace for visualization. 2 | // Perfect if you instrment in Node.js with performance mark()/measure(), or the NPM marky package. 3 | // Run like: 4 | // node user-timings-to-trace.mjs user-timings.json 5 | // 6 | // Most of this file is from Lighthouse: https://github.com/GoogleChrome/lighthouse/blob/0da3e1d85d1920e3e75e423e6f905ddf4bd8fd53/core/lib/timing-trace-saver.js 7 | // But I've adapted it to be solo and modernized it a tad. ~Paul. 2024-10 8 | /** 9 | * @license 10 | * Copyright 2018 Google LLC 11 | * SPDX-License-Identifier: Apache-2.0 12 | */ 13 | 14 | import fs from 'fs'; 15 | import path from 'path'; 16 | 17 | /** @typedef {import('./types/chromium-trace').TraceEvent} TraceEvent */ 18 | 19 | /** 20 | * Generates a Chrome trace file from user timing measures 21 | * 22 | * Originally adapted from https://github.com/tdresser/performance-observer-tracing 23 | * 24 | * @param {PerformanceEntryList} entries user timing entries 25 | * @param {number=} threadId Can be provided to separate a series of trace events into another thread, useful if timings do not share the same timeOrigin, but should both be "left-aligned". 26 | */ 27 | export function generateTraceEvents(entries, threadId = 8) { 28 | entries.sort((a, b) => a.startTime - b.startTime); 29 | 30 | /** @type {TraceEvent[]} */ 31 | const currentTrace = []; 32 | const baseEvt = { 33 | pid: 7, 34 | tid: threadId, 35 | args: {}, 36 | // Hack: Use one to avoid some mishandling in the devtools of valid trace events 37 | // with a ts of 0. We should fix the bug there, but making this a microsecond off 38 | // seems an okay tradeoff. 39 | ts: 1, 40 | name: '', 41 | }; 42 | 43 | const frameData = { 44 | processId: baseEvt.pid, 45 | frame: '_frameid_', 46 | name: '_frame_name_', 47 | url: '', 48 | navigationId: '_navid_', 49 | }; 50 | 51 | function addBaselineTraceEvents() { 52 | /** @type {TraceEvent} */ 53 | const metaEvtBase = { 54 | ...baseEvt, 55 | cat: '__metadata', 56 | ph: 'M', 57 | }; 58 | 59 | currentTrace.push({ 60 | ...metaEvtBase, 61 | name: 'process_labels', 62 | args: {labels: 'User Timing'}, 63 | }); 64 | 65 | currentTrace.push({ 66 | ...metaEvtBase, 67 | name: 'thread_name', 68 | args: { 69 | name: 'CrRendererMain', 70 | }, 71 | }); 72 | 73 | currentTrace.push({ 74 | ...metaEvtBase, 75 | name: 'process_name', 76 | args: { 77 | name: 'Renderer', 78 | }, 79 | }); 80 | 81 | threadId === 8 && 82 | currentTrace.push({ 83 | ...metaEvtBase, 84 | cat: 'disabled-by-default-devtools.timeline', 85 | name: 'TracingStartedInBrowser', 86 | ph: 'I', 87 | // s: 't', 88 | args: { 89 | data: { 90 | frameTreeNodeId: 1, 91 | persistentIds: true, 92 | frames: [frameData], 93 | }, 94 | }, 95 | }); 96 | 97 | threadId === 8 && 98 | currentTrace.push({ 99 | ...metaEvtBase, 100 | cat: 'disabled-by-default-devtools.timeline', 101 | name: 'FrameCommittedInBrowser', 102 | ph: 'I', 103 | args: { 104 | data: frameData, 105 | }, 106 | }); 107 | } 108 | addBaselineTraceEvents(); 109 | 110 | entries.forEach((entry, i) => { 111 | if (entry.entryType === 'mark') { 112 | const markEvt = { 113 | ...baseEvt, 114 | name: entry.name, 115 | cat: 'blink.user_timing', 116 | ts: entry.startTime * 1000, 117 | ph: 'I', 118 | }; 119 | return currentTrace.push(markEvt); 120 | } 121 | 122 | /** @type {TraceEvent} */ 123 | const measureBeginEvt = { 124 | ...baseEvt, 125 | // FYI Colons in user_timing names get special handling in about:tracing you may not want. https://github.com/catapult-project/catapult/blob/b026043a43f9ce3f37c1cd57269f92cb8bee756c/tracing/tracing/extras/importer/trace_event_importer.html#L1643-L1654 126 | // But no adjustments made here. 127 | name: entry.name, 128 | cat: 'blink.user_timing', 129 | ts: entry.startTime * 1000, 130 | id2: {local: '0x' + (i + 1).toString(16)}, 131 | ph: 'b', 132 | }; 133 | 134 | const measureEndEvt = { 135 | ...measureBeginEvt, 136 | ph: 'e', 137 | ts: measureBeginEvt.ts + entry.duration * 1000, 138 | }; 139 | 140 | currentTrace.push(measureBeginEvt); 141 | currentTrace.push(measureEndEvt); 142 | }); 143 | 144 | // DevTools likes to calculate trace bounds with 'real' events. 145 | // We'll give it a little breathing room for more enjoyable UI. 146 | const firstTs = (entries.at(0)?.startTime ?? 0) * 1000; 147 | const lastTs = currentTrace.at(-1)?.ts ?? currentTrace.reduce((acc, e) => (e.ts + (e.dur ?? 0) > acc ? e.ts + (e.dur ?? 0) : acc), 0); 148 | const finalTs = 2.1 * lastTs - firstTs; 149 | const zeroEvent = { 150 | ...baseEvt, 151 | name: 'RunTask', 152 | cat: 'disabled-by-default-devtools.timeline', 153 | ph: 'X', 154 | ts: firstTs * 0.9, 155 | dur: 2, 156 | }; 157 | const finalEvent = { 158 | ...zeroEvent, 159 | ts: finalTs, 160 | }; 161 | currentTrace.push(zeroEvent); 162 | currentTrace.push(finalEvent); 163 | 164 | return currentTrace; 165 | } 166 | 167 | /** 168 | * Writes a trace file to disk 169 | * @param {PerformanceEntryList} entries 170 | * @return {string} 171 | */ 172 | export function createTraceString(entries) { 173 | if (!Array.isArray(entries) || !entries[0].entryType) { 174 | throw new Error('This doesnt look like measures/marks'); 175 | } 176 | 177 | const traceEvents = generateTraceEvents(entries); 178 | 179 | const jsonStr = `{"traceEvents":[ 180 | ${traceEvents.map(evt => JSON.stringify(evt)).join(',\n')} 181 | ]}`; 182 | return jsonStr; 183 | } 184 | 185 | // CLI direct invocation? 186 | if (import.meta.url.endsWith(process.argv[1])) { 187 | cli(); 188 | } 189 | async function cli() { 190 | const filename = process.argv[2] && path.resolve(process.cwd(), process.argv[2]); 191 | if (!filename || !fs.existsSync(filename)) { 192 | throw new Error(`File not found: ${filename}`); 193 | } 194 | 195 | const mark = performance.mark.bind(performance); 196 | mark('1'); 197 | const entries = JSON.parse(fs.readFileSync(filename, 'utf8')); 198 | mark('2'); 199 | const jsonStr = createTraceString(entries); 200 | mark('3'); 201 | 202 | const pathObj = path.parse(filename); 203 | const traceFilePath = path.join(pathObj.dir, `${pathObj.name}.trace.json`); 204 | fs.writeFileSync(traceFilePath, jsonStr, 'utf8'); 205 | mark('4'); 206 | 207 | if (process.env.TIMING) { 208 | performance.measure('all', '1', '4'); 209 | performance.measure('read json', '1', '2'); 210 | performance.measure('craft trace', '2', '3'); 211 | performance.measure('write file', '3', '4'); 212 | fs.writeFileSync('./selftimings.json', JSON.stringify(performance.getEntries()), 'utf8'); 213 | } 214 | 215 | console.log(` 216 | > Timing trace saved to: ${traceFilePath} 217 | > View in DevTools perf panel, https://ui.perfetto.dev or https://trace.cafe 218 | `); 219 | } 220 | -------------------------------------------------------------------------------- /third_party/pwmetrics.events.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Copyright 2017 The Lighthouse Authors. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 5 | */ 6 | 'use strict'; 7 | 8 | const log = require('lighthouse-logger'); 9 | const TraceProcessor = require('../tracehouse/trace-processor.js'); 10 | 11 | /** 12 | * @param {LH.Result['audits']} auditResults 13 | * @return {LH.Artifacts.TimingSummary|undefined} 14 | */ 15 | function getUberMetrics(auditResults) { 16 | const metricsAudit = auditResults.metrics; 17 | if (!metricsAudit || !metricsAudit.details || !('items' in metricsAudit.details)) return; 18 | 19 | return metricsAudit.details.items[0]; 20 | } 21 | 22 | class Metrics { 23 | /** 24 | * @param {Array} traceEvents 25 | * @param {LH.Result['audits']} auditResults 26 | */ 27 | constructor(traceEvents, auditResults) { 28 | this._traceEvents = traceEvents; 29 | this._auditResults = auditResults; 30 | } 31 | 32 | /** 33 | * Returns simplified representation of all metrics 34 | * @return {Array<{id: string, name: string, tsKey: keyof LH.Artifacts.TimingSummary}>} metrics to consider 35 | */ 36 | static get metricsDefinitions() { 37 | return [ 38 | { 39 | name: 'Time Origin', 40 | id: 'timeorigin', 41 | tsKey: 'observedTimeOriginTs', 42 | }, 43 | { 44 | name: 'First Contentful Paint', 45 | id: 'ttfcp', 46 | tsKey: 'observedFirstContentfulPaintTs', 47 | }, 48 | { 49 | name: 'First Meaningful Paint', 50 | id: 'ttfmp', 51 | tsKey: 'observedFirstMeaningfulPaintTs', 52 | }, 53 | { 54 | name: 'Speed Index', 55 | id: 'si', 56 | tsKey: 'observedSpeedIndexTs', 57 | }, 58 | { 59 | name: 'First Visual Change', 60 | id: 'fv', 61 | tsKey: 'observedFirstVisualChangeTs', 62 | }, 63 | { 64 | name: 'Visually Complete 100%', 65 | id: 'vc100', 66 | tsKey: 'observedLastVisualChangeTs', 67 | }, 68 | { 69 | name: 'Interactive', 70 | id: 'tti', 71 | tsKey: 'interactiveTs', 72 | }, 73 | { 74 | name: 'End of Trace', 75 | id: 'eot', 76 | tsKey: 'observedTraceEndTs', 77 | }, 78 | { 79 | name: 'On Load', 80 | id: 'onload', 81 | tsKey: 'observedLoadTs', 82 | }, 83 | { 84 | name: 'DOM Content Loaded', 85 | id: 'dcl', 86 | tsKey: 'observedDomContentLoadedTs', 87 | }, 88 | ]; 89 | } 90 | 91 | /** 92 | * Returns simplified representation of all metrics' timestamps from monotonic clock 93 | * @return {Array<{ts: number, id: string, name: string}>} metrics to consider 94 | */ 95 | gatherMetrics() { 96 | const uberMetrics = getUberMetrics(this._auditResults); 97 | if (!uberMetrics) { 98 | return []; 99 | } 100 | 101 | /** @type {Array<{ts: number, id: string, name: string}>} */ 102 | const resolvedMetrics = []; 103 | Metrics.metricsDefinitions.forEach(metric => { 104 | // Skip if auditResults is missing a particular audit result 105 | const ts = uberMetrics[metric.tsKey]; 106 | if (ts === undefined) { 107 | log.error('pwmetrics-events', `${metric.name} timestamp not found`); 108 | return; 109 | } 110 | 111 | resolvedMetrics.push({ 112 | id: metric.id, 113 | name: metric.name, 114 | ts, 115 | }); 116 | }); 117 | 118 | return resolvedMetrics; 119 | } 120 | 121 | /** 122 | * Get the trace event data for our timeOrigin 123 | * @param {Array<{ts: number, id: string, name: string}>} metrics 124 | * @return {{pid: number, tid: number, ts: number} | {errorMessage: string}} 125 | */ 126 | getTimeOriginEvt(metrics) { 127 | const timeOriginMetric = metrics.find(e => e.id === 'timeorigin'); 128 | if (!timeOriginMetric) return {errorMessage: 'timeorigin Metric not found in definitions'}; 129 | try { 130 | const frameIds = TraceProcessor.findMainFrameIds(this._traceEvents); 131 | return {pid: frameIds.pid, tid: frameIds.tid, ts: timeOriginMetric.ts}; 132 | } catch (err) { 133 | return {errorMessage: err.message}; 134 | } 135 | } 136 | 137 | /** 138 | * Constructs performance.measure trace events, which have start/end events as follows: 139 | * { "pid": 89922,"tid":1295,"ts":77176783452,"ph":"b","cat":"blink.user_timing","name":"innermeasure","args":{},"tts":1257886,"id":"0xe66c67"} 140 | * { "pid": 89922,"tid":1295,"ts":77176882592,"ph":"e","cat":"blink.user_timing","name":"innermeasure","args":{},"tts":1257898,"id":"0xe66c67"} 141 | * @param {{ts: number, id: string, name: string}} metric 142 | * @param {{pid: number, tid: number, ts: number}} timeOriginEvt 143 | * @return {Array} Pair of trace events (start/end) 144 | */ 145 | synthesizeEventPair(metric, timeOriginEvt) { 146 | // We'll masquerade our fake events to look mostly like the timeOrigin event 147 | const eventBase = { 148 | pid: timeOriginEvt.pid, 149 | tid: timeOriginEvt.tid, 150 | cat: 'blink.user_timing', 151 | name: metric.name, 152 | args: {}, 153 | // randomized id is same for the pair 154 | id: `0x${((Math.random() * 1000000) | 0).toString(16)}`, 155 | }; 156 | const fakeMeasureStartEvent = Object.assign({}, eventBase, { 157 | ts: timeOriginEvt.ts, 158 | ph: 'b', 159 | }); 160 | const fakeMeasureEndEvent = Object.assign({}, eventBase, { 161 | ts: metric.ts, 162 | ph: 'e', 163 | }); 164 | return /** @type {Array} */ ([fakeMeasureStartEvent, fakeMeasureEndEvent]); 165 | } 166 | 167 | /** 168 | * @return {Array} User timing raw trace event pairs 169 | */ 170 | generateFakeEvents() { 171 | const metrics = this.gatherMetrics(); 172 | if (metrics.length === 0) { 173 | log.error('metrics-events', 'Metrics collection had errors, not synthetizing trace events'); 174 | return []; 175 | } 176 | 177 | const timeOriginEvt = this.getTimeOriginEvt(metrics); 178 | if ('errorMessage' in timeOriginEvt) { 179 | log.error('pwmetrics-events', `Reference timeOrigin error: ${timeOriginEvt.errorMessage}`); 180 | return []; 181 | } 182 | 183 | /** @type {Array} */ 184 | const fakeEvents = []; 185 | metrics.forEach(metric => { 186 | if (metric.id === 'timeorigin') { 187 | return; 188 | } 189 | if (!metric.ts) { 190 | log.error('pwmetrics-events', `(${metric.name}) missing timestamp. Skipping…`); 191 | return; 192 | } 193 | log.verbose('pwmetrics-events', `Sythesizing trace events for ${metric.name}`); 194 | fakeEvents.push(...this.synthesizeEventPair(metric, timeOriginEvt)); 195 | }); 196 | return fakeEvents; 197 | } 198 | } 199 | 200 | module.exports = Metrics; -------------------------------------------------------------------------------- /extract-cpu-profile-from-trace.mjs: -------------------------------------------------------------------------------- 1 | // Extract .cpuprofile from a trace. 2 | // 3 | // run like: 4 | // node extract-cpu-profile-from-trace.mjs ~/Downloads/Profile-20200214T165958.json 5 | // it'll create 1 or more .cpuprofiles next to the trace 6 | 7 | 8 | import fs from 'fs'; 9 | import path from 'node:path'; 10 | import {strict as assert} from 'assert'; 11 | import {saveCpuProfile, loadTraceEventsFromFile} from './trace-file-utils.mjs'; 12 | 13 | 14 | // A saved .cpuprofile from JS Profiler panel matches `Profiler.Profile` exactly. 15 | // https://chromedevtools.github.io/devtools-protocol/tot/Profiler/#type-Profile 16 | 17 | // node.hitCount and node.children are populated in Profiler.stop's payload (and when saved from Jsprofiler pane), 18 | // but not when it comes in a trace. This is weird yes. 19 | // CPUProfileDataModel.translateProfileTree() calculates these, according to nancyly@'s tech talk. sweet! 20 | 21 | // See also go/cpu-profiler-notes which has plenty more stuff like this. 22 | 23 | 24 | export function extractCPUProfileData(events) { 25 | 26 | // Cat = `disabled-by-default-v8.cpu_profiler` 27 | const metaEvts = events.filter(e => e.cat === '__metadata'); 28 | 29 | events = events.filter(e => e.cat.includes('v8.cpu_profiler')); 30 | 31 | 32 | console.log(events.length); 33 | // At this point e.name is either 'ProfileChunk' or 'Profile'; 34 | // ProfileChunk events can be on a diff thread id than the header. but the header is canonical. 35 | const profileHeadEvts = events.filter(e => e.name === 'Profile'); 36 | // What pid's do we have? 37 | const pidtids = profileHeadEvts.reduce((prev, curr) => prev.add(`p${curr.pid}t${curr.tid}`), new Set()) 38 | // TODO: use this appraoch instead 39 | // const pidtids = profileHeadEvts.reduce((map_, evt) => { 40 | // if (!map_.has(evt.pid)) { 41 | // map_.set(evt.pid, new Set()); 42 | // } 43 | // map_.get(evt.pid).add(evt.tid); 44 | // return map_; 45 | // }, new Map()); 46 | 47 | 48 | // See also `extractCpuProfile` in CDT's TimelineModel 49 | return Array.from(pidtids).map(async pidtid => { 50 | const pid = parseInt(pidtid.split('t')[0].replace('p', ''), 10); 51 | const tid = parseInt(pidtid.split('t')[1], 10); 52 | const threadName = metaEvts.find(e => e.pid === pid && e.tid === tid)?.args.name; 53 | console.log(`Looking at: "pid":${pid},"tid":${tid}, … ${threadName}`); 54 | 55 | const profileHeadEvt = profileHeadEvts.find(e => e.pid === pid && e.tid === tid); 56 | // id's like 0x2. Match on id and also pid. 57 | const chunkEvts = events.filter(e => e.name === 'ProfileChunk' && e.id === profileHeadEvt.id && e.pid === profileHeadEvt.pid); 58 | 59 | if (!profileHeadEvt) { 60 | return console.error('missing profile header evt.... probably resolvable but not now'); 61 | } 62 | if (!chunkEvts.length){ 63 | return console.error(`No chunk events for ${pidtid}!`); 64 | } 65 | 66 | /** {Crdp.Profiler.Profile} */ 67 | const profile = { 68 | nodes: [], 69 | startTime: -1, 70 | endTime: -1, 71 | samples: [], 72 | timeDeltas: [], 73 | ...profileHeadEvt.args.data 74 | }; 75 | 76 | // CPU profile generator makes chunks every 100 samples.. which seems really low IMO. 77 | // https://source.chromium.org/chromium/chromium/src/+/main:v8/src/profiler/profile-generator.cc;l=650-654;drc=4106e2406bd1b7219657a730bc389eb3a4629daa 78 | chunkEvts.forEach(chunk => { 79 | const chunkData = chunk.args.data.cpuProfile; 80 | profile.nodes.push(... chunkData.nodes || []); 81 | profile.samples.push(... chunkData.samples || []); 82 | // profile.lines is apparently also a thing (later me. whatttttttttt?) but i dont see that it does anything.. so ignoring for now. 83 | // todo delete this comment 84 | 85 | 86 | // Why is timeDeltas not in .args.data.cpuProfile???? beats me. 87 | profile.timeDeltas.push(... chunk.args.data.timeDeltas || []); 88 | // shrug. https://source.chromium.org/chromium/chromium/src/+/main:v8/src/profiler/profile-generator.cc;l=755;bpv=0;bpt=1 89 | profile.endTime = chunkData.endTime || profile.endTime; 90 | }); 91 | 92 | // Stole this from timelinemodel 93 | if (profile.endTime === -1){ 94 | profile.endTime = profile.timeDeltas.reduce((x, y) => x + y, profile.startTime); 95 | } 96 | 97 | // for compat with vscode's viewer. 98 | for (const node of profile.nodes) { 99 | node.callFrame.url = node.callFrame.url || ''; 100 | } 101 | 102 | convertParentIntoChildrenIfNeeded(profile); 103 | 104 | return { 105 | pid, 106 | tid, 107 | id: profileHeadEvt.id, 108 | headTs: profileHeadEvt.ts, 109 | profile, 110 | threadName, 111 | } 112 | }); 113 | } 114 | 115 | 116 | // thanks! https://github.com/lahmatiy/cpupro/blob/be452cf876c6daf4ed665fac2f29a5aa37ee7466/app/prepare/formats/cpuprofile.ts#L56C1-L92C2 117 | 118 | // nodes may missing children field but have parent field, rebuild children arrays then; 119 | // avoid updating children when nodes have parent and children fields 120 | export function convertParentIntoChildrenIfNeeded(data) { 121 | const nodes = data.nodes; 122 | 123 | // no action when just one node or both first nodes has no parent (since only root node can has no parent) 124 | if (nodes.length < 2 || (typeof nodes[0].parent !== 'number' && typeof nodes[1].parent !== 'number')) { 125 | return; 126 | } 127 | 128 | // build map for nodes with no children only 129 | const nodeWithNoChildrenById = new Map(); 130 | 131 | for (const node of data.nodes) { 132 | if (!Array.isArray(node.children) || node.children.length === 0) { 133 | nodeWithNoChildrenById.set(node.id, node); 134 | } 135 | } 136 | 137 | // rebuild children for nodes which missed it 138 | if (nodeWithNoChildrenById.size > 0) { 139 | for (const node of nodes) { 140 | if (typeof node.parent === 'number') { 141 | const parent = nodeWithNoChildrenById.get(node.parent); 142 | 143 | if (parent !== undefined) { 144 | if (Array.isArray(parent.children)) { 145 | parent.children.push(node.id); 146 | } else { 147 | parent.children = [node.id]; 148 | } 149 | } 150 | } 151 | } 152 | } 153 | } 154 | 155 | 156 | // CLI direct invocation? 157 | if (import.meta.url.endsWith(process?.argv[1])) { 158 | cli(); 159 | } 160 | 161 | async function cli() { 162 | const filename = path.resolve(process.cwd(), process.argv[2]); 163 | 164 | const traceEvents = loadTraceEventsFromFile(filename); 165 | const cpuProfileData = await Promise.all(extractCPUProfileData(traceEvents)); 166 | 167 | cpuProfileData.forEach(async ({pid, tid, profile, threadName}) => { 168 | // Uncomment to manually "crop" the cpu profile. (probably dont want this....) 169 | // profile.samples = profile.samples.slice(0, 50_000); 170 | // profile.timeDeltas = profile.timeDeltas.slice(0, 50_000); 171 | 172 | console.log('counts:', profile.nodes.length, profile.samples.length, profile.timeDeltas.length) 173 | 174 | const cpuFilename = `${filename}-pid-${pid}-tid-${tid}-${threadName}.cpuprofile`; 175 | 176 | 177 | // format it and save 178 | await saveCpuProfile(profile, cpuFilename); 179 | 180 | const readRes = fs.readFileSync(cpuFilename, 'utf-8'); 181 | console.log(`written ${readRes.length.toLocaleString()} bytes to: ${cpuFilename}`); 182 | }); 183 | 184 | } 185 | -------------------------------------------------------------------------------- /third_party/lantern-trace-saver.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Copyright 2018 The Lighthouse Authors. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 5 | */ 6 | 7 | /** @typedef {import('./dependency-graph/base-node.js').Node} Node */ 8 | /** @typedef {import('./dependency-graph/simulator/simulator.js').CompleteNodeTiming} CompleteNodeTiming */ 9 | 10 | /** 11 | * @param {Map} nodeTimings 12 | * @return {LH.Trace} 13 | */ 14 | function convertNodeTimingsToTrace(nodeTimings) { 15 | /** @type {LH.TraceEvent[]} */ 16 | const traceEvents = []; 17 | const baseTs = 1e9; 18 | const baseEvent = {pid: 1, tid: 1, cat: 'devtools.timeline'}; 19 | const frame = 'A00001'; 20 | /** @param {number} ms */ 21 | const toMicroseconds = ms => baseTs + ms * 1000; 22 | 23 | traceEvents.push(createFakeTracingStartedEvent()); 24 | traceEvents.push({...createFakeTracingStartedEvent(), name: 'TracingStartedInBrowser'}); 25 | 26 | // Create a fake requestId counter 27 | let requestId = 1; 28 | let lastEventEndTime = 0; 29 | for (const [node, timing] of nodeTimings.entries()) { 30 | lastEventEndTime = Math.max(lastEventEndTime, timing.endTime); 31 | if (node.type === 'cpu') { 32 | // Represent all CPU work that was bundled in a task as an EvaluateScript event 33 | traceEvents.push(...createFakeTaskEvents(node, timing)); 34 | } else { 35 | // Ignore data URIs as they don't really add much value 36 | if (/^data/.test(node.record.url)) continue; 37 | traceEvents.push(...createFakeNetworkEvents(requestId, node.record, timing)); 38 | requestId++; 39 | } 40 | } 41 | 42 | // Create a fake task event ~1s after the trace ends for a sane default bounds in DT 43 | traceEvents.push( 44 | ...createFakeTaskEvents( 45 | // @ts-expect-error 46 | {childEvents: [], event: {}}, 47 | { 48 | startTime: lastEventEndTime + 1000, 49 | endTime: lastEventEndTime + 1001, 50 | } 51 | ) 52 | ); 53 | 54 | return {traceEvents}; 55 | 56 | /** 57 | * @return {LH.TraceEvent} 58 | */ 59 | function createFakeTracingStartedEvent() { 60 | const argsData = { 61 | frameTreeNodeId: 1, 62 | sessionId: '1.1', 63 | page: frame, 64 | persistentIds: true, 65 | frames: [{frame, url: 'about:blank', name: '', processId: 1}], 66 | }; 67 | 68 | return { 69 | ...baseEvent, 70 | ts: baseTs - 1e5, 71 | ph: 'I', 72 | s: 't', 73 | cat: 'disabled-by-default-devtools.timeline', 74 | name: 'TracingStartedInPage', 75 | args: {data: argsData}, 76 | dur: 0, 77 | }; 78 | } 79 | 80 | /** 81 | * @param {LH.Gatherer.Simulation.GraphCPUNode} cpuNode 82 | * @param {{startTime: number, endTime: number}} timing 83 | * @return {LH.TraceEvent[]} 84 | */ 85 | function createFakeTaskEvents(cpuNode, timing) { 86 | const argsData = { 87 | url: '', 88 | frame, 89 | lineNumber: 0, 90 | columnNumber: 0, 91 | }; 92 | 93 | const eventTs = toMicroseconds(timing.startTime); 94 | 95 | /** @type {LH.TraceEvent[]} */ 96 | const events = [ 97 | { 98 | ...baseEvent, 99 | ph: 'X', 100 | name: 'Task', 101 | ts: eventTs, 102 | dur: (timing.endTime - timing.startTime) * 1000, 103 | args: {data: argsData}, 104 | }, 105 | ]; 106 | 107 | const nestedBaseTs = cpuNode.event.ts || 0; 108 | const multiplier = (timing.endTime - timing.startTime) * 1000 / cpuNode.event.dur; 109 | // https://github.com/ChromeDevTools/devtools-frontend/blob/5429ac8a61ad4fa/front_end/timeline_model/TimelineModel.js#L1129-L1130 110 | const netReqEvents = new Set(['ResourceSendRequest', 'ResourceFinish', 111 | 'ResourceReceiveResponse', 'ResourceReceivedData']); 112 | for (const event of cpuNode.childEvents) { 113 | if (netReqEvents.has(event.name)) continue; 114 | const ts = eventTs + (event.ts - nestedBaseTs) * multiplier; 115 | const newEvent = {...event, ...{pid: baseEvent.pid, tid: baseEvent.tid}, ts}; 116 | if (event.dur) newEvent.dur = event.dur * multiplier; 117 | events.push(newEvent); 118 | } 119 | 120 | return events; 121 | } 122 | 123 | /** 124 | * @param {number} requestId 125 | * @param {LH.Artifacts.NetworkRequest} record 126 | * @param {CompleteNodeTiming} timing 127 | * @return {LH.TraceEvent} 128 | */ 129 | function createWillSendRequestEvent(requestId, record, timing) { 130 | return { 131 | ...baseEvent, 132 | ph: 'I', 133 | s: 't', 134 | // No `dur` on network instant events but add to keep types happy. 135 | dur: 0, 136 | name: 'ResourceWillSendRequest', 137 | ts: toMicroseconds(timing.startTime), 138 | args: {data: {requestId: String(requestId)}}, 139 | }; 140 | } 141 | 142 | /** 143 | * @param {number} requestId 144 | * @param {LH.Artifacts.NetworkRequest} record 145 | * @param {CompleteNodeTiming} timing 146 | * @return {LH.TraceEvent[]} 147 | */ 148 | function createFakeNetworkEvents(requestId, record, timing) { 149 | if (!('connectionTiming' in timing)) { 150 | throw new Error('Network node timing incomplete'); 151 | } 152 | 153 | // 0ms requests get super-messed up rendering 154 | // Use 0.3ms instead so they're still hoverable, https://github.com/GoogleChrome/lighthouse/pull/5350#discussion_r194563201 155 | let {startTime, endTime} = timing; // eslint-disable-line prefer-const 156 | if (startTime === endTime) endTime += 0.3; 157 | 158 | const requestData = {requestId: requestId.toString(), frame}; 159 | // No `dur` on network instant events but add to keep types happy. 160 | /** @type {LH.Util.StrictOmit} */ 161 | const baseRequestEvent = {...baseEvent, ph: 'I', s: 't', dur: 0}; 162 | 163 | const sendRequestData = { 164 | ...requestData, 165 | requestMethod: record.requestMethod, 166 | url: record.url, 167 | priority: record.priority, 168 | }; 169 | 170 | const {dnsResolutionTime, connectionTime, sslTime, timeToFirstByte} = timing.connectionTiming; 171 | let sslStart = -1; 172 | let sslEnd = -1; 173 | if (connectionTime !== undefined && sslTime !== undefined) { 174 | sslStart = connectionTime - sslTime; 175 | sslEnd = connectionTime; 176 | } 177 | const receiveResponseData = { 178 | ...requestData, 179 | statusCode: record.statusCode, 180 | mimeType: record.mimeType, 181 | encodedDataLength: record.transferSize, 182 | fromCache: record.fromDiskCache, 183 | fromServiceWorker: record.fetchedViaServiceWorker, 184 | timing: { 185 | // `requestTime` is in seconds. 186 | requestTime: toMicroseconds(startTime) / (1000 * 1000), 187 | // Remaining values are milliseconds after `requestTime`. 188 | dnsStart: dnsResolutionTime === undefined ? -1 : 0, 189 | dnsEnd: dnsResolutionTime ?? -1, 190 | connectStart: dnsResolutionTime ?? -1, 191 | connectEnd: connectionTime ?? -1, 192 | sslStart, 193 | sslEnd, 194 | sendStart: connectionTime ?? 0, 195 | sendEnd: connectionTime ?? 0, 196 | receiveHeadersEnd: timeToFirstByte, 197 | workerStart: -1, 198 | workerReady: -1, 199 | proxyStart: -1, 200 | proxyEnd: -1, 201 | pushStart: 0, 202 | pushEnd: 0, 203 | }, 204 | }; 205 | 206 | const resourceFinishData = { 207 | requestId: requestId.toString(), 208 | encodedDataLength: record.transferSize, 209 | decodedBodyLength: record.resourceSize, 210 | didFail: !!record.failed, 211 | finishTime: toMicroseconds(endTime) / (1000 * 1000), 212 | }; 213 | 214 | /** @type {LH.TraceEvent[]} */ 215 | const events = []; 216 | 217 | // Navigation request needs an additional ResourceWillSendRequest event. 218 | if (requestId === 1) { 219 | events.push(createWillSendRequestEvent(requestId, record, timing)); 220 | } 221 | 222 | events.push( 223 | { 224 | ...baseRequestEvent, 225 | name: 'ResourceSendRequest', 226 | ts: toMicroseconds(startTime), 227 | args: {data: sendRequestData}, 228 | }, 229 | { 230 | ...baseRequestEvent, 231 | name: 'ResourceFinish', 232 | ts: toMicroseconds(endTime), 233 | args: {data: resourceFinishData}, 234 | } 235 | ); 236 | 237 | if (!record.failed) { 238 | events.push({ 239 | ...baseRequestEvent, 240 | name: 'ResourceReceiveResponse', 241 | // Event `ts` isn't meaningful, so just pick a time. 242 | ts: toMicroseconds((startTime + endTime) / 2), 243 | args: {data: receiveResponseData}, 244 | }); 245 | } 246 | 247 | return events; 248 | } 249 | } 250 | 251 | export default { 252 | simulationNamesToIgnore: [ 253 | 'unlabeled', 254 | // These node timings should be nearly identical to the ones produced for Interactive 255 | 'optimisticSpeedIndex', 256 | 'optimisticFlexSpeedIndex', 257 | 'pessimisticSpeedIndex', 258 | ], 259 | convertNodeTimingsToTrace, 260 | }; 261 | -------------------------------------------------------------------------------- /winnow-trace.mjs: -------------------------------------------------------------------------------- 1 | // Use a filter predicate to remove excess stuff. eg: stripping down to a timerange, just removing `disabled-by-default.v8.compile` 2 | 3 | import path from 'node:path'; 4 | import {saveTrace, loadTraceEventsFromFile} from './trace-file-utils.mjs'; 5 | import {extractCPUProfileData} from './extract-cpu-profile-from-trace.mjs'; 6 | 7 | /** 8 | * Usage: 9 | * 10 | * For time crop, adjust the `MIN_TS` and `MAX_TS` numbers 11 | * To avoid time-crop, comment out those 3 lines. 12 | * 13 | * To exclude specific events, set them in `eventNamesToExclude` 14 | * 15 | * To exclude everything except some specific event names.... do that yourself (haha) within `filterEventFn` 16 | */ 17 | 18 | const MIN_TS = 2401383928728; 19 | const MAX_TS = 2401384251864; 20 | const isTsWithinRange = (ts) => MIN_TS < ts && ts < MAX_TS; 21 | 22 | 23 | const eventNamesToExclude = []; // ['V8.ParseFunction', 'V8.CompileIgnition', 'V8.CompileIgnitionFinalization', 'v8.compile', 'V8.CompileCode'] 24 | 25 | 26 | // return true to keep. false to drop 27 | function filterEventFn(e, cpuProfileData) { 28 | // Basics that need to be present regardless. 29 | if (e.name === 'TracingStartedInBrowser' || e.cat === '__metadata' || e.ts === 0) return true; 30 | if (typeof isTsWithinRange === 'function' && e.name === 'FrameCommittedInBrowser') return e.ts < MAX_TS; 31 | 32 | if (eventNamesToExclude.length && eventNamesToExclude.includes(e.name)) return false; 33 | 34 | if (typeof isTsWithinRange === 'function') { 35 | adjustCPUProfilesForTimeCrop(e, cpuProfileData); 36 | if (e.shouldRemove) return false; 37 | 38 | return isTsWithinRange(e.ts); 39 | } 40 | 41 | return true; // Keep anything not false'd at this point. 42 | } 43 | 44 | 45 | // We need to adjust samples and timeDeltas arrays. 46 | function adjustCPUProfilesForTimeCrop(e, cpuProfileData) { 47 | if (e.name === 'Profile') return true; 48 | if (e.name.startsWith('ProfileChunk')) { 49 | const index = cpuProfileData.findIndex(d => d.id === e.id && d.pid === e.pid); // tid doesnt match on profilechunks becaose weird reasons. 50 | if (index !== -1) { 51 | const [cpuProfileDatum] = cpuProfileData.splice(index, 1); // Remove from cpuProfileData so we can exclude later matching ProfileChunks 52 | 53 | let currentTime = cpuProfileDatum.profile.endTime; // We'll keep track of current time as we go through them 54 | for (let i = cpuProfileDatum.profile.samples.length - 1; i >= 0; i--) { // reverse loop to avoid index issues while shifting 55 | const withinTimeRange = isTsWithinRange(currentTime); 56 | currentTime -= cpuProfileDatum.profile.timeDeltas[i]; 57 | if (!withinTimeRange) { 58 | // Delete that sample and timeDelta 59 | cpuProfileDatum.profile.samples.splice(i, 1); 60 | cpuProfileDatum.profile.timeDeltas.splice(i, 1); 61 | } 62 | } 63 | // Shouldn't make a diff but we'll recompute this end time, now that it's different. 64 | cpuProfileDatum.profile.endTime = cpuProfileDatum.profile.timeDeltas.reduce((x, y) => x + y, cpuProfileDatum.profile.startTime); 65 | // If samples array is > 125_507, this will trigger `Maximum callstack exceeded` in SamplesHandler due to a `.push(...samples)` 66 | e.args.data.cpuProfile = cpuProfileDatum.profile; 67 | e.args.data.timeDeltas = cpuProfileDatum.profile.timeDeltas; 68 | e.args.data.lines = cpuProfileDatum.profile.lines; 69 | } else { 70 | // Remove because we already put all the data in an earlier one. Also, Dumb hack but hey. 71 | e.shouldRemove = true; 72 | } 73 | } 74 | } 75 | 76 | export async function resaveTrace(filename, filterEventFn) { 77 | const traceEvents = loadTraceEventsFromFile(filename); 78 | console.log('Refomatting', traceEvents.length, 'events'); 79 | 80 | const cpuProfileData = typeof isTsWithinRange === 'function' ? await Promise.all(extractCPUProfileData(traceEvents)) : []; 81 | const afterTraceEvents = filteredTraceSort(traceEvents, e => filterEventFn(e, cpuProfileData)); 82 | 83 | const afterFilename = `${filename}.winnowed.json`; 84 | await saveTrace({traceEvents: afterTraceEvents}, afterFilename); 85 | console.log(`Written: ${afterFilename}`); 86 | 87 | console.log('eventCount: ', traceEvents.length, '==>', afterTraceEvents.length); 88 | } 89 | 90 | // CLI direct invocation? 91 | if (import.meta.url.endsWith(process?.argv[1])) { 92 | cli(); 93 | } 94 | 95 | async function cli() { 96 | const filename = path.resolve(process.cwd(), process.argv[2]); 97 | await resaveTrace(filename, filterEventFn); 98 | } 99 | 100 | 101 | 102 | 103 | 104 | 105 | // Below functions are lifted from Lighthouse's trace-processor. Probably can use something more straightforward… 106 | 107 | /** 108 | * Sorts and filters trace events by timestamp and respecting the nesting structure inherent to 109 | * parent/child event relationships. 110 | * @param {LH.TraceEvent[]} traceEvents 111 | * @param {(e: LH.TraceEvent) => boolean} filter 112 | */ 113 | function filteredTraceSort(traceEvents, filter) { 114 | // create an array of the indices that we want to keep 115 | const indices = []; 116 | for (let srcIndex = 0; srcIndex < traceEvents.length; srcIndex++) { 117 | if (filter(traceEvents[srcIndex])) { 118 | indices.push(srcIndex); 119 | } 120 | } 121 | 122 | // Sort by ascending timestamp first. 123 | indices.sort((indexA, indexB) => traceEvents[indexA].ts - traceEvents[indexB].ts); 124 | 125 | // Now we find groups with equal timestamps and order them by their nesting structure. 126 | for (let i = 0; i < indices.length - 1; i++) { 127 | const ts = traceEvents[indices[i]].ts; 128 | const tsGroupIndices = [i]; 129 | for (let j = i + 1; j < indices.length; j++) { 130 | if (traceEvents[indices[j]].ts !== ts) break; 131 | tsGroupIndices.push(j); 132 | } 133 | 134 | // We didn't find any other events with the same timestamp, just keep going. 135 | if (tsGroupIndices.length === 1) continue; 136 | 137 | // Sort the group by other criteria and replace our index array with it. 138 | const finalIndexOrder = _sortTimestampEventGroup( 139 | tsGroupIndices, 140 | indices, 141 | i, 142 | traceEvents 143 | ); 144 | indices.splice(i, finalIndexOrder.length, ...finalIndexOrder); 145 | // We just sorted this set of identical timestamps, so skip over the rest of the group. 146 | // -1 because we already have i++. 147 | i += tsGroupIndices.length - 1; 148 | } 149 | 150 | // create a new array using the target indices from previous sort step 151 | const sorted = []; 152 | for (let i = 0; i < indices.length; i++) { 153 | sorted.push(traceEvents[indices[i]]); 154 | } 155 | 156 | return sorted; 157 | } 158 | 159 | 160 | /** 161 | * This method sorts a group of trace events that have the same timestamp. We want to... 162 | * 163 | * 1. Put E events first, we finish off our existing events before we start new ones. 164 | * 2. Order B/X events by their duration, we want parents to start before child events. 165 | * 3. If we don't have any of this to go on, just use the position in the original array (stable sort). 166 | * 167 | * Note that the typical group size with the same timestamp will be quite small (<10 or so events), 168 | * and the number of groups typically ~1% of total trace, so the same ultra-performance-sensitive consideration 169 | * given to functions that run on entire traces does not necessarily apply here. 170 | * 171 | * @param {number[]} tsGroupIndices 172 | * @param {number[]} timestampSortedIndices 173 | * @param {number} indexOfTsGroupIndicesStart 174 | * @param {LH.TraceEvent[]} traceEvents 175 | * @return {number[]} 176 | */ 177 | function _sortTimestampEventGroup( 178 | tsGroupIndices, 179 | timestampSortedIndices, 180 | indexOfTsGroupIndicesStart, 181 | traceEvents 182 | ) { 183 | /* 184 | * We have two different sets of indices going on here. 185 | 186 | * 1. There's the index for an element of `traceEvents`, referred to here as an `ArrayIndex`. 187 | * `timestampSortedIndices` is an array of `ArrayIndex` elements. 188 | * 2. There's the index for an element of `timestampSortedIndices`, referred to here as a `TsIndex`. 189 | * A `TsIndex` is therefore an index to an element which is itself an index. 190 | * 191 | * These two helper functions help resolve this layer of indirection. 192 | * Our final return value is an array of `ArrayIndex` in their final sort order. 193 | */ 194 | /** @param {number} i */ 195 | const lookupArrayIndexByTsIndex = i => timestampSortedIndices[i]; 196 | /** @param {number} i */ 197 | const lookupEventByTsIndex = i => traceEvents[lookupArrayIndexByTsIndex(i)]; 198 | 199 | /** @type {Array} */ 200 | const eEventIndices = []; 201 | /** @type {Array} */ 202 | const bxEventIndices = []; 203 | /** @type {Array} */ 204 | const otherEventIndices = []; 205 | 206 | for (const tsIndex of tsGroupIndices) { 207 | // See comment above for the distinction between `tsIndex` and `arrayIndex`. 208 | const arrayIndex = lookupArrayIndexByTsIndex(tsIndex); 209 | const event = lookupEventByTsIndex(tsIndex); 210 | if (event.ph === 'E') eEventIndices.push(arrayIndex); 211 | else if (event.ph === 'X' || event.ph === 'B') bxEventIndices.push(arrayIndex); 212 | else otherEventIndices.push(arrayIndex); 213 | } 214 | 215 | /** @type {Map} */ 216 | const effectiveDuration = new Map(); 217 | for (const index of bxEventIndices) { 218 | const event = traceEvents[index]; 219 | if (event.ph === 'X') { 220 | effectiveDuration.set(index, event.dur); 221 | } else { 222 | // Find the next available 'E' event *after* the current group of events that matches our name, pid, and tid. 223 | let duration = Number.MAX_SAFE_INTEGER; 224 | // To find the next "available" 'E' event, we need to account for nested events of the same name. 225 | let additionalNestedEventsWithSameName = 0; 226 | const startIndex = indexOfTsGroupIndicesStart + tsGroupIndices.length; 227 | for (let j = startIndex; j < timestampSortedIndices.length; j++) { 228 | const potentialMatchingEvent = lookupEventByTsIndex(j); 229 | const eventMatches = potentialMatchingEvent.name === event.name && 230 | potentialMatchingEvent.pid === event.pid && 231 | potentialMatchingEvent.tid === event.tid; 232 | 233 | // The event doesn't match, just skip it. 234 | if (!eventMatches) continue; 235 | 236 | if (potentialMatchingEvent.ph === 'E' && additionalNestedEventsWithSameName === 0) { 237 | // It's the next available 'E' event for us, so set the duration and break the loop. 238 | duration = potentialMatchingEvent.ts - event.ts; 239 | break; 240 | } else if (potentialMatchingEvent.ph === 'E') { 241 | // It's an 'E' event but for a nested event. Decrement our counter and move on. 242 | additionalNestedEventsWithSameName--; 243 | } else if (potentialMatchingEvent.ph === 'B') { 244 | // It's a nested 'B' event. Increment our counter and move on. 245 | additionalNestedEventsWithSameName++; 246 | } 247 | } 248 | 249 | effectiveDuration.set(index, duration); 250 | } 251 | } 252 | 253 | bxEventIndices.sort((indexA, indexB) => ((effectiveDuration.get(indexB) || 0) - 254 | (effectiveDuration.get(indexA) || 0) || (indexA - indexB))); 255 | 256 | otherEventIndices.sort((indexA, indexB) => indexA - indexB); 257 | 258 | return [...eEventIndices, ...bxEventIndices, ...otherEventIndices]; 259 | } 260 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /process-traces.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2023 The Lighthouse Authors 3 | * SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import {strict as assert} from 'node:assert'; 7 | import os from 'node:os'; 8 | import test from 'node:test'; 9 | 10 | import {glob} from 'glob'; 11 | // Lighthouse 12 | // import {LH_ROOT} from 'lighthouse/shared/root.js'; 13 | // import {TraceProcessor} from 'lighthouse/core/lib/tracehouse/trace-processor.js'; 14 | // import {MainThreadTasks as MainThreadTasks_} from 'lighthouse/core/lib/tracehouse/main-thread-tasks.js'; 15 | 16 | // DevTools 17 | import {polyfillDOMRect} from '../trace_engine/analyze-trace.mjs'; 18 | import * as Trace from '../trace_engine/models/trace/trace.js'; 19 | 20 | // import pkg from '@paulirish/trace_engine/package.json' with { "type": "json" }; 21 | 22 | // console.log('@paulirish/trace_engine', {version: pkg.version}); 23 | 24 | // trace-stuff 25 | import {loadTraceEventsFromFile} from './trace-file-utils.mjs'; 26 | 27 | // (async function main() { 28 | 29 | const passedArg = process.argv[2]?.trim(); 30 | let filenames = []; 31 | 32 | if (passedArg) { 33 | filenames.push(passedArg); 34 | } else { 35 | filenames.push( 36 | ...[ 37 | // ...glob.sync(`${LH_ROOT}/latest*/defaultPass.trace.json`), 38 | // ...glob.sync(`${LH_ROOT}/core/test/fixtures/traces/**.json`), // catches a bunch of other non-trace json. eh. 39 | // ...glob.sync(`${LH_ROOT}/core/test/fixtures/**/trace.json`), 40 | // ...glob.sync(`${LH_ROOT}/core/test/fixtures/traces/load.json`), 41 | 42 | // ...glob.sync(`${os.homedir()}/chromium-devtools/devtools-frontend/front_end/panels/timeline/fixtures/traces/*.gz`), 43 | ...glob.sync(`${os.homedir()}/Downloads/traces/**.json`), 44 | // ...glob.sync(`${os.homedir()}/Downloads/traces/tracecafe-stored-traces/traces/*`), 45 | ...glob.sync(`${os.homedir()}/Downloads/traces/**.json.gz`), 46 | ].filter(filename => { 47 | const blocklist = ['devtoolslog', 'devtools.log', 'network-records', 'cpuprofile', 'BUG', 'Busted', 'Profile-']; 48 | return !blocklist.some(blocklistItem => filename.includes(blocklistItem)); 49 | }) 50 | ); 51 | } 52 | 53 | filenames = Array.from(new Set(filenames)).sort() // .slice(0, 3); // uniq 54 | 55 | 56 | polyfillDOMRect(); 57 | 58 | const allFailures = new Map(); 59 | 60 | process.on('SIGINT', () => { 61 | console.log('\n\nFatal parsing errors, grouped:\n', allFailures); 62 | process.exit(0); // Exit code 0 indicates success 63 | }); 64 | 65 | 66 | // test('ok', async () => { 67 | for (const filename of filenames) { 68 | 69 | await parseTraceText(filename); 70 | } 71 | // }); 72 | 73 | console.log('\n\nFatal parsing errors, grouped:\n', allFailures); 74 | 75 | // })(); 76 | 77 | async function parseTraceText(filename) { 78 | process.stderr.write(`\n🥳 Loading… ${filename.replace(os.homedir(), '$HOME')}`); 79 | 80 | let traceEvents; 81 | try { 82 | traceEvents = loadTraceEventsFromFile(filename); 83 | } catch (e) { 84 | console.warn(e.message); 85 | return; 86 | } 87 | 88 | process.stderr.write(` ${traceEvents.length.toLocaleString()} evts\n`); 89 | const trace = { 90 | traceEvents, 91 | }; 92 | if (typeof trace?.traceEvents?.at(0)?.pid === 'undefined') { 93 | console.error('\n❌ ... skipping. not an actual trace.', filename); 94 | return; 95 | } 96 | 97 | 98 | const logFatal = tag => e => { 99 | process.stdout.write(`\n- ‼️ ${tag} FATAL: ${e.message}`) && false; 100 | const signature = e.stack.split('\n').slice(0,2).join(' | ') 101 | const failuresPerMessage = allFailures.get(signature) ?? []; 102 | failuresPerMessage.push(filename); 103 | allFailures.set(signature, failuresPerMessage); 104 | }; 105 | const logFail = tag => e => process.stdout.write(`\n- 😞 ${tag} fail: ${e.message}`) && false; 106 | 107 | 108 | // const proTrace = await processWithLighthouse(trace).catch(logFatal('LH processor')); 109 | // proTrace && assertLighthouseData(proTrace).catch(logFail('LH assertion')); 110 | 111 | 112 | let model = await processWithTraceEngine(trace).catch(logFatal('Trace engine parse')); 113 | model && assertEngineData(model, filename).catch(logFail('Trace engine assertion')); 114 | // also result.insights 115 | 116 | model.resetProcessor(); 117 | model = undefined; 118 | traceEvents = []; 119 | } 120 | 121 | async function processWithLighthouse(trace) { 122 | const proTrace = await TraceProcessor.processTrace(trace); 123 | 124 | const processedNavigation = await TraceProcessor.processNavigation(proTrace); 125 | const longTasks = TraceProcessor.getMainThreadTopLevelEvents(proTrace); 126 | const mainTT = MainThreadTasks_.getMainThreadTasks( 127 | proTrace.mainThreadEvents, 128 | proTrace.frames, 129 | proTrace.timestamps.traceEnd, 130 | proTrace.timestamps.timeOrigin 131 | ); 132 | return proTrace; 133 | } 134 | 135 | async function assertLighthouseData(proTrace) { 136 | // name all pid & tids we find in the frame tree 137 | const processTree = new Map(); 138 | proTrace.frameTreeEvents.forEach(e => processTree.set(`p_${e.pid}_t_${e.tid}`, undefined)); 139 | for (const pidtid of processTree.keys()) { 140 | const [pid, tid] = pidtid 141 | .replace('p_', '') 142 | .split('_t_') 143 | .map(n => parseInt(n, 10)); 144 | const threadnames = proTrace._keyEvents.filter(e => e.cat === '__metadata' && e.name === 'thread_name'); 145 | const name = threadnames.find(e => e.pid === pid && e.tid === tid)?.args?.name; 146 | processTree.set(pidtid, name); 147 | } 148 | 149 | // console.log(' - ok 1/2', { 150 | // frames: proTrace.frames.length, 151 | // ids: proTrace.mainFrameIds ?? proTrace.mainFrameInfo, 152 | // processTree: processTree.size, 153 | // rendererPidToTid: proTrace._rendererPidToTid, 154 | // }); 155 | 156 | // console.log(' - ok 2/2', {timings: processedNavigation.timings}); 157 | 158 | // console.log('- ok 2/2', { 159 | // longtasks: longTasks.length, 160 | // 'main thread tasks': proTrace.mainThreadEvents.length, 161 | // mainTT: mainTT.length, 162 | // }); 163 | 164 | // const str = MainThreadTasks_.printTaskTreeToDebugString(mainTT, {printWidth: process.stdout.columns - 3}); 165 | // console.log(str); 166 | } 167 | 168 | async function processWithTraceEngine(trace) { 169 | const model = Trace.TraceModel.Model.createWithAllHandlers(Trace.Types.Configuration.DEFAULT); 170 | await model.parse(trace.traceEvents); 171 | return model; 172 | } 173 | 174 | async function assertEngineData(model, filename) { 175 | 176 | filename = filename.split('/').at(-1); 177 | const data = model.parsedTrace(); 178 | const insights = model.traceInsights() 179 | 180 | 181 | // // return; 182 | // // test(`engine data looks good for ${filename}`, t => { 183 | // // assertions extrcted from trace_engine/test-trace-engine.mjs 184 | // assert.equal(data.Renderer.allTraceEntries.length > 1, true); 185 | // // assert.equal(data.Screenshots.length > 2, true); 186 | // assert.equal(data.Meta.threadsInProcess.size > 0, true); 187 | // // assert.equal(data.Meta.mainFrameNavigations.length > 0, true); 188 | 189 | // const shouldBeNumbers = { 190 | // traceBounds: data.Meta.traceBounds.min, 191 | // traceBounds: data.Meta.traceBounds.max, 192 | // traceBounds: data.Meta.traceBounds.range, 193 | // browserProcessId: data.Meta.browserProcessId, 194 | // browserThreadId: data.Meta.browserThreadId, 195 | // gpuProcessId: data.Meta.gpuProcessId, 196 | // // gpuThreadId: data.Meta.gpuThreadId, 197 | // topLevelRendererIds: Array.from(data.Meta.topLevelRendererIds.values()).at(0), 198 | // frameByProcessId: Array.from(data.Meta.frameByProcessId.keys()).at(0), 199 | // }; 200 | 201 | // Object.entries(shouldBeNumbers).forEach(([key, val], i) => { 202 | // assert.equal(isNaN(val), false, `${key} is NaN`); 203 | // assert.equal(typeof val, 'number', `${key} is not a number`); 204 | // assert.equal(val > 10, true, `${key} is not more than 10`); 205 | // }); 206 | // const shouldBeStrings = { 207 | // mainFrameId: data.Meta.mainFrameId, 208 | // mainFrameURL: data.Meta.mainFrameURL, 209 | // // navigationsByFrameId: Array.from(data.Meta.navigationsByFrameId.keys()).at(0), 210 | // // navigationsByNavigationId: Array.from(data.Meta.navigationsByNavigationId.keys()).at(0), 211 | // mainFrameId: data.Meta.mainFrameId, 212 | // }; 213 | 214 | 215 | // Object.entries(shouldBeStrings).forEach(([key, val], i) => { 216 | // assert.equal(typeof val, 'string',`${key} isn't a string, but instead it's: ${typeof val}.}`, ); 217 | // assert.equal(val.length > 10, true, `${key} is not more than 10`); 218 | // }); 219 | // // }); 220 | // const test = (d, fn) => fn(); 221 | 222 | 223 | test('key values are populated. ' + filename, t => { 224 | // assert.equal((data.Screenshots.legacySyntheticScreenshots?.length ?? 0) > 2, true); 225 | assert.equal(data.Meta.threadsInProcess.size > 0, true); 226 | assert.equal(data.Meta.mainFrameNavigations.length > 0, true); 227 | }); 228 | 229 | test('numeric values are set and look legit. ' + filename, t => { 230 | const shouldBeNumbers = [ 231 | data.Meta.traceBounds.min, 232 | data.Meta.traceBounds.max, 233 | data.Meta.traceBounds.range, 234 | data.Meta.browserProcessId, 235 | data.Meta.browserThreadId, 236 | data.Meta.gpuProcessId, 237 | data.Meta.gpuThreadId, 238 | Array.from(data.Meta.topLevelRendererIds.values()).at(0), 239 | Array.from(data.Meta.frameByProcessId.keys()).at(0), 240 | ]; 241 | for (const datum of shouldBeNumbers) { 242 | assert.equal(typeof datum, 'number'); 243 | if (typeof datum !== 'number') 244 | throw new Error(); 245 | assert.equal(isNaN(datum), false); 246 | assert.equal(datum > 10, true); 247 | } 248 | }); 249 | 250 | test('string values are set and look legit. ' + filename, t => { 251 | const shouldBeStrings = [ 252 | data.Meta.mainFrameId, 253 | data.Meta.mainFrameURL, 254 | Array.from(data.Meta.navigationsByFrameId.keys()).at(0), 255 | Array.from(data.Meta.navigationsByNavigationId.keys()).at(0), 256 | data.Meta.mainFrameId, 257 | ]; 258 | 259 | for (const datum of shouldBeStrings) { 260 | assert.equal(typeof datum, 'string'); 261 | if (typeof datum !== 'string') 262 | throw new Error(); 263 | assert.equal(datum.length > 10, true); 264 | } 265 | }); 266 | 267 | test('insights look ok. ' + filename, t => { 268 | if (insights === null) { 269 | throw new Error('insights null'); 270 | } 271 | const insightSet = Array.from(insights.values()).at(-1); 272 | if (typeof insightSet === 'undefined') { 273 | throw new Error(); 274 | } 275 | const keys = Object.keys(insightSet.model); 276 | assert.deepStrictEqual(keys, [ 277 | 'INPBreakdown', 278 | 'LCPBreakdown', 279 | 'LCPDiscovery', 280 | 'CLSCulprits', 281 | 'RenderBlocking', 282 | 'NetworkDependencyTree', 283 | 'ImageDelivery', 284 | 'DocumentLatency', 285 | 'FontDisplay', 286 | 'Viewport', 287 | 'DOMSize', 288 | 'ThirdParties', 289 | 'DuplicatedJavaScript', 290 | 'SlowCSSSelector', 291 | 'ForcedReflow', 292 | 'Cache', 293 | 'ModernHTTP', 294 | 'LegacyJavaScript', 295 | ]); 296 | for (const [insightName, insightItem] of Object.entries(insightSet.model)) { 297 | const msg = insightItem instanceof Error ? 298 | `${insightName} is an error. ${insightItem.toString()} ${insightItem.stack?.toString()}` : 299 | ''; 300 | assert.ok(insightItem instanceof Error === false, msg); 301 | assert.ok(typeof insightItem === 'object', `insightName ${insightName} is not an object`); 302 | } 303 | 304 | }); 305 | 306 | test('bottom-up summary is good. ' + filename, t => { 307 | const parsedTrace = data; 308 | const visibleEvents = Trace.Helpers.Trace.VISIBLE_TRACE_EVENT_TYPES.values().toArray(); 309 | const filter = new Trace.Extras.TraceFilter.VisibleEventsFilter( 310 | visibleEvents.concat([Trace.Types.Events.Name.SYNTHETIC_NETWORK_REQUEST])); 311 | const milliBounds = Trace.Helpers.Timing.traceWindowMilliSeconds(parsedTrace.Meta.traceBounds); 312 | 313 | 314 | const mainThreadProbably = 315 | Trace.Handlers.Threads.threadsInTrace(parsedTrace) 316 | .filter(t => t.type === Trace.Handlers.Threads.ThreadType.MAIN_THREAD && t.processIsOnMainFrame) 317 | .sort((a, b) => b.entries.length - a.entries.length) 318 | .at(0); 319 | if (!mainThreadProbably) 320 | assert.fail('No main thread found in trace'); 321 | 322 | /** @param {Trace.Types.Events.Event} event */ 323 | const groupingFunction = event => event.name; 324 | 325 | const node = new Trace.Extras.TraceTree.BottomUpRootNode([...mainThreadProbably.entries], { 326 | textFilter: new Trace.Extras.TraceFilter.ExclusiveNameFilter([]), 327 | filters: [filter], 328 | startTime: milliBounds.min, 329 | endTime: milliBounds.max, 330 | eventGroupIdCallback: groupingFunction, 331 | }); 332 | 333 | const bottomUpByName = 334 | Array.from(node.children().values()) 335 | .map(c => [c.id.toString().padEnd(30), c.selfTime.toLocaleString().padStart(10) + 'ms'].join('\t')); 336 | assert.ok(bottomUpByName); 337 | }); 338 | 339 | 340 | 341 | } 342 | 343 | 344 | // extra parsing handling that I may want in the future 345 | // let trace; 346 | // try { 347 | // text = text.trim(); 348 | // if (text.length === 0) { 349 | // console.log('❌ ... empty file', filename); return; 350 | // } 351 | // const firstChar = text.at(0); 352 | // if (firstChar !== '{' && firstChar !== '[') { 353 | // console.log('😞 ... Does not look like json', filename, text.slice(0, 200).replace(/\n/g, ' ')); 354 | // return; 355 | // } 356 | // trace = JSON.parse(text); 357 | // text = undefined; 358 | // } catch (e) { 359 | // if (text.at(-1) === ',') { 360 | // text = text.slice(0, text.length - 1) + ']'; 361 | // console.log(' ... JSON ending with a comma, trying to fix it...', filename); 362 | // return parseTraceText(filename); 363 | // } 364 | // console.log('❌ ... invalid json', filename, e.message); return; 365 | // } 366 | 367 | // if (Array.isArray(trace)) { 368 | // trace = { 369 | // traceEvents: trace, 370 | // }; 371 | // } -------------------------------------------------------------------------------- /third_party/cpu-profile-model.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license Copyright 2020 The Lighthouse Authors. All Rights Reserved. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 4 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 5 | */ 6 | 7 | import {MainThreadTasks} from './main-thread-tasks.js'; 8 | 9 | const SAMPLER_TRACE_EVENT_NAME = 'FunctionCall-SynthesizedByProfilerModel'; 10 | 11 | /** 12 | * @fileoverview 13 | * 14 | * This model converts the `Profile` and `ProfileChunk` mega trace events from the `disabled-by-default-v8.cpu_profiler` 15 | * category into B/E-style trace events that main-thread-tasks.js already knows how to parse into a task tree. 16 | * 17 | * The V8 CPU profiler measures where time is being spent by sampling the stack (See https://www.jetbrains.com/help/profiler/Profiling_Guidelines__Choosing_the_Right_Profiling_Mode.html 18 | * for a generic description of the differences between tracing and sampling). 19 | * 20 | * A `Profile` event is a record of the stack that was being executed at different sample points in time. 21 | * It has a structure like this: 22 | * 23 | * nodes: [function A, function B, function C] 24 | * samples: [node with id 2, node with id 1, ...] 25 | * timeDeltas: [4125μs since last sample, 121μs since last sample, ...] 26 | * 27 | * Note that this is subtly different from the protocol-based Crdp.Profiler.Profile type. 28 | * 29 | * Helpful prior art: 30 | * @see https://cs.chromium.org/chromium/src/third_party/devtools-frontend/src/front_end/sdk/CPUProfileDataModel.js?sq=package:chromium&g=0&l=42 31 | * @see https://github.com/v8/v8/blob/99ca333b0efba3236954b823101315aefeac51ab/tools/profile.js 32 | * @see https://github.com/jlfwong/speedscope/blob/9ed1eb192cb7e9dac43a5f25bd101af169dc654a/src/import/chrome.ts#L200 33 | */ 34 | 35 | /** 36 | * @typedef CpuProfile 37 | * @property {string} id 38 | * @property {number} pid 39 | * @property {number} tid 40 | * @property {number} startTime 41 | * @property {Required['nodes']} nodes 42 | * @property {Array} samples 43 | * @property {Array} timeDeltas 44 | */ 45 | 46 | /** @typedef {Required['data']>['_syntheticProfilerRange']} ProfilerRange */ 47 | /** @typedef {LH.TraceEvent & {args: {data: {_syntheticProfilerRange: ProfilerRange}}}} SynthethicEvent */ 48 | /** @typedef {Omit & {event: SynthethicEvent, endEvent: SynthethicEvent}} SynthethicTaskNode */ 49 | 50 | class CpuProfileModel { 51 | /** 52 | * @param {CpuProfile} profile 53 | */ 54 | constructor(profile) { 55 | this._profile = profile; 56 | this._nodesById = this._createNodeMap(); 57 | this._activeNodeArraysById = this._createActiveNodeArrays(); 58 | } 59 | 60 | /** 61 | * Initialization function to enable O(1) access to nodes by node ID. 62 | * @return {Map} 63 | */ 64 | _createNodeMap() { 65 | /** @type {Map} */ 66 | const map = new Map(); 67 | for (const node of this._profile.nodes) { 68 | map.set(node.id, node); 69 | } 70 | 71 | return map; 72 | } 73 | 74 | /** 75 | * Initialization function to enable O(1) access to the set of active nodes in the stack by node ID. 76 | * @return {Map>} 77 | */ 78 | _createActiveNodeArrays() { 79 | /** @type {Map>} */ 80 | const map = new Map(); 81 | /** @param {number} id @return {Array} */ 82 | const getActiveNodes = id => { 83 | if (map.has(id)) return map.get(id) || []; 84 | 85 | const node = this._nodesById.get(id); 86 | if (!node) throw new Error(`No such node ${id}`); 87 | if (typeof node.parent === 'number') { 88 | const array = getActiveNodes(node.parent).concat([id]); 89 | map.set(id, array); 90 | return array; 91 | } else { 92 | return [id]; 93 | } 94 | }; 95 | 96 | for (const node of this._profile.nodes) { 97 | map.set(node.id, getActiveNodes(node.id)); 98 | } 99 | 100 | return map; 101 | } 102 | 103 | /** 104 | * Returns all the node IDs in a stack when a specific nodeId is at the top of the stack 105 | * (i.e. a stack's node ID and the node ID of all of its parents). 106 | * 107 | * @param {number} nodeId 108 | * @return {Array} 109 | */ 110 | _getActiveNodeIds(nodeId) { 111 | const activeNodeIds = this._activeNodeArraysById.get(nodeId); 112 | if (!activeNodeIds) throw new Error(`No such node ID ${nodeId}`); 113 | return activeNodeIds; 114 | } 115 | 116 | /** 117 | * Generates the necessary B/E-style trace events for a single transition from stack A to stack B 118 | * at the given latest timestamp (includes possible range in event.args.data). 119 | * 120 | * Example: 121 | * 122 | * latestPossibleTimestamp 1234 123 | * previousNodeIds 1,2,3 124 | * currentNodeIds 1,2,4 125 | * 126 | * yields [end 3 at ts 1234, begin 4 at ts 1234] 127 | * 128 | * @param {number} earliestPossibleTimestamp 129 | * @param {number} latestPossibleTimestamp 130 | * @param {Array} previousNodeIds 131 | * @param {Array} currentNodeIds 132 | * @return {Array} 133 | */ 134 | _synthesizeTraceEventsForTransition( 135 | earliestPossibleTimestamp, 136 | latestPossibleTimestamp, 137 | previousNodeIds, 138 | currentNodeIds 139 | ) { 140 | const startNodes = currentNodeIds 141 | .filter(id => !previousNodeIds.includes(id)) 142 | .map(id => this._nodesById.get(id)) 143 | .filter(/** @return {node is CpuProfile['nodes'][0]} */ node => !!node); 144 | const endNodes = previousNodeIds 145 | .filter(id => !currentNodeIds.includes(id)) 146 | .map(id => this._nodesById.get(id)) 147 | .filter(/** @return {node is CpuProfile['nodes'][0]} */ node => !!node); 148 | 149 | /** @param {CpuProfile['nodes'][0]} node @return {SynthethicEvent} */ 150 | const createSyntheticEvent = node => ({ 151 | ts: Number.isFinite(latestPossibleTimestamp) 152 | ? latestPossibleTimestamp 153 | : earliestPossibleTimestamp, 154 | pid: this._profile.pid, 155 | tid: this._profile.tid, 156 | dur: 0, 157 | ph: 'I', 158 | // This trace event name is Lighthouse-specific and wouldn't be found in a real trace. 159 | // Attribution logic in main-thread-tasks.js special cases this event. 160 | name: SAMPLER_TRACE_EVENT_NAME, 161 | cat: 'lighthouse', 162 | args: { 163 | data: { 164 | callFrame: node.callFrame, 165 | _syntheticProfilerRange: {earliestPossibleTimestamp, latestPossibleTimestamp}, 166 | }, 167 | }, 168 | }); 169 | 170 | /** @type {Array} */ 171 | const startEvents = startNodes.map(createSyntheticEvent).map(evt => ({...evt, ph: 'B'})); 172 | /** @type {Array} */ 173 | const endEvents = endNodes.map(createSyntheticEvent).map(evt => ({...evt, ph: 'E'})); 174 | // Ensure we put end events in first to finish prior tasks before starting new ones. 175 | return [...endEvents.reverse(), ...startEvents]; 176 | } 177 | 178 | /** 179 | * @param {LH.TraceEvent | undefined} event 180 | * @return {event is SynthethicEvent} 181 | */ 182 | static isSyntheticEvent(event) { 183 | if (!event) return false; 184 | return Boolean( 185 | event.name === SAMPLER_TRACE_EVENT_NAME && 186 | event.args.data?._syntheticProfilerRange 187 | ); 188 | } 189 | 190 | /** 191 | * @param {LH.Artifacts.TaskNode} task 192 | * @return {task is SynthethicTaskNode} 193 | */ 194 | static isSyntheticTask(task) { 195 | return CpuProfileModel.isSyntheticEvent(task.event) && 196 | CpuProfileModel.isSyntheticEvent(task.endEvent); 197 | } 198 | 199 | /** 200 | * Finds all the tasks that started or ended (depending on `type`) within the provided time range. 201 | * Uses a memory index to remember the place in the array the last invocation left off to avoid 202 | * re-traversing the entire array, but note that this index might still be slightly off from the 203 | * true start position. 204 | * 205 | * @param {Array<{startTime: number, endTime: number}>} knownTasks 206 | * @param {{type: 'startTime'|'endTime', initialIndex: number, earliestPossibleTimestamp: number, latestPossibleTimestamp: number}} options 207 | */ 208 | static _getTasksInRange(knownTasks, options) { 209 | const {type, initialIndex, earliestPossibleTimestamp, latestPossibleTimestamp} = options; 210 | 211 | // We may have overshot a little from last time, so back up to find the real starting index. 212 | let startIndex = initialIndex; 213 | while (startIndex > 0) { 214 | const task = knownTasks[startIndex]; 215 | if (task && task[type] < earliestPossibleTimestamp) break; 216 | startIndex--; 217 | } 218 | 219 | /** @type {Array<{startTime: number, endTime: number}>} */ 220 | const matchingTasks = []; 221 | for (let i = startIndex; i < knownTasks.length; i++) { 222 | const task = knownTasks[i]; 223 | // Task is before our range of interest, keep looping. 224 | if (task[type] < earliestPossibleTimestamp) continue; 225 | 226 | // Task is after our range of interest, we're done. 227 | if (task[type] > latestPossibleTimestamp) { 228 | return {tasks: matchingTasks, lastIndex: i}; 229 | } 230 | 231 | // Task is in our range of interest, add it to our list. 232 | matchingTasks.push(task); 233 | } 234 | 235 | // We went through all tasks before reaching the end of our range. 236 | return {tasks: matchingTasks, lastIndex: knownTasks.length}; 237 | } 238 | 239 | /** 240 | * Given a particular time range and a set of known true tasks, find the correct timestamp to use 241 | * for a transition between tasks. 242 | * 243 | * Because the sampling profiler only provides a *range* of start/stop function boundaries, this 244 | * method uses knowledge of a known set of tasks to find the most accurate timestamp for a particular 245 | * range. For example, if we know that a function ended between 800ms and 810ms, we can use the 246 | * knowledge that a toplevel task ended at 807ms to use 807ms as the correct endtime for this function. 247 | * 248 | * @param {{syntheticTask: SynthethicTaskNode, eventType: 'start'|'end', allEventsAtTs: {naive: Array, refined: Array}, knownTaskStartTimeIndex: number, knownTaskEndTimeIndex: number, knownTasksByStartTime: Array<{startTime: number, endTime: number}>, knownTasksByEndTime: Array<{startTime: number, endTime: number}>}} data 249 | * @return {{timestamp: number, lastStartTimeIndex: number, lastEndTimeIndex: number}} 250 | */ 251 | static _findEffectiveTimestamp(data) { 252 | const { 253 | eventType, 254 | syntheticTask, 255 | allEventsAtTs, 256 | knownTasksByStartTime, 257 | knownTaskStartTimeIndex, 258 | knownTasksByEndTime, 259 | knownTaskEndTimeIndex, 260 | } = data; 261 | 262 | const targetEvent = eventType === 'start' ? syntheticTask.event : syntheticTask.endEvent; 263 | const pairEvent = eventType === 'start' ? syntheticTask.endEvent : syntheticTask.event; 264 | 265 | const timeRange = targetEvent.args.data._syntheticProfilerRange; 266 | const pairTimeRange = pairEvent.args.data._syntheticProfilerRange; 267 | 268 | const {tasks: knownTasksStarting, lastIndex: lastStartTimeIndex} = this._getTasksInRange( 269 | knownTasksByStartTime, 270 | { 271 | type: 'startTime', 272 | initialIndex: knownTaskStartTimeIndex, 273 | earliestPossibleTimestamp: timeRange.earliestPossibleTimestamp, 274 | latestPossibleTimestamp: timeRange.latestPossibleTimestamp, 275 | } 276 | ); 277 | 278 | const {tasks: knownTasksEnding, lastIndex: lastEndTimeIndex} = this._getTasksInRange( 279 | knownTasksByEndTime, 280 | { 281 | type: 'endTime', 282 | initialIndex: knownTaskEndTimeIndex, 283 | earliestPossibleTimestamp: timeRange.earliestPossibleTimestamp, 284 | latestPossibleTimestamp: timeRange.latestPossibleTimestamp, 285 | } 286 | ); 287 | 288 | // First, find all the tasks that span *across* (not fully contained within) our ambiguous range. 289 | const knownTasksStartingNotContained = knownTasksStarting 290 | .filter(t => !knownTasksEnding.includes(t)); 291 | const knownTasksEndingNotContained = knownTasksEnding 292 | .filter(t => !knownTasksStarting.includes(t)); 293 | 294 | // Each one of these spanning tasks can be in one of three situations: 295 | // - Task is a parent of the sample. 296 | // - Task is a child of the sample. 297 | // - Task has no overlap with the sample. 298 | 299 | // Parent tasks must satisfy... 300 | // parentTask.startTime <= syntheticTask.startTime 301 | // AND 302 | // syntheticTask.endTime <= parentTask.endTime 303 | const parentTasks = 304 | eventType === 'start' 305 | ? knownTasksStartingNotContained.filter( 306 | t => t.endTime >= pairTimeRange.earliestPossibleTimestamp 307 | ) 308 | : knownTasksEndingNotContained.filter( 309 | t => t.startTime <= pairTimeRange.latestPossibleTimestamp 310 | ); 311 | 312 | // Child tasks must satisfy... 313 | // syntheticTask.startTime <= childTask.startTime 314 | // AND 315 | // childTask.endTime <= syntheticTask.endTime 316 | const childTasks = 317 | eventType === 'start' 318 | ? knownTasksStartingNotContained.filter( 319 | t => t.endTime < pairTimeRange.earliestPossibleTimestamp 320 | ) 321 | : knownTasksEndingNotContained.filter( 322 | t => t.startTime > pairTimeRange.latestPossibleTimestamp 323 | ); 324 | 325 | // Unrelated tasks must satisfy... 326 | // unrelatedTask.endTime <= syntheticTask.startTime 327 | // OR 328 | // syntheticTask.endTime <= unrelatedTask.startTime 329 | const unrelatedTasks = 330 | eventType === 'start' ? knownTasksEndingNotContained : knownTasksStartingNotContained; 331 | 332 | // Now we narrow our allowable range using the three types of tasks and the other events 333 | // that we've already refined. 334 | const minimumTs = Math.max( 335 | // Sampled event couldn't be earlier than this to begin with. 336 | timeRange.earliestPossibleTimestamp, 337 | // Sampled start event can't be before its parent started. 338 | // Sampled end event can't be before its child ended. 339 | ...(eventType === 'start' 340 | ? parentTasks.map(t => t.startTime) 341 | : childTasks.map(t => t.endTime)), 342 | // Sampled start event can't be before unrelated tasks ended. 343 | ...(eventType === 'start' ? unrelatedTasks.map(t => t.endTime) : []), 344 | // Sampled start event can't be before the other `E` events at its same timestamp. 345 | ...(eventType === 'start' 346 | ? allEventsAtTs.refined.filter(e => e.ph === 'E').map(e => e.ts) 347 | : []) 348 | ); 349 | 350 | const maximumTs = Math.min( 351 | // Sampled event couldn't be later than this to begin with. 352 | timeRange.latestPossibleTimestamp, 353 | // Sampled start event can't be after its child started. 354 | // Sampled end event can't be after its parent ended. 355 | ...(eventType === 'start' 356 | ? childTasks.map(t => t.startTime) 357 | : parentTasks.map(t => t.endTime)), 358 | // Sampled end event can't be after unrelated tasks started. 359 | ...(eventType === 'start' ? [] : unrelatedTasks.map(t => t.startTime)), 360 | // Sampled end event can't be after the other `B` events at its same timestamp. 361 | // This is _currently_ only possible in contrived scenarios due to the sorted order of processing, 362 | // but it's a non-obvious observation and case to account for. 363 | ...(eventType === 'start' 364 | ? [] 365 | : allEventsAtTs.refined.filter(e => e.ph === 'B').map(e => e.ts)) 366 | ); 367 | 368 | // We want to maximize the size of the sampling tasks within our constraints, so we'll pick 369 | // the _earliest_ possible time for start events and the _latest_ possible time for end events. 370 | const effectiveTimestamp = 371 | (eventType === 'start' && Number.isFinite(minimumTs)) || !Number.isFinite(maximumTs) 372 | ? minimumTs 373 | : maximumTs; 374 | 375 | return {timestamp: effectiveTimestamp, lastStartTimeIndex, lastEndTimeIndex}; 376 | } 377 | 378 | /** 379 | * Creates the B/E-style trace events using only data from the profile itself. Each B/E event will 380 | * include the actual _range_ the timestamp could have been in its metadata that is used for 381 | * refinement later. 382 | * 383 | * @return {Array} 384 | */ 385 | _synthesizeNaiveTraceEvents() { 386 | const profile = this._profile; 387 | const length = profile.samples.length; 388 | if (profile.timeDeltas.length !== length) throw new Error(`Invalid CPU profile length`); 389 | 390 | /** @type {Array} */ 391 | const events = []; 392 | 393 | let currentProfilerTimestamp = profile.startTime; 394 | let earliestPossibleTimestamp = -Infinity; 395 | 396 | /** @type {Array} */ 397 | let lastActiveNodeIds = []; 398 | for (let i = 0; i < profile.samples.length; i++) { 399 | const nodeId = profile.samples[i]; 400 | const timeDelta = Math.max(profile.timeDeltas[i], 1); 401 | const node = this._nodesById.get(nodeId); 402 | if (!node) throw new Error(`Missing node ${nodeId}`); 403 | 404 | currentProfilerTimestamp += timeDelta; 405 | 406 | const activeNodeIds = this._getActiveNodeIds(nodeId); 407 | events.push( 408 | ...this._synthesizeTraceEventsForTransition( 409 | earliestPossibleTimestamp, 410 | currentProfilerTimestamp, 411 | lastActiveNodeIds, 412 | activeNodeIds 413 | ) 414 | ); 415 | 416 | earliestPossibleTimestamp = currentProfilerTimestamp; 417 | lastActiveNodeIds = activeNodeIds; 418 | } 419 | 420 | events.push( 421 | ...this._synthesizeTraceEventsForTransition( 422 | currentProfilerTimestamp, 423 | Infinity, 424 | lastActiveNodeIds, 425 | [] 426 | ) 427 | ); 428 | 429 | return events; 430 | } 431 | 432 | /** 433 | * Creates a copy of B/E-style trace events with refined timestamps using knowledge from the 434 | * tasks that have definitive timestamps. 435 | * 436 | * With the sampling profiler we know that a function started/ended _sometime between_ two points, 437 | * but not exactly when. Using the information from other tasks gives us more information to be 438 | * more precise with timings and allows us to create a valid task tree later on. 439 | * 440 | * @param {Array<{startTime: number, endTime: number}>} knownTasks 441 | * @param {Array} syntheticTasks 442 | * @param {Array} syntheticEvents 443 | * @return {Array} 444 | */ 445 | _refineTraceEventsWithTasks(knownTasks, syntheticTasks, syntheticEvents) { 446 | /** @type {Array} */ 447 | const refinedEvents = []; 448 | 449 | /** @type {Map, refined: Array}>} */ 450 | const syntheticEventsByTs = new Map(); 451 | for (const event of syntheticEvents) { 452 | const group = syntheticEventsByTs.get(event.ts) || {naive: [], refined: []}; 453 | group.naive.push(event); 454 | syntheticEventsByTs.set(event.ts, group); 455 | } 456 | 457 | /** @type {Map} */ 458 | const syntheticTasksByEvent = new Map(); 459 | for (const task of syntheticTasks) { 460 | syntheticTasksByEvent.set(task.event, task); 461 | syntheticTasksByEvent.set(task.endEvent, task); 462 | } 463 | 464 | const knownTasksByStartTime = knownTasks.slice().sort((a, b) => a.startTime - b.startTime); 465 | const knownTasksByEndTime = knownTasks.slice().sort((a, b) => a.endTime - b.endTime); 466 | 467 | let knownTaskStartTimeIndex = 0; 468 | let knownTaskEndTimeIndex = 0; 469 | 470 | for (const event of syntheticEvents) { 471 | const syntheticTask = syntheticTasksByEvent.get(event); 472 | if (!syntheticTask) throw new Error('Impossible - all events have a task'); 473 | const allEventsAtTs = syntheticEventsByTs.get(event.ts); 474 | if (!allEventsAtTs) throw new Error('Impossible - we just mapped every event'); 475 | 476 | const effectiveTimestampData = CpuProfileModel._findEffectiveTimestamp({ 477 | eventType: event.ph === 'B' ? 'start' : 'end', 478 | syntheticTask, 479 | allEventsAtTs, 480 | knownTaskStartTimeIndex, 481 | knownTaskEndTimeIndex, 482 | knownTasksByStartTime, 483 | knownTasksByEndTime, 484 | }); 485 | 486 | knownTaskStartTimeIndex = effectiveTimestampData.lastStartTimeIndex; 487 | knownTaskEndTimeIndex = effectiveTimestampData.lastEndTimeIndex; 488 | 489 | const refinedEvent = {...event, ts: effectiveTimestampData.timestamp}; 490 | refinedEvents.push(refinedEvent); 491 | allEventsAtTs.refined.push(refinedEvent); 492 | } 493 | 494 | return refinedEvents; 495 | } 496 | 497 | /** 498 | * Creates B/E-style trace events from a CpuProfile object created by `collectProfileEvents()`. 499 | * An optional set of tasks can be passed in to refine the start/end times. 500 | * 501 | * @param {Array} [knownTaskNodes] 502 | * @return {Array} 503 | */ 504 | synthesizeTraceEvents(knownTaskNodes = []) { 505 | const naiveEvents = this._synthesizeNaiveTraceEvents(); 506 | if (!naiveEvents.length) return []; 507 | 508 | let finalEvents = naiveEvents; 509 | if (knownTaskNodes.length) { 510 | // If we have task information, put the times back into raw trace event ts scale. 511 | /** @type {(baseTs: number) => (node: LH.Artifacts.TaskNode) => LH.Artifacts.TaskNode} */ 512 | const rebaseTaskTime = baseTs => node => ({ 513 | ...node, 514 | startTime: baseTs + node.startTime * 1000, 515 | endTime: baseTs + node.endTime * 1000, 516 | duration: node.duration * 1000, 517 | }); 518 | 519 | // The first task node might not be time 0, so recompute the baseTs. 520 | const baseTs = knownTaskNodes[0].event.ts - knownTaskNodes[0].startTime * 1000; 521 | const knownTasks = knownTaskNodes.map(rebaseTaskTime(baseTs)); 522 | 523 | // We'll also create tasks for our naive events so we have the B/E pairs readily available. 524 | const naiveProfilerTasks = MainThreadTasks.getMainThreadTasks(naiveEvents, [], Infinity) 525 | .map(rebaseTaskTime(naiveEvents[0].ts)) 526 | .filter(CpuProfileModel.isSyntheticTask); 527 | if (!naiveProfilerTasks.length) throw new Error('Failed to create naive profiler tasks'); 528 | 529 | finalEvents = this._refineTraceEventsWithTasks(knownTasks, naiveProfilerTasks, naiveEvents); 530 | } 531 | 532 | return finalEvents; 533 | } 534 | 535 | /** 536 | * Creates B/E-style trace events from a CpuProfile object created by `collectProfileEvents()` 537 | * 538 | * @param {CpuProfile} profile 539 | * @param {Array} tasks 540 | * @return {Array} 541 | */ 542 | static synthesizeTraceEvents(profile, tasks) { 543 | const model = new CpuProfileModel(profile); 544 | return model.synthesizeTraceEvents(tasks); 545 | } 546 | 547 | /** 548 | * Merges the data of all the `ProfileChunk` trace events into a single CpuProfile object for consumption 549 | * by `synthesizeTraceEvents()`. 550 | * 551 | * @param {Array} traceEvents 552 | * @return {Array} 553 | */ 554 | static collectProfileEvents(traceEvents) { 555 | /** @type {Map} */ 556 | const profiles = new Map(); 557 | for (const event of traceEvents) { 558 | if (event.name !== 'Profile' && event.name !== 'ProfileChunk') continue; 559 | if (typeof event.id !== 'string') continue; 560 | 561 | // `Profile` or `ProfileChunk` can partially define these across multiple events. 562 | // We'll fallback to empty values and worry about validation in the `synthesizeTraceEvents` phase. 563 | const cpuProfileArg = event.args.data?.cpuProfile || {}; 564 | const timeDeltas = event.args.data?.timeDeltas || cpuProfileArg.timeDeltas; 565 | let profile = profiles.get(event.id); 566 | 567 | if (event.name === 'Profile') { 568 | profile = { 569 | id: event.id, 570 | pid: event.pid, 571 | tid: event.tid, 572 | startTime: event.args.data?.startTime || event.ts, 573 | nodes: cpuProfileArg.nodes || [], 574 | samples: cpuProfileArg.samples || [], 575 | timeDeltas: timeDeltas || [], 576 | }; 577 | } else { 578 | if (!profile) continue; 579 | profile.nodes.push(...(cpuProfileArg.nodes || [])); 580 | profile.samples.push(...(cpuProfileArg.samples || [])); 581 | profile.timeDeltas.push(...(timeDeltas || [])); 582 | } 583 | 584 | profiles.set(profile.id, profile); 585 | } 586 | 587 | return Array.from(profiles.values()); 588 | } 589 | } 590 | 591 | export {CpuProfileModel}; 592 | -------------------------------------------------------------------------------- /extract-netlog-from-trace.mjs: -------------------------------------------------------------------------------- 1 | // Extract netlog from a trace. 2 | // 3 | // In perfetto, "convert to json" 4 | // Run: 5 | // node ./extract-netlog-from-trace.mjs ~/Downloads/Trace.json 6 | // If there are errors about file too big, sorry. You can try removing all non-netlog trace events from trace. 7 | 8 | import path from 'node:path'; 9 | import {loadTraceEventsFromFile, saveNetlog} from './trace-file-utils.mjs'; 10 | 11 | /** @typedef {import('./types/chromium-trace').TraceEvent} TraceEvent */ 12 | /** 13 | * @typedef Netlog 14 | * @property {string} time 15 | * @property {number} type 16 | * @property {{id: number, type: number, start_time: string}} source 17 | * @property {number} phase 18 | * @property {undefined|Record} params 19 | */ 20 | 21 | // This is in Constants in a real netlog 22 | const logEntryPhase = { 23 | PHASE_BEGIN: 1, 24 | PHASE_END: 2, 25 | PHASE_NONE: 0, 26 | }; 27 | 28 | const traceEventPhaseTologEntryhase = { 29 | b: 'PHASE_BEGIN', 30 | e: 'PHASE_END', 31 | n: 'PHASE_NONE', 32 | }; 33 | 34 | // https://source.chromium.org/chromium/chromium/src/+/main:net/log/net_log_event_type_list.h 35 | /** @type {string[]} */ 36 | const logEntryTypes = []; 37 | // https://source.chromium.org/chromium/chromium/src/+/main:net/log/net_log_source_type_list.h;l=15-58;drc=c27c88438da17b5103e304d807846f9b45067c40 38 | const sourceTypes = ['NONE']; 39 | let minTime = undefined; 40 | 41 | /** 42 | * Basically the reverse of this https://source.chromium.org/chromium/chromium/src/+/main:net/log/trace_net_log_observer.cc;l=56-80;drc=6bfd45d97e9a780d2d5a6f04be930131848eb0b2 43 | * See also https://source.chromium.org/chromium/chromium/src/+/main:net/log/net_log_entry.cc 44 | * @param {TraceEvent} event 45 | */ 46 | function eventToLogEntry(event) { 47 | if (event.cat !== 'netlog') return false; 48 | if (!minTime) { 49 | minTime = event.ts; 50 | } 51 | 52 | // build out these ENUM lookups and grab the index in the entry 53 | logEntryTypes.includes(event.name) || logEntryTypes.push(event.name); 54 | const logEntryTypeIndex = logEntryTypes.indexOf(event.name); 55 | sourceTypes.includes(event.args.source_type) || sourceTypes.push(event.args.source_type); 56 | const sourceTypeIndex = sourceTypes.indexOf(event.args.source_type); 57 | 58 | // SAD. the value of `time` (or `source.start_time`) is lost. This is approximate. 59 | // We don't _need_ to subtract off minTime, but the bonus is that the timestamps in netlog Viewer match up with the timestamps written to file. 60 | const timeMs = Math.floor((event.ts - minTime) / 1000).toString(); 61 | 62 | // https://www.chromium.org/developers/design-documents/network-stack/netlog/#:~:Terminology 63 | /** @type Netlog */ 64 | const logEntry = { 65 | time: timeMs, 66 | type: logEntryTypeIndex, 67 | source: { 68 | // the source.id is used as event id. but perfetto strings it into hex. maybe this is fine. // https://source.chromium.org/chromium/chromium/src/+/main:net/log/trace_net_log_observer.cc;l=62;drc=6bfd45d97e9a780d2d5a6f04be930131848eb0b2 69 | id: parseInt(event.id), 70 | type: sourceTypeIndex, 71 | start_time: timeMs, 72 | }, 73 | phase: logEntryPhase[traceEventPhaseTologEntryhase[event.ph]], 74 | params: event.args.params, 75 | }; 76 | return logEntry; 77 | } 78 | 79 | /** @param {TraceEvent[]} traceEvents */ 80 | function makeConstants(traceEvents) { 81 | const metadata = traceEvents.metadata; 82 | const timeTickOffset = new Date(metadata['trace-capture-datetime']).valueOf(); 83 | 84 | const constants = { 85 | ...myConstants(), 86 | logEventTypes: Object.fromEntries(Array.from(logEntryTypes.entries()).map(([index, type]) => [type, index])), 87 | logSourceType: Object.fromEntries(Array.from(sourceTypes.entries()).map(([index, type]) => [type, index])), 88 | logEventPhase: logEntryPhase, 89 | // Refers to ms since epoch for the start of the netlog. 90 | timeTickOffset, 91 | }; 92 | return constants; 93 | } 94 | 95 | /** @param {TraceEvent[]} traceEvents */ 96 | function extractNetlog(traceEvents) { 97 | traceEvents.sort((a, b) => a.ts - b.ts); 98 | const events = traceEvents.map(eventToLogEntry).filter(Boolean); 99 | // TODO: recreate some of `polledData` ?! seems nearly impossible tho 100 | const constants = makeConstants(traceEvents); 101 | return { 102 | constants, 103 | events, 104 | }; 105 | } 106 | 107 | // CLI direct invocation? 108 | if (import.meta.url.endsWith(process?.argv[1])) { 109 | cli(); 110 | } 111 | 112 | async function cli() { 113 | const filename = path.resolve(process.cwd(), process.argv[2]); 114 | 115 | const traceEvents = loadTraceEventsFromFile(filename); 116 | const netlog = extractNetlog(traceEvents); 117 | console.log(`counts: 118 | eventTypes: ${Object.keys(netlog.constants.logEventTypes).length}, 119 | sourceTypes: ${Object.keys(netlog.constants.logSourceType).length}, 120 | events: ${netlog.events.length.toLocaleString()}`); 121 | 122 | const netlogFilename = `${filename}.netlog.json`; 123 | await saveNetlog(netlog, netlogFilename); 124 | console.log('Wrote ' + netlogFilename + ' to disk. ' + new Date()); 125 | } 126 | 127 | function myConstants() { 128 | return { 129 | activeFieldTrialGroups: ['Redacted'], 130 | addressFamily: { 131 | ADDRESS_FAMILY_IPV4: 1, 132 | ADDRESS_FAMILY_IPV6: 2, 133 | ADDRESS_FAMILY_UNSPECIFIED: 0, 134 | }, 135 | certPathBuilderDigestPolicy: { 136 | kStrong: 0, 137 | kWeakAllowSha1: 1, 138 | }, 139 | certStatusFlag: { 140 | AUTHORITY_INVALID: 4, 141 | CERTIFICATE_TRANSPARENCY_REQUIRED: 16777216, 142 | COMMON_NAME_INVALID: 1, 143 | DATE_INVALID: 2, 144 | INVALID: 128, 145 | IS_EV: 65536, 146 | KNOWN_INTERCEPTION_BLOCKED: 67108864, 147 | KNOWN_INTERCEPTION_DETECTED: 2097152, 148 | NAME_CONSTRAINT_VIOLATION: 16384, 149 | NON_UNIQUE_NAME: 1024, 150 | NO_REVOCATION_MECHANISM: 16, 151 | PINNED_KEY_MISSING: 8192, 152 | REVOKED: 64, 153 | REV_CHECKING_ENABLED: 131072, 154 | SHA1_SIGNATURE_PRESENT: 524288, 155 | SYMANTEC_LEGACY: 33554432, 156 | UNABLE_TO_CHECK_REVOCATION: 32, 157 | VALIDITY_TOO_LONG: 32768, 158 | WEAK_KEY: 2048, 159 | WEAK_SIGNATURE_ALGORITHM: 256, 160 | }, 161 | certVerifierFlags: { 162 | VERIFY_DISABLE_NETWORK_FETCHES: 1, 163 | }, 164 | certVerifyFlags: { 165 | VERIFY_DISABLE_NETWORK_FETCHES: 16, 166 | VERIFY_DISABLE_SYMANTEC_ENFORCEMENT: 8, 167 | VERIFY_ENABLE_SHA1_LOCAL_ANCHORS: 4, 168 | VERIFY_REV_CHECKING_ENABLED: 1, 169 | VERIFY_REV_CHECKING_REQUIRED_LOCAL_ANCHORS: 2, 170 | }, 171 | clientInfo: { 172 | cl: 'deadbeef-refs/branch-heads/6699@{#1400}', 173 | command_line: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome --flag-switches-begin --flag-switches-end', 174 | name: 'Google Chrome', 175 | official: 'official', 176 | os_type: 'Mac OS X: 14.6.1 (x86_64)', 177 | version: '128.0.6613.120', 178 | version_mod: '', 179 | }, 180 | dnsQueryType: { 181 | A: 1, 182 | AAAA: 2, 183 | HTTPS: 7, 184 | PTR: 4, 185 | SRV: 5, 186 | TXT: 3, 187 | UNSPECIFIED: 0, 188 | }, 189 | loadFlag: { 190 | BYPASS_CACHE: 2, 191 | BYPASS_PROXY: 128, 192 | CAN_USE_RESTRICTED_PREFETCH: 65536, 193 | CAN_USE_SHARED_DICTIONARY: 131072, 194 | DISABLE_CACHE: 16, 195 | DISABLE_CERT_NETWORK_FETCHES: 32, 196 | DISABLE_CONNECTION_MIGRATION_TO_CELLULAR: 4096, 197 | DISABLE_SHARED_DICTIONARY_AFTER_CROSS_ORIGIN_REDIRECT: 262144, 198 | DO_NOT_SAVE_COOKIES: 64, 199 | DO_NOT_USE_EMBEDDED_IDENTITY: 2048, 200 | IGNORE_LIMITS: 1024, 201 | MAIN_FRAME_DEPRECATED: 256, 202 | NORMAL: 0, 203 | ONLY_FROM_CACHE: 8, 204 | PREFETCH: 512, 205 | RESTRICTED_PREFETCH: 32768, 206 | SHOULD_BYPASS_HSTS: 524288, 207 | SKIP_CACHE_VALIDATION: 4, 208 | SKIP_VARY_CHECK: 8192, 209 | SUPPORT_ASYNC_REVALIDATION: 16384, 210 | VALIDATE_CACHE: 1, 211 | }, 212 | loadState: { 213 | CONNECTING: 11, 214 | DOWNLOADING_PAC_FILE: 6, 215 | ESTABLISHING_PROXY_TUNNEL: 9, 216 | IDLE: 0, 217 | OBSOLETE_WAITING_FOR_APPCACHE: 5, 218 | READING_RESPONSE: 15, 219 | RESOLVING_HOST: 10, 220 | RESOLVING_HOST_IN_PAC_FILE: 8, 221 | RESOLVING_PROXY_FOR_URL: 7, 222 | SENDING_REQUEST: 13, 223 | SSL_HANDSHAKE: 12, 224 | WAITING_FOR_AVAILABLE_SOCKET: 2, 225 | WAITING_FOR_CACHE: 4, 226 | WAITING_FOR_DELEGATE: 3, 227 | WAITING_FOR_RESPONSE: 14, 228 | WAITING_FOR_STALLED_SOCKET_POOL: 1, 229 | }, 230 | logCaptureMode: 'Default', 231 | logEventPhase: { 232 | PHASE_BEGIN: 1, 233 | PHASE_END: 2, 234 | PHASE_NONE: 0, 235 | }, 236 | logEventTypes: { 237 | AUTH_BOUND_TO_CONTROLLER: 380, 238 | AUTH_CHANNEL_BINDINGS: 390, 239 | AUTH_CONTROLLER: 379, 240 | AUTH_GENERATE_TOKEN: 383, 241 | AUTH_HANDLER_CREATE_RESULT: 381, 242 | AUTH_HANDLER_INIT: 382, 243 | AUTH_HANDLE_CHALLENGE: 384, 244 | AUTH_LIBRARY_ACQUIRE_CREDS: 388, 245 | AUTH_LIBRARY_BIND_FAILED: 386, 246 | AUTH_LIBRARY_IMPORT_NAME: 387, 247 | AUTH_LIBRARY_INIT_SEC_CTX: 389, 248 | AUTH_LIBRARY_LOAD: 385, 249 | BAD_PROXY_LIST_REPORTED: 34, 250 | BIDIRECTIONAL_STREAM_ALIVE: 192, 251 | BIDIRECTIONAL_STREAM_BOUND_TO_QUIC_SESSION: 202, 252 | BIDIRECTIONAL_STREAM_BYTES_RECEIVED: 197, 253 | BIDIRECTIONAL_STREAM_BYTES_SENT: 196, 254 | BIDIRECTIONAL_STREAM_BYTES_SENT_COALESCED: 195, 255 | BIDIRECTIONAL_STREAM_FAILED: 201, 256 | BIDIRECTIONAL_STREAM_READY: 200, 257 | BIDIRECTIONAL_STREAM_READ_DATA: 193, 258 | BIDIRECTIONAL_STREAM_RECV_HEADERS: 198, 259 | BIDIRECTIONAL_STREAM_RECV_TRAILERS: 199, 260 | BIDIRECTIONAL_STREAM_SENDV_DATA: 194, 261 | BOUND_TO_QUIC_SESSION_POOL_JOB: 251, 262 | BROKERED_CREATE_SOCKET: 45, 263 | BROKERED_SOCKET_ALIVE: 44, 264 | CANCELLED: 0, 265 | CERTIFICATE_DATABASE_CLIENT_CERT_STORE_CHANGED: 401, 266 | CERTIFICATE_DATABASE_TRUST_STORE_CHANGED: 400, 267 | CERT_CT_COMPLIANCE_CHECKED: 85, 268 | CERT_VERIFIER_JOB: 414, 269 | CERT_VERIFIER_REQUEST: 413, 270 | CERT_VERIFIER_REQUEST_BOUND_TO_JOB: 415, 271 | CERT_VERIFIER_TASK: 416, 272 | CERT_VERIFIER_TASK_BOUND: 417, 273 | CERT_VERIFY_PROC: 419, 274 | CERT_VERIFY_PROC_ADDITIONAL_CERT: 423, 275 | CERT_VERIFY_PROC_CHROME_ROOT_STORE_VERSION: 422, 276 | CERT_VERIFY_PROC_CREATED: 418, 277 | CERT_VERIFY_PROC_INPUT_CERT: 421, 278 | CERT_VERIFY_PROC_PATH_BUILDER_DEBUG: 426, 279 | CERT_VERIFY_PROC_PATH_BUILD_ATTEMPT: 424, 280 | CERT_VERIFY_PROC_PATH_BUILT: 425, 281 | CERT_VERIFY_PROC_TARGET_CERT: 420, 282 | CHECK_CORS_PREFLIGHT_CACHE: 528, 283 | CHECK_CORS_PREFLIGHT_REQUIRED: 527, 284 | CLEAR_CACHED_CLIENT_CERT: 402, 285 | CLEAR_MATCHING_CACHED_CLIENT_CERT: 403, 286 | COMPUTED_PRIVACY_MODE: 535, 287 | CONNECT_JOB: 93, 288 | CONNECT_JOB_SET_SOCKET: 94, 289 | CONNECT_JOB_TIMED_OUT: 95, 290 | COOKIE_GET_BLOCKED_BY_NETWORK_DELEGATE: 494, 291 | COOKIE_INCLUSION_STATUS: 496, 292 | COOKIE_PERSISTENT_STORE_CLOSED: 493, 293 | COOKIE_PERSISTENT_STORE_KEY_LOAD_COMPLETED: 492, 294 | COOKIE_PERSISTENT_STORE_KEY_LOAD_STARTED: 491, 295 | COOKIE_PERSISTENT_STORE_LOAD: 490, 296 | COOKIE_PERSISTENT_STORE_ORIGIN_FILTERED: 489, 297 | COOKIE_SET_BLOCKED_BY_NETWORK_DELEGATE: 495, 298 | COOKIE_STORE_ALIVE: 482, 299 | COOKIE_STORE_COOKIE_ADDED: 483, 300 | COOKIE_STORE_COOKIE_DELETED: 484, 301 | COOKIE_STORE_COOKIE_PRESERVED_SKIPPED_SECURE: 487, 302 | COOKIE_STORE_COOKIE_REJECTED_HTTPONLY: 486, 303 | COOKIE_STORE_COOKIE_REJECTED_SECURE: 485, 304 | COOKIE_STORE_SESSION_PERSISTENCE: 488, 305 | CORS_PREFLIGHT_CACHED_RESULT: 532, 306 | CORS_PREFLIGHT_ERROR: 530, 307 | CORS_PREFLIGHT_RESULT: 529, 308 | CORS_PREFLIGHT_URL_REQUEST: 531, 309 | CORS_REQUEST: 526, 310 | CREATED_BY: 534, 311 | CT_LOG_ENTRY_AUDITED: 86, 312 | DELEGATE_INFO: 127, 313 | DISK_CACHE_ENTRY_IMPL: 147, 314 | DISK_CACHE_MEM_ENTRY_IMPL: 148, 315 | DNS_CONFIG_CHANGED: 395, 316 | DNS_TRANSACTION: 406, 317 | DNS_TRANSACTION_ATTEMPT: 408, 318 | DNS_TRANSACTION_HTTPS_ATTEMPT: 410, 319 | DNS_TRANSACTION_QUERY: 407, 320 | DNS_TRANSACTION_RESPONSE: 411, 321 | DNS_TRANSACTION_TCP_ATTEMPT: 409, 322 | DOH_URL_REQUEST: 412, 323 | ENTRY_CLOSE: 157, 324 | ENTRY_DOOM: 158, 325 | ENTRY_READ_DATA: 149, 326 | ENTRY_WRITE_DATA: 150, 327 | FAILED: 1, 328 | FIRST_PARTY_SETS_METADATA: 547, 329 | FTP_COMMAND_SENT: 427, 330 | FTP_CONTROL_CONNECTION: 428, 331 | FTP_CONTROL_RESPONSE: 430, 332 | FTP_DATA_CONNECTION: 429, 333 | HOST_CACHE_PERSISTENCE_START_TIMER: 476, 334 | HOST_CACHE_PREF_READ: 474, 335 | HOST_CACHE_PREF_WRITE: 475, 336 | HOST_RESOLVER_DNS_TASK: 18, 337 | HOST_RESOLVER_DNS_TASK_EXTRACTION_FAILURE: 19, 338 | HOST_RESOLVER_DNS_TASK_EXTRACTION_RESULTS: 20, 339 | HOST_RESOLVER_DNS_TASK_TIMEOUT: 21, 340 | HOST_RESOLVER_MANAGER_ATTEMPT_FINISHED: 13, 341 | HOST_RESOLVER_MANAGER_ATTEMPT_STARTED: 12, 342 | HOST_RESOLVER_MANAGER_CACHE_HIT: 5, 343 | HOST_RESOLVER_MANAGER_CONFIG_PRESET_MATCH: 7, 344 | HOST_RESOLVER_MANAGER_CREATE_JOB: 8, 345 | HOST_RESOLVER_MANAGER_HOSTS_HIT: 6, 346 | HOST_RESOLVER_MANAGER_IPV6_REACHABILITY_CHECK: 4, 347 | HOST_RESOLVER_MANAGER_JOB: 9, 348 | HOST_RESOLVER_MANAGER_JOB_ATTACH: 14, 349 | HOST_RESOLVER_MANAGER_JOB_EVICTED: 10, 350 | HOST_RESOLVER_MANAGER_JOB_REQUEST_ATTACH: 15, 351 | HOST_RESOLVER_MANAGER_JOB_REQUEST_DETACH: 16, 352 | HOST_RESOLVER_MANAGER_JOB_STARTED: 11, 353 | HOST_RESOLVER_MANAGER_REQUEST: 3, 354 | HOST_RESOLVER_SERVICE_ENDPOINTS_RESOLUTION_DELAY: 23, 355 | HOST_RESOLVER_SERVICE_ENDPOINTS_UPDATED: 22, 356 | HOST_RESOLVER_SYSTEM_TASK: 17, 357 | HTTP2_PROXY_CLIENT_SESSION: 242, 358 | HTTP2_SESSION: 203, 359 | HTTP2_SESSION_CLOSE: 226, 360 | HTTP2_SESSION_INITIALIZED: 204, 361 | HTTP2_SESSION_INITIAL_WINDOW_SIZE_OUT_OF_RANGE: 228, 362 | HTTP2_SESSION_PING: 215, 363 | HTTP2_SESSION_POOL_CREATED_NEW_SESSION: 233, 364 | HTTP2_SESSION_POOL_FOUND_EXISTING_SESSION: 231, 365 | HTTP2_SESSION_POOL_FOUND_EXISTING_SESSION_FROM_IP_POOL: 232, 366 | HTTP2_SESSION_POOL_IMPORTED_SESSION_FROM_SOCKET: 234, 367 | HTTP2_SESSION_POOL_REMOVE_SESSION: 235, 368 | HTTP2_SESSION_RECV_ACCEPT_CH: 209, 369 | HTTP2_SESSION_RECV_DATA: 223, 370 | HTTP2_SESSION_RECV_GOAWAY: 216, 371 | HTTP2_SESSION_RECV_HEADERS: 206, 372 | HTTP2_SESSION_RECV_INVALID_HEADER: 221, 373 | HTTP2_SESSION_RECV_RST_STREAM: 213, 374 | HTTP2_SESSION_RECV_SETTING: 211, 375 | HTTP2_SESSION_RECV_SETTINGS: 210, 376 | HTTP2_SESSION_RECV_SETTINGS_ACK: 212, 377 | HTTP2_SESSION_RECV_WINDOW_UPDATE: 217, 378 | HTTP2_SESSION_SEND_DATA: 222, 379 | HTTP2_SESSION_SEND_GREASED_FRAME: 230, 380 | HTTP2_SESSION_SEND_HEADERS: 205, 381 | HTTP2_SESSION_SEND_RST_STREAM: 214, 382 | HTTP2_SESSION_SEND_SETTINGS: 207, 383 | HTTP2_SESSION_SEND_SETTINGS_ACK: 208, 384 | HTTP2_SESSION_SEND_WINDOW_UPDATE: 218, 385 | HTTP2_SESSION_STALLED_MAX_STREAMS: 227, 386 | HTTP2_SESSION_STREAM_STALLED_BY_SESSION_SEND_WINDOW: 224, 387 | HTTP2_SESSION_STREAM_STALLED_BY_STREAM_SEND_WINDOW: 225, 388 | HTTP2_SESSION_UPDATE_RECV_WINDOW: 220, 389 | HTTP2_SESSION_UPDATE_SEND_WINDOW: 219, 390 | HTTP2_SESSION_UPDATE_STREAMS_SEND_WINDOW_SIZE: 229, 391 | HTTP2_STREAM: 236, 392 | HTTP2_STREAM_ERROR: 240, 393 | HTTP2_STREAM_FLOW_CONTROL_UNSTALLED: 237, 394 | HTTP2_STREAM_SEND_PRIORITY: 241, 395 | HTTP2_STREAM_UPDATE_RECV_WINDOW: 239, 396 | HTTP2_STREAM_UPDATE_SEND_WINDOW: 238, 397 | HTTP3_CANCEL_PUSH_RECEIVED: 503, 398 | HTTP3_DATA_FRAME_RECEIVED: 508, 399 | HTTP3_DATA_SENT: 517, 400 | HTTP3_GOAWAY_RECEIVED: 505, 401 | HTTP3_GOAWAY_SENT: 514, 402 | HTTP3_HEADERS_DECODED: 510, 403 | HTTP3_HEADERS_RECEIVED: 509, 404 | HTTP3_HEADERS_SENT: 518, 405 | HTTP3_LOCAL_CONTROL_STREAM_CREATED: 497, 406 | HTTP3_LOCAL_QPACK_DECODER_STREAM_CREATED: 499, 407 | HTTP3_LOCAL_QPACK_ENCODER_STREAM_CREATED: 498, 408 | HTTP3_MAX_PUSH_ID_RECEIVED: 506, 409 | HTTP3_MAX_PUSH_ID_SENT: 515, 410 | HTTP3_PEER_CONTROL_STREAM_CREATED: 500, 411 | HTTP3_PEER_QPACK_DECODER_STREAM_CREATED: 502, 412 | HTTP3_PEER_QPACK_ENCODER_STREAM_CREATED: 501, 413 | HTTP3_PRIORITY_UPDATE_RECEIVED: 507, 414 | HTTP3_PRIORITY_UPDATE_SENT: 516, 415 | HTTP3_PUSH_PROMISE_SENT: 519, 416 | HTTP3_SETTINGS_RECEIVED: 504, 417 | HTTP3_SETTINGS_RESUMED: 513, 418 | HTTP3_SETTINGS_SENT: 512, 419 | HTTP3_UNKNOWN_FRAME_RECEIVED: 511, 420 | HTTP_CACHE_ADD_TO_ENTRY: 138, 421 | HTTP_CACHE_CALLER_REQUEST_HEADERS: 144, 422 | HTTP_CACHE_CREATE_ENTRY: 137, 423 | HTTP_CACHE_DOOM_ENTRY: 139, 424 | HTTP_CACHE_GET_BACKEND: 134, 425 | HTTP_CACHE_OPEN_ENTRY: 136, 426 | HTTP_CACHE_OPEN_OR_CREATE_ENTRY: 135, 427 | HTTP_CACHE_READ_DATA: 142, 428 | HTTP_CACHE_READ_INFO: 140, 429 | HTTP_CACHE_RESTART_PARTIAL_REQUEST: 145, 430 | HTTP_CACHE_RE_SEND_PARTIAL_REQUEST: 146, 431 | HTTP_CACHE_WRITE_DATA: 143, 432 | HTTP_CACHE_WRITE_INFO: 141, 433 | HTTP_PROXY_CONNECT_JOB_CONNECT: 99, 434 | HTTP_SERVER_PROPERTIES_INITIALIZATION: 471, 435 | HTTP_SERVER_PROPERTIES_UPDATE_CACHE: 472, 436 | HTTP_SERVER_PROPERTIES_UPDATE_PREFS: 473, 437 | HTTP_STREAM_JOB: 160, 438 | HTTP_STREAM_JOB_BOUND_TO_REQUEST: 167, 439 | HTTP_STREAM_JOB_CONTROLLER: 172, 440 | HTTP_STREAM_JOB_CONTROLLER_ALT_SVC_FOUND: 175, 441 | HTTP_STREAM_JOB_CONTROLLER_BOUND: 173, 442 | HTTP_STREAM_JOB_CONTROLLER_PROXY_SERVER_RESOLVED: 174, 443 | HTTP_STREAM_JOB_DELAYED: 170, 444 | HTTP_STREAM_JOB_INIT_CONNECTION: 165, 445 | HTTP_STREAM_JOB_ORPHANED: 169, 446 | HTTP_STREAM_JOB_RESUMED: 171, 447 | HTTP_STREAM_JOB_RESUME_INIT_CONNECTION: 164, 448 | HTTP_STREAM_JOB_THROTTLED: 163, 449 | HTTP_STREAM_JOB_WAITING: 161, 450 | HTTP_STREAM_PARSER_READ_HEADERS: 374, 451 | HTTP_STREAM_REQUEST: 159, 452 | HTTP_STREAM_REQUEST_BOUND_TO_JOB: 166, 453 | HTTP_STREAM_REQUEST_BOUND_TO_QUIC_SESSION: 341, 454 | HTTP_STREAM_REQUEST_PROTO: 168, 455 | HTTP_STREAM_REQUEST_STARTED_JOB: 162, 456 | HTTP_TRANSACTION_DRAIN_BODY_FOR_AUTH_RESTART: 189, 457 | HTTP_TRANSACTION_HTTP2_SEND_REQUEST_HEADERS: 183, 458 | HTTP_TRANSACTION_QUIC_SEND_REQUEST_HEADERS: 184, 459 | HTTP_TRANSACTION_READ_BODY: 188, 460 | HTTP_TRANSACTION_READ_EARLY_HINTS_RESPONSE_HEADERS: 187, 461 | HTTP_TRANSACTION_READ_HEADERS: 185, 462 | HTTP_TRANSACTION_READ_RESPONSE_HEADERS: 186, 463 | HTTP_TRANSACTION_READ_TUNNEL_RESPONSE_HEADERS: 179, 464 | HTTP_TRANSACTION_RESTART_AFTER_ERROR: 190, 465 | HTTP_TRANSACTION_RESTART_MISDIRECTED_REQUEST: 191, 466 | HTTP_TRANSACTION_SEND_REQUEST: 180, 467 | HTTP_TRANSACTION_SEND_REQUEST_BODY: 182, 468 | HTTP_TRANSACTION_SEND_REQUEST_HEADERS: 181, 469 | HTTP_TRANSACTION_SEND_TUNNEL_HEADERS: 177, 470 | HTTP_TRANSACTION_TUNNEL_READ_HEADERS: 178, 471 | HTTP_TRANSACTION_TUNNEL_SEND_REQUEST: 176, 472 | IN_MEMORY_CACHE_BYTES_READ: 469, 473 | IN_MEMORY_CACHE_READ_REQUEST_HEADERS: 467, 474 | IN_MEMORY_CACHE_READ_RESPONSE_HEADERS: 468, 475 | NETWORK_CHANGED: 393, 476 | NETWORK_CONNECTIVITY_CHANGED: 392, 477 | NETWORK_DELEGATE_BEFORE_START_TRANSACTION: 119, 478 | NETWORK_DELEGATE_BEFORE_URL_REQUEST: 120, 479 | NETWORK_DELEGATE_HEADERS_RECEIVED: 121, 480 | NETWORK_IP_ADDRESSES_CHANGED: 391, 481 | NETWORK_MAC_OS_CONFIG_CHANGED: 394, 482 | NETWORK_QUALITY_CHANGED: 470, 483 | OBLIVIOUS_HTTP_REQUEST: 543, 484 | OBLIVIOUS_HTTP_REQUEST_DATA: 544, 485 | OBLIVIOUS_HTTP_RESPONSE_DATA: 545, 486 | OBLIVIOUS_HTTP_RESPONSE_HEADERS: 546, 487 | PAC_FILE_DECIDER: 24, 488 | PAC_FILE_DECIDER_FALLING_BACK_TO_NEXT_PAC_SOURCE: 28, 489 | PAC_FILE_DECIDER_FETCH_PAC_SCRIPT: 26, 490 | PAC_FILE_DECIDER_HAS_NO_FETCHER: 27, 491 | PAC_FILE_DECIDER_WAIT: 25, 492 | PAC_JAVASCRIPT_ALERT: 37, 493 | PAC_JAVASCRIPT_ERROR: 36, 494 | PRIVATE_NETWORK_ACCESS_CHECK: 533, 495 | PROXY_CONFIG_CHANGED: 33, 496 | PROXY_LIST_FALLBACK: 35, 497 | PROXY_RESOLUTION_SERVICE: 29, 498 | PROXY_RESOLUTION_SERVICE_DEPRIORITIZED_BAD_PROXIES: 32, 499 | PROXY_RESOLUTION_SERVICE_RESOLVED_PROXY_LIST: 31, 500 | PROXY_RESOLUTION_SERVICE_WAITING_FOR_INIT_PAC: 30, 501 | QUIC_ACCEPT_CH_FRAME_RECEIVED: 335, 502 | QUIC_CHROMIUM_CLIENT_STREAM_READ_EARLY_HINTS_RESPONSE_HEADERS: 343, 503 | QUIC_CHROMIUM_CLIENT_STREAM_READ_RESPONSE_HEADERS: 344, 504 | QUIC_CHROMIUM_CLIENT_STREAM_READ_RESPONSE_TRAILERS: 345, 505 | QUIC_CHROMIUM_CLIENT_STREAM_SEND_REQUEST_HEADERS: 342, 506 | QUIC_CONGESTION_CONTROL_CONFIGURED: 270, 507 | QUIC_CONNECTION_MIGRATION_FAILURE: 348, 508 | QUIC_CONNECTION_MIGRATION_FAILURE_AFTER_PROBING: 357, 509 | QUIC_CONNECTION_MIGRATION_FAILURE_WAITING_FOR_NETWORK: 359, 510 | QUIC_CONNECTION_MIGRATION_MODE: 346, 511 | QUIC_CONNECTION_MIGRATION_ON_MIGRATE_BACK: 356, 512 | QUIC_CONNECTION_MIGRATION_ON_NETWORK_CONNECTED: 350, 513 | QUIC_CONNECTION_MIGRATION_ON_NETWORK_DISCONNECTED: 352, 514 | QUIC_CONNECTION_MIGRATION_ON_NETWORK_MADE_DEFAULT: 351, 515 | QUIC_CONNECTION_MIGRATION_ON_PATH_DEGRADING: 355, 516 | QUIC_CONNECTION_MIGRATION_ON_WRITE_ERROR: 353, 517 | QUIC_CONNECTION_MIGRATION_SUCCESS: 349, 518 | QUIC_CONNECTION_MIGRATION_SUCCESS_AFTER_PROBING: 358, 519 | QUIC_CONNECTION_MIGRATION_SUCCESS_WAITING_FOR_NETWORK: 360, 520 | QUIC_CONNECTION_MIGRATION_TRIGGERED: 347, 521 | QUIC_CONNECTION_MIGRATION_WAITING_FOR_NEW_NETWORK: 354, 522 | QUIC_CONNECTIVITY_PROBING_MANAGER_CANCEL_PROBING: 362, 523 | QUIC_CONNECTIVITY_PROBING_MANAGER_PROBE_RECEIVED: 364, 524 | QUIC_CONNECTIVITY_PROBING_MANAGER_PROBE_SENT: 363, 525 | QUIC_CONNECTIVITY_PROBING_MANAGER_START_PROBING: 361, 526 | QUIC_CONNECTIVITY_PROBING_MANAGER_STATELESS_RESET_RECEIVED: 365, 527 | QUIC_FAILED_TO_VALIDATE_SERVER_PREFERRED_ADDRESS: 371, 528 | QUIC_ON_SERVER_PREFERRED_ADDRESS_AVAILABLE: 369, 529 | QUIC_PORT_MIGRATION_FAILURE: 367, 530 | QUIC_PORT_MIGRATION_SUCCESS: 368, 531 | QUIC_PORT_MIGRATION_TRIGGERED: 366, 532 | QUIC_READ_ERROR: 373, 533 | QUIC_SESSION: 261, 534 | QUIC_SESSION_ACK_FRAME_RECEIVED: 277, 535 | QUIC_SESSION_ACK_FRAME_SENT: 278, 536 | QUIC_SESSION_ATTEMPTING_TO_PROCESS_UNDECRYPTABLE_PACKET: 339, 537 | QUIC_SESSION_BLOCKED_FRAME_RECEIVED: 281, 538 | QUIC_SESSION_BLOCKED_FRAME_SENT: 282, 539 | QUIC_SESSION_BUFFERED_UNDECRYPTABLE_PACKET: 337, 540 | QUIC_SESSION_CERTIFICATE_VERIFIED: 265, 541 | QUIC_SESSION_CERTIFICATE_VERIFY_FAILED: 264, 542 | QUIC_SESSION_CLOSED: 310, 543 | QUIC_SESSION_CLOSE_ON_ERROR: 263, 544 | QUIC_SESSION_COALESCED_PACKET_SENT: 336, 545 | QUIC_SESSION_CONNECTION_CLOSE_FRAME_RECEIVED: 292, 546 | QUIC_SESSION_CONNECTION_CLOSE_FRAME_SENT: 293, 547 | QUIC_SESSION_CONNECTIVITY_PROBING_FINISHED: 311, 548 | QUIC_SESSION_CREATED: 262, 549 | QUIC_SESSION_CRYPTO_FRAME_RECEIVED: 317, 550 | QUIC_SESSION_CRYPTO_FRAME_SENT: 316, 551 | QUIC_SESSION_CRYPTO_HANDSHAKE_MESSAGE_RECEIVED: 298, 552 | QUIC_SESSION_CRYPTO_HANDSHAKE_MESSAGE_SENT: 299, 553 | QUIC_SESSION_DROPPED_UNDECRYPTABLE_PACKET: 338, 554 | QUIC_SESSION_DUPLICATE_PACKET_RECEIVED: 271, 555 | QUIC_SESSION_GOAWAY_FRAME_RECEIVED: 283, 556 | QUIC_SESSION_GOAWAY_FRAME_SENT: 284, 557 | QUIC_SESSION_HANDSHAKE_DONE_FRAME_RECEIVED: 334, 558 | QUIC_SESSION_KEY_UPDATE: 340, 559 | QUIC_SESSION_MAX_STREAMS_FRAME_RECEIVED: 323, 560 | QUIC_SESSION_MAX_STREAMS_FRAME_SENT: 322, 561 | QUIC_SESSION_MESSAGE_FRAME_RECEIVED: 333, 562 | QUIC_SESSION_MESSAGE_FRAME_SENT: 332, 563 | QUIC_SESSION_MTU_DISCOVERY_FRAME_SENT: 287, 564 | QUIC_SESSION_NETWORK_CONNECTED: 309, 565 | QUIC_SESSION_NETWORK_DISCONNECTED: 308, 566 | QUIC_SESSION_NETWORK_MADE_DEFAULT: 307, 567 | QUIC_SESSION_NEW_CONNECTION_ID_FRAME_RECEIVED: 327, 568 | QUIC_SESSION_NEW_CONNECTION_ID_FRAME_SENT: 326, 569 | QUIC_SESSION_NEW_TOKEN_FRAME_RECEIVED: 329, 570 | QUIC_SESSION_NEW_TOKEN_FRAME_SENT: 328, 571 | QUIC_SESSION_PACKET_AUTHENTICATED: 273, 572 | QUIC_SESSION_PACKET_HEADER_REVIVED: 297, 573 | QUIC_SESSION_PACKET_LOST: 269, 574 | QUIC_SESSION_PACKET_RECEIVED: 266, 575 | QUIC_SESSION_PACKET_RETRANSMITTED: 268, 576 | QUIC_SESSION_PACKET_SENT: 267, 577 | QUIC_SESSION_PADDING_FRAME_RECEIVED: 325, 578 | QUIC_SESSION_PADDING_FRAME_SENT: 324, 579 | QUIC_SESSION_PATH_CHALLENGE_FRAME_RECEIVED: 313, 580 | QUIC_SESSION_PATH_CHALLENGE_FRAME_SENT: 312, 581 | QUIC_SESSION_PATH_RESPONSE_FRAME_RECEIVED: 315, 582 | QUIC_SESSION_PATH_RESPONSE_FRAME_SENT: 314, 583 | QUIC_SESSION_PING_FRAME_RECEIVED: 285, 584 | QUIC_SESSION_PING_FRAME_SENT: 286, 585 | QUIC_SESSION_POOL_ATTACH_HTTP_STREAM_JOB_TO_EXISTING_SESSION: 244, 586 | QUIC_SESSION_POOL_CLOSE_ALL_SESSIONS: 247, 587 | QUIC_SESSION_POOL_JOB: 249, 588 | QUIC_SESSION_POOL_JOB_BOUND_TO: 250, 589 | QUIC_SESSION_POOL_JOB_CONNECT: 252, 590 | QUIC_SESSION_POOL_JOB_RESULT: 260, 591 | QUIC_SESSION_POOL_JOB_RETRY_ON_ALTERNATE_NETWORK: 255, 592 | QUIC_SESSION_POOL_JOB_STALE_HOST_NOT_USED_ON_CONNECTION: 257, 593 | QUIC_SESSION_POOL_JOB_STALE_HOST_RESOLUTION_MATCHED: 259, 594 | QUIC_SESSION_POOL_JOB_STALE_HOST_RESOLUTION_NO_MATCH: 258, 595 | QUIC_SESSION_POOL_JOB_STALE_HOST_TRIED_ON_CONNECTION: 256, 596 | QUIC_SESSION_POOL_MARK_ALL_ACTIVE_SESSIONS_GOING_AWAY: 248, 597 | QUIC_SESSION_POOL_ON_IP_ADDRESS_CHANGED: 246, 598 | QUIC_SESSION_POOL_PLATFORM_NOTIFICATION: 245, 599 | QUIC_SESSION_POOL_PROXY_JOB_CONNECT: 253, 600 | QUIC_SESSION_POOL_PROXY_JOB_CREATE_PROXY_SESSION: 254, 601 | QUIC_SESSION_POOL_USE_EXISTING_SESSION: 243, 602 | QUIC_SESSION_PUBLIC_RESET_PACKET_RECEIVED: 294, 603 | QUIC_SESSION_RETIRE_CONNECTION_ID_FRAME_RECEIVED: 331, 604 | QUIC_SESSION_RETIRE_CONNECTION_ID_FRAME_SENT: 330, 605 | QUIC_SESSION_RST_STREAM_FRAME_RECEIVED: 290, 606 | QUIC_SESSION_RST_STREAM_FRAME_SENT: 291, 607 | QUIC_SESSION_STOP_SENDING_FRAME_RECEIVED: 319, 608 | QUIC_SESSION_STOP_SENDING_FRAME_SENT: 318, 609 | QUIC_SESSION_STOP_WAITING_FRAME_RECEIVED: 288, 610 | QUIC_SESSION_STOP_WAITING_FRAME_SENT: 289, 611 | QUIC_SESSION_STREAMS_BLOCKED_FRAME_RECEIVED: 321, 612 | QUIC_SESSION_STREAMS_BLOCKED_FRAME_SENT: 320, 613 | QUIC_SESSION_STREAM_FRAME_COALESCED: 276, 614 | QUIC_SESSION_STREAM_FRAME_RECEIVED: 274, 615 | QUIC_SESSION_STREAM_FRAME_SENT: 275, 616 | QUIC_SESSION_TRANSPORT_PARAMETERS_RECEIVED: 300, 617 | QUIC_SESSION_TRANSPORT_PARAMETERS_RESUMED: 302, 618 | QUIC_SESSION_TRANSPORT_PARAMETERS_SENT: 301, 619 | QUIC_SESSION_UNAUTHENTICATED_PACKET_HEADER_RECEIVED: 272, 620 | QUIC_SESSION_VERSION_NEGOTIATED: 296, 621 | QUIC_SESSION_VERSION_NEGOTIATION_PACKET_RECEIVED: 295, 622 | QUIC_SESSION_WEBTRANSPORT_CLIENT_ALIVE: 303, 623 | QUIC_SESSION_WEBTRANSPORT_CLIENT_STATE_CHANGED: 304, 624 | QUIC_SESSION_WEBTRANSPORT_SESSION_READY: 305, 625 | QUIC_SESSION_WINDOW_UPDATE_FRAME_RECEIVED: 279, 626 | QUIC_SESSION_WINDOW_UPDATE_FRAME_SENT: 280, 627 | QUIC_SESSION_ZERO_RTT_REJECTED: 306, 628 | QUIC_START_VALIDATING_SERVER_PREFERRED_ADDRESS: 370, 629 | QUIC_SUCCESSFULLY_MIGRATED_TO_SERVER_PREFERRED_ADDRESS: 372, 630 | REQUEST_ALIVE: 2, 631 | RESOURCE_SCHEDULER_REQUEST_STARTED: 466, 632 | SIGNED_CERTIFICATE_TIMESTAMPS_CHECKED: 84, 633 | SIGNED_CERTIFICATE_TIMESTAMPS_RECEIVED: 83, 634 | SIMPLE_CACHE_ENTRY: 431, 635 | SIMPLE_CACHE_ENTRY_CHECKSUM_BEGIN: 446, 636 | SIMPLE_CACHE_ENTRY_CHECKSUM_END: 447, 637 | SIMPLE_CACHE_ENTRY_CLOSE_BEGIN: 462, 638 | SIMPLE_CACHE_ENTRY_CLOSE_CALL: 461, 639 | SIMPLE_CACHE_ENTRY_CLOSE_END: 463, 640 | SIMPLE_CACHE_ENTRY_CREATE_BEGIN: 438, 641 | SIMPLE_CACHE_ENTRY_CREATE_CALL: 436, 642 | SIMPLE_CACHE_ENTRY_CREATE_END: 439, 643 | SIMPLE_CACHE_ENTRY_CREATE_OPTIMISTIC: 437, 644 | SIMPLE_CACHE_ENTRY_DOOM_BEGIN: 459, 645 | SIMPLE_CACHE_ENTRY_DOOM_CALL: 458, 646 | SIMPLE_CACHE_ENTRY_DOOM_END: 460, 647 | SIMPLE_CACHE_ENTRY_OPEN_BEGIN: 434, 648 | SIMPLE_CACHE_ENTRY_OPEN_CALL: 433, 649 | SIMPLE_CACHE_ENTRY_OPEN_END: 435, 650 | SIMPLE_CACHE_ENTRY_OPEN_OR_CREATE_BEGIN: 441, 651 | SIMPLE_CACHE_ENTRY_OPEN_OR_CREATE_CALL: 440, 652 | SIMPLE_CACHE_ENTRY_OPEN_OR_CREATE_END: 442, 653 | SIMPLE_CACHE_ENTRY_READ_BEGIN: 444, 654 | SIMPLE_CACHE_ENTRY_READ_CALL: 443, 655 | SIMPLE_CACHE_ENTRY_READ_END: 445, 656 | SIMPLE_CACHE_ENTRY_READ_SPARSE_BEGIN: 453, 657 | SIMPLE_CACHE_ENTRY_READ_SPARSE_CALL: 452, 658 | SIMPLE_CACHE_ENTRY_READ_SPARSE_END: 454, 659 | SIMPLE_CACHE_ENTRY_SET_KEY: 432, 660 | SIMPLE_CACHE_ENTRY_WRITE_BEGIN: 450, 661 | SIMPLE_CACHE_ENTRY_WRITE_CALL: 448, 662 | SIMPLE_CACHE_ENTRY_WRITE_END: 451, 663 | SIMPLE_CACHE_ENTRY_WRITE_OPTIMISTIC: 449, 664 | SIMPLE_CACHE_ENTRY_WRITE_SPARSE_BEGIN: 456, 665 | SIMPLE_CACHE_ENTRY_WRITE_SPARSE_CALL: 455, 666 | SIMPLE_CACHE_ENTRY_WRITE_SPARSE_END: 457, 667 | SOCKET_ALIVE: 40, 668 | SOCKET_BIND_TO_NETWORK: 43, 669 | SOCKET_BYTES_RECEIVED: 77, 670 | SOCKET_BYTES_SENT: 75, 671 | SOCKET_CLOSED: 81, 672 | SOCKET_CONNECT: 42, 673 | SOCKET_IN_USE: 49, 674 | SOCKET_OPEN: 41, 675 | SOCKET_POOL: 103, 676 | SOCKET_POOL_BOUND_TO_CONNECT_JOB: 110, 677 | SOCKET_POOL_BOUND_TO_SOCKET: 111, 678 | SOCKET_POOL_CLOSING_SOCKET: 113, 679 | SOCKET_POOL_CONNECTING_N_SOCKETS: 112, 680 | SOCKET_POOL_CONNECT_JOB_CREATED: 109, 681 | SOCKET_POOL_REUSED_AN_EXISTING_SOCKET: 106, 682 | SOCKET_POOL_STALLED_MAX_SOCKETS: 104, 683 | SOCKET_POOL_STALLED_MAX_SOCKETS_PER_GROUP: 105, 684 | SOCKET_READ_ERROR: 79, 685 | SOCKET_WRITE_ERROR: 80, 686 | SOCKS5_CONNECT: 51, 687 | SOCKS5_GREET_READ: 376, 688 | SOCKS5_GREET_WRITE: 375, 689 | SOCKS5_HANDSHAKE_READ: 378, 690 | SOCKS5_HANDSHAKE_WRITE: 377, 691 | SOCKS_CONNECT: 50, 692 | SOCKS_CONNECT_JOB_CONNECT: 98, 693 | SOCKS_HOSTNAME_TOO_BIG: 52, 694 | SOCKS_SERVER_ERROR: 56, 695 | SOCKS_UNEXPECTEDLY_CLOSED_DURING_GREETING: 53, 696 | SOCKS_UNEXPECTEDLY_CLOSED_DURING_HANDSHAKE: 54, 697 | SOCKS_UNEXPECTED_AUTH: 57, 698 | SOCKS_UNEXPECTED_VERSION: 55, 699 | SOCKS_UNKNOWN_ADDRESS_TYPE: 58, 700 | SPARSE_DELETE_CHILDREN: 156, 701 | SPARSE_GET_RANGE: 155, 702 | SPARSE_READ: 151, 703 | SPARSE_READ_CHILD_DATA: 153, 704 | SPARSE_WRITE: 152, 705 | SPARSE_WRITE_CHILD_DATA: 154, 706 | SPECIFIC_NETWORK_CONNECTED: 396, 707 | SPECIFIC_NETWORK_DISCONNECTED: 397, 708 | SPECIFIC_NETWORK_MADE_DEFAULT: 399, 709 | SPECIFIC_NETWORK_SOON_TO_DISCONNECT: 398, 710 | SSL_ALERT_RECEIVED: 69, 711 | SSL_ALERT_SENT: 70, 712 | SSL_CERTIFICATES_RECEIVED: 82, 713 | SSL_CLIENT_CERT_PROVIDED: 64, 714 | SSL_CLIENT_CERT_REQUESTED: 62, 715 | SSL_CONFIRM_HANDSHAKE: 71, 716 | SSL_CONNECT: 59, 717 | SSL_CONNECT_JOB_CONNECT: 97, 718 | SSL_CONNECT_JOB_RESTART_WITH_ECH_CONFIG_LIST: 100, 719 | SSL_ECH_CONFIG_LIST: 60, 720 | SSL_ENCRYPTED_CLIENT_HELLO: 74, 721 | SSL_HANDSHAKE_ERROR: 65, 722 | SSL_HANDSHAKE_MESSAGE_RECEIVED: 72, 723 | SSL_HANDSHAKE_MESSAGE_SENT: 73, 724 | SSL_PRIVATE_KEY_OP: 63, 725 | SSL_READ_ERROR: 66, 726 | SSL_SERVER_HANDSHAKE: 61, 727 | SSL_SOCKET_BYTES_RECEIVED: 78, 728 | SSL_SOCKET_BYTES_SENT: 76, 729 | SSL_VERIFICATION_MERGED: 68, 730 | SSL_WRITE_ERROR: 67, 731 | SUBMITTED_TO_RESOLVER_THREAD: 39, 732 | TCP_ACCEPT: 48, 733 | TCP_CLIENT_SOCKET_POOL_REQUESTED_SOCKET: 107, 734 | TCP_CLIENT_SOCKET_POOL_REQUESTED_SOCKETS: 108, 735 | TCP_CONNECT: 46, 736 | TCP_CONNECT_ATTEMPT: 47, 737 | TCP_STREAM_ATTEMPT_ALIVE: 114, 738 | THROTTLING_DISABLED_FOR_HOST: 404, 739 | THROTTLING_REJECTED_REQUEST: 405, 740 | TLS_STREAM_ATTEMPT_ALIVE: 115, 741 | TRANSPORT_CONNECT_JOB_CONNECT: 96, 742 | TRANSPORT_CONNECT_JOB_CONNECT_ATTEMPT: 102, 743 | TRANSPORT_CONNECT_JOB_IPV6_FALLBACK: 101, 744 | TRANSPORT_SECURITY_STATE_SHOULD_UPGRADE_TO_SSL: 542, 745 | TRUST_TOKEN_OPERATION_BEGIN_ISSUANCE: 521, 746 | TRUST_TOKEN_OPERATION_BEGIN_REDEMPTION: 523, 747 | TRUST_TOKEN_OPERATION_BEGIN_SIGNING: 525, 748 | TRUST_TOKEN_OPERATION_FINALIZE_ISSUANCE: 522, 749 | TRUST_TOKEN_OPERATION_FINALIZE_REDEMPTION: 524, 750 | TRUST_TOKEN_OPERATION_REQUESTED: 520, 751 | UDP_BYTES_RECEIVED: 89, 752 | UDP_BYTES_SENT: 90, 753 | UDP_CONNECT: 87, 754 | UDP_LOCAL_ADDRESS: 88, 755 | UDP_RECEIVE_ERROR: 91, 756 | UDP_SEND_ERROR: 92, 757 | UPLOAD_DATA_STREAM_INIT: 464, 758 | UPLOAD_DATA_STREAM_READ: 465, 759 | URL_REQUEST_DELEGATE_CERTIFICATE_REQUESTED: 122, 760 | URL_REQUEST_DELEGATE_CONNECTED: 126, 761 | URL_REQUEST_DELEGATE_RECEIVED_REDIRECT: 123, 762 | URL_REQUEST_DELEGATE_RESPONSE_STARTED: 124, 763 | URL_REQUEST_DELEGATE_SSL_CERTIFICATE_ERROR: 125, 764 | URL_REQUEST_FAKE_RESPONSE_HEADERS_CREATED: 132, 765 | URL_REQUEST_FILTERS_SET: 133, 766 | URL_REQUEST_JOB_BYTES_READ: 128, 767 | URL_REQUEST_JOB_FILTERED_BYTES_READ: 129, 768 | URL_REQUEST_REDIRECTED: 117, 769 | URL_REQUEST_REDIRECT_JOB: 131, 770 | URL_REQUEST_RETRY_WITH_STORAGE_ACCESS: 118, 771 | URL_REQUEST_SET_PRIORITY: 130, 772 | URL_REQUEST_START_JOB: 116, 773 | WAITING_FOR_PROXY_RESOLVER_THREAD: 38, 774 | WEBSOCKET_CLOSE_TIMEOUT: 540, 775 | WEBSOCKET_INVALID_FRAME: 541, 776 | WEBSOCKET_READ_BUFFER_SIZE_CHANGED: 537, 777 | WEBSOCKET_RECV_FRAME_HEADER: 538, 778 | WEBSOCKET_SENT_FRAME_HEADER: 539, 779 | WEBSOCKET_UPGRADE_FAILURE: 536, 780 | WPAD_DHCP_WIN_FETCH: 477, 781 | WPAD_DHCP_WIN_GET_ADAPTERS: 478, 782 | WPAD_DHCP_WIN_ON_FETCHER_DONE: 479, 783 | WPAD_DHCP_WIN_ON_WAIT_TIMER: 481, 784 | WPAD_DHCP_WIN_START_WAIT_TIMER: 480, 785 | }, 786 | logFormatVersion: 1, 787 | logSourceType: { 788 | BIDIRECTIONAL_STREAM: 24, 789 | CERT_VERIFIER_JOB: 21, 790 | CERT_VERIFIER_TASK: 22, 791 | CERT_VERIFY_PROC_CREATED: 20, 792 | COOKIE_STORE: 32, 793 | CT_TREE_STATE_TRACKER: 27, 794 | DISK_CACHE_ENTRY: 14, 795 | DNS_OVER_HTTPS: 37, 796 | DNS_TRANSACTION: 38, 797 | EXPONENTIAL_BACKOFF_THROTTLING: 18, 798 | HOST_CACHE_PERSISTENCE_MANAGER: 31, 799 | HOST_RESOLVER_IMPL_JOB: 13, 800 | HTTP2_SESSION: 10, 801 | HTTP3_SESSION: 34, 802 | HTTP_AUTH_CONTROLLER: 33, 803 | HTTP_PROXY_CONNECT_JOB: 3, 804 | HTTP_SERVER_PROPERTIES: 30, 805 | HTTP_STREAM_JOB: 16, 806 | HTTP_STREAM_JOB_CONTROLLER: 26, 807 | HTTP_STREAM_POOL_JOB: 17, 808 | MEMORY_CACHE_ENTRY: 15, 809 | NETWORK_CHANGE_NOTIFIER: 39, 810 | NETWORK_QUALITY_ESTIMATOR: 25, 811 | NETWORK_SERVICE_HOST_RESOLVER: 36, 812 | NONE: 0, 813 | PAC_FILE_DECIDER: 2, 814 | PROXY_CLIENT_SOCKET: 23, 815 | QUIC_CONNECTION_MIGRATION: 11, 816 | QUIC_PROXY_DATAGRAM_CLIENT_SOCKET: 42, 817 | QUIC_SESSION: 12, 818 | QUIC_SESSION_POOL: 40, 819 | QUIC_SESSION_POOL_DIRECT_JOB: 28, 820 | QUIC_SESSION_POOL_PROXY_JOB: 29, 821 | SOCKET: 9, 822 | SOCKS_CONNECT_JOB: 4, 823 | SSL_CONNECT_JOB: 5, 824 | TCP_STREAM_ATTEMPT: 7, 825 | TLS_STREAM_ATTEMPT: 8, 826 | TRANSPORT_CONNECT_JOB: 6, 827 | UDP_CLIENT_SOCKET: 41, 828 | UDP_SOCKET: 19, 829 | URL_REQUEST: 1, 830 | WEB_TRANSPORT_CLIENT: 35, 831 | }, 832 | netError: { 833 | ERR_ABORTED: -3, 834 | ERR_ACCESS_DENIED: -10, 835 | ERR_ADDRESS_INVALID: -108, 836 | ERR_ADDRESS_IN_USE: -147, 837 | ERR_ADDRESS_UNREACHABLE: -109, 838 | ERR_ADD_USER_CERT_FAILED: -503, 839 | ERR_ALPN_NEGOTIATION_FAILED: -122, 840 | ERR_BAD_SSL_CLIENT_AUTH_CERT: -117, 841 | ERR_BLOCKED_BY_ADMINISTRATOR: -22, 842 | ERR_BLOCKED_BY_CLIENT: -20, 843 | ERR_BLOCKED_BY_CSP: -30, 844 | ERR_BLOCKED_BY_ORB: -32, 845 | ERR_BLOCKED_BY_PRIVATE_NETWORK_ACCESS_CHECKS: -385, 846 | ERR_BLOCKED_BY_RESPONSE: -27, 847 | ERR_CACHED_IP_ADDRESS_SPACE_BLOCKED_BY_PRIVATE_NETWORK_ACCESS_POLICY: -384, 848 | ERR_CACHE_AUTH_FAILURE_AFTER_READ: -410, 849 | ERR_CACHE_CHECKSUM_MISMATCH: -408, 850 | ERR_CACHE_CHECKSUM_READ_FAILURE: -407, 851 | ERR_CACHE_CREATE_FAILURE: -405, 852 | ERR_CACHE_DOOM_FAILURE: -412, 853 | ERR_CACHE_ENTRY_NOT_SUITABLE: -411, 854 | ERR_CACHE_LOCK_TIMEOUT: -409, 855 | ERR_CACHE_MISS: -400, 856 | ERR_CACHE_OPEN_FAILURE: -404, 857 | ERR_CACHE_OPEN_OR_CREATE_FAILURE: -413, 858 | ERR_CACHE_OPERATION_NOT_SUPPORTED: -403, 859 | ERR_CACHE_RACE: -406, 860 | ERR_CACHE_READ_FAILURE: -401, 861 | ERR_CACHE_WRITE_FAILURE: -402, 862 | ERR_CERTIFICATE_TRANSPARENCY_REQUIRED: -214, 863 | ERR_CERT_AUTHORITY_INVALID: -202, 864 | ERR_CERT_COMMON_NAME_INVALID: -200, 865 | ERR_CERT_CONTAINS_ERRORS: -203, 866 | ERR_CERT_DATABASE_CHANGED: -714, 867 | ERR_CERT_DATE_INVALID: -201, 868 | ERR_CERT_END: -219, 869 | ERR_CERT_INVALID: -207, 870 | ERR_CERT_KNOWN_INTERCEPTION_BLOCKED: -217, 871 | ERR_CERT_NAME_CONSTRAINT_VIOLATION: -212, 872 | ERR_CERT_NON_UNIQUE_NAME: -210, 873 | ERR_CERT_NO_REVOCATION_MECHANISM: -204, 874 | ERR_CERT_REVOKED: -206, 875 | ERR_CERT_SYMANTEC_LEGACY: -215, 876 | ERR_CERT_UNABLE_TO_CHECK_REVOCATION: -205, 877 | ERR_CERT_VALIDITY_TOO_LONG: -213, 878 | ERR_CERT_VERIFIER_CHANGED: -716, 879 | ERR_CERT_WEAK_KEY: -211, 880 | ERR_CERT_WEAK_SIGNATURE_ALGORITHM: -208, 881 | ERR_CLEARTEXT_NOT_PERMITTED: -29, 882 | ERR_CLIENT_AUTH_CERT_TYPE_UNSUPPORTED: -151, 883 | ERR_CONNECTION_ABORTED: -103, 884 | ERR_CONNECTION_CLOSED: -100, 885 | ERR_CONNECTION_FAILED: -104, 886 | ERR_CONNECTION_REFUSED: -102, 887 | ERR_CONNECTION_RESET: -101, 888 | ERR_CONNECTION_TIMED_OUT: -118, 889 | ERR_CONTENT_DECODING_FAILED: -330, 890 | ERR_CONTENT_DECODING_INIT_FAILED: -371, 891 | ERR_CONTENT_LENGTH_MISMATCH: -354, 892 | ERR_CONTEXT_SHUT_DOWN: -26, 893 | ERR_CT_CONSISTENCY_PROOF_PARSING_FAILED: -171, 894 | ERR_CT_STH_INCOMPLETE: -169, 895 | ERR_CT_STH_PARSING_FAILED: -168, 896 | ERR_DICTIONARY_LOAD_FAILED: -387, 897 | ERR_DISALLOWED_URL_SCHEME: -301, 898 | ERR_DNS_CACHE_MISS: -804, 899 | ERR_DNS_MALFORMED_RESPONSE: -800, 900 | ERR_DNS_NAME_HTTPS_ONLY: -809, 901 | ERR_DNS_NO_MATCHING_SUPPORTED_ALPN: -811, 902 | ERR_DNS_REQUEST_CANCELLED: -810, 903 | ERR_DNS_SEARCH_EMPTY: -805, 904 | ERR_DNS_SECURE_PROBE_RECORD_INVALID: -814, 905 | ERR_DNS_SECURE_RESOLVER_HOSTNAME_RESOLUTION_FAILED: -808, 906 | ERR_DNS_SERVER_FAILED: -802, 907 | ERR_DNS_SERVER_REQUIRES_TCP: -801, 908 | ERR_DNS_SORT_ERROR: -806, 909 | ERR_DNS_TIMED_OUT: -803, 910 | ERR_EARLY_DATA_REJECTED: -178, 911 | ERR_ECH_FALLBACK_CERTIFICATE_INVALID: -184, 912 | ERR_ECH_NOT_NEGOTIATED: -183, 913 | ERR_EMPTY_RESPONSE: -324, 914 | ERR_ENCODING_CONVERSION_FAILED: -333, 915 | ERR_ENCODING_DETECTION_FAILED: -340, 916 | ERR_FAILED: -2, 917 | ERR_FILE_EXISTS: -16, 918 | ERR_FILE_NOT_FOUND: -6, 919 | ERR_FILE_NO_SPACE: -18, 920 | ERR_FILE_PATH_TOO_LONG: -17, 921 | ERR_FILE_TOO_BIG: -8, 922 | ERR_FILE_VIRUS_INFECTED: -19, 923 | ERR_H2_OR_QUIC_REQUIRED: -31, 924 | ERR_HOST_RESOLVER_QUEUE_TOO_LARGE: -119, 925 | ERR_HTTP2_COMPRESSION_ERROR: -363, 926 | ERR_HTTP2_FLOW_CONTROL_ERROR: -361, 927 | ERR_HTTP2_FRAME_SIZE_ERROR: -362, 928 | ERR_HTTP2_INADEQUATE_TRANSPORT_SECURITY: -360, 929 | ERR_HTTP2_PING_FAILED: -352, 930 | ERR_HTTP2_PROTOCOL_ERROR: -337, 931 | ERR_HTTP2_RST_STREAM_NO_ERROR_RECEIVED: -372, 932 | ERR_HTTP2_SERVER_REFUSED_STREAM: -351, 933 | ERR_HTTP2_STREAM_CLOSED: -376, 934 | ERR_HTTPS_PROXY_TUNNEL_RESPONSE_REDIRECT: -140, 935 | ERR_HTTP_1_1_REQUIRED: -365, 936 | ERR_HTTP_RESPONSE_CODE_FAILURE: -379, 937 | ERR_ICANN_NAME_COLLISION: -166, 938 | ERR_IMPORT_CA_CERT_FAILED: -705, 939 | ERR_IMPORT_CA_CERT_NOT_CA: -703, 940 | ERR_IMPORT_CERT_ALREADY_EXISTS: -704, 941 | ERR_IMPORT_SERVER_CERT_FAILED: -706, 942 | ERR_INCOMPLETE_CHUNKED_ENCODING: -355, 943 | ERR_INCOMPLETE_HTTP2_HEADERS: -347, 944 | ERR_INCONSISTENT_IP_ADDRESS_SPACE: -383, 945 | ERR_INSECURE_RESPONSE: -501, 946 | ERR_INSUFFICIENT_RESOURCES: -12, 947 | ERR_INTERNET_DISCONNECTED: -106, 948 | ERR_INVALID_ARGUMENT: -4, 949 | ERR_INVALID_AUTH_CREDENTIALS: -338, 950 | ERR_INVALID_CHUNKED_ENCODING: -321, 951 | ERR_INVALID_ECH_CONFIG_LIST: -182, 952 | ERR_INVALID_HANDLE: -5, 953 | ERR_INVALID_HTTP_RESPONSE: -370, 954 | ERR_INVALID_REDIRECT: -303, 955 | ERR_INVALID_RESPONSE: -320, 956 | ERR_INVALID_SIGNED_EXCHANGE: -504, 957 | ERR_INVALID_URL: -300, 958 | ERR_INVALID_WEB_BUNDLE: -505, 959 | ERR_IO_PENDING: -1, 960 | ERR_KEY_GENERATION_FAILED: -710, 961 | ERR_MALFORMED_IDENTITY: -329, 962 | ERR_MANDATORY_PROXY_CONFIGURATION_FAILED: -131, 963 | ERR_METHOD_NOT_SUPPORTED: -322, 964 | ERR_MISCONFIGURED_AUTH_ENVIRONMENT: -343, 965 | ERR_MISSING_AUTH_CREDENTIALS: -341, 966 | ERR_MSG_TOO_BIG: -142, 967 | ERR_NAME_NOT_RESOLVED: -105, 968 | ERR_NAME_RESOLUTION_FAILED: -137, 969 | ERR_NETWORK_ACCESS_DENIED: -138, 970 | ERR_NETWORK_ACCESS_REVOKED: -33, 971 | ERR_NETWORK_CHANGED: -21, 972 | ERR_NETWORK_IO_SUSPENDED: -331, 973 | ERR_NOT_IMPLEMENTED: -11, 974 | ERR_NO_BUFFER_SPACE: -176, 975 | ERR_NO_PRIVATE_KEY_FOR_CERT: -502, 976 | ERR_NO_SSL_VERSIONS_ENABLED: -112, 977 | ERR_NO_SUPPORTED_PROXIES: -336, 978 | ERR_OUT_OF_MEMORY: -13, 979 | ERR_PAC_NOT_IN_DHCP: -348, 980 | ERR_PAC_SCRIPT_FAILED: -327, 981 | ERR_PAC_SCRIPT_TERMINATED: -367, 982 | ERR_PKCS12_IMPORT_BAD_PASSWORD: -701, 983 | ERR_PKCS12_IMPORT_FAILED: -702, 984 | ERR_PKCS12_IMPORT_INVALID_FILE: -708, 985 | ERR_PKCS12_IMPORT_INVALID_MAC: -707, 986 | ERR_PKCS12_IMPORT_UNSUPPORTED: -709, 987 | ERR_PRECONNECT_MAX_SOCKET_LIMIT: -133, 988 | ERR_PRIVATE_KEY_EXPORT_FAILED: -712, 989 | ERR_PROXY_AUTH_REQUESTED: -127, 990 | ERR_PROXY_AUTH_REQUESTED_WITH_NO_CONNECTION: -364, 991 | ERR_PROXY_AUTH_UNSUPPORTED: -115, 992 | ERR_PROXY_CERTIFICATE_INVALID: -136, 993 | ERR_PROXY_CONNECTION_FAILED: -130, 994 | ERR_PROXY_HTTP_1_1_REQUIRED: -366, 995 | ERR_QUIC_CERT_ROOT_NOT_KNOWN: -380, 996 | ERR_QUIC_GOAWAY_REQUEST_CAN_BE_RETRIED: -381, 997 | ERR_QUIC_HANDSHAKE_FAILED: -358, 998 | ERR_QUIC_PROTOCOL_ERROR: -356, 999 | ERR_READ_IF_READY_NOT_IMPLEMENTED: -174, 1000 | ERR_REQUEST_RANGE_NOT_SATISFIABLE: -328, 1001 | ERR_RESPONSE_BODY_TOO_BIG_TO_DRAIN: -345, 1002 | ERR_RESPONSE_HEADERS_MULTIPLE_CONTENT_DISPOSITION: -349, 1003 | ERR_RESPONSE_HEADERS_MULTIPLE_CONTENT_LENGTH: -346, 1004 | ERR_RESPONSE_HEADERS_MULTIPLE_LOCATION: -350, 1005 | ERR_RESPONSE_HEADERS_TOO_BIG: -325, 1006 | ERR_RESPONSE_HEADERS_TRUNCATED: -357, 1007 | ERR_SELF_SIGNED_CERT_GENERATION_FAILED: -713, 1008 | ERR_SOCKET_IS_CONNECTED: -23, 1009 | ERR_SOCKET_NOT_CONNECTED: -15, 1010 | ERR_SOCKET_RECEIVE_BUFFER_SIZE_UNCHANGEABLE: -162, 1011 | ERR_SOCKET_SEND_BUFFER_SIZE_UNCHANGEABLE: -163, 1012 | ERR_SOCKET_SET_RECEIVE_BUFFER_SIZE_ERROR: -160, 1013 | ERR_SOCKET_SET_SEND_BUFFER_SIZE_ERROR: -161, 1014 | ERR_SOCKS_CONNECTION_FAILED: -120, 1015 | ERR_SOCKS_CONNECTION_HOST_UNREACHABLE: -121, 1016 | ERR_SSL_BAD_PEER_PUBLIC_KEY: -149, 1017 | ERR_SSL_BAD_RECORD_MAC_ALERT: -126, 1018 | ERR_SSL_CLIENT_AUTH_CERT_BAD_FORMAT: -164, 1019 | ERR_SSL_CLIENT_AUTH_CERT_NEEDED: -110, 1020 | ERR_SSL_CLIENT_AUTH_CERT_NO_PRIVATE_KEY: -135, 1021 | ERR_SSL_CLIENT_AUTH_NO_COMMON_ALGORITHMS: -177, 1022 | ERR_SSL_CLIENT_AUTH_PRIVATE_KEY_ACCESS_DENIED: -134, 1023 | ERR_SSL_CLIENT_AUTH_SIGNATURE_FAILED: -141, 1024 | ERR_SSL_DECOMPRESSION_FAILURE_ALERT: -125, 1025 | ERR_SSL_DECRYPT_ERROR_ALERT: -153, 1026 | ERR_SSL_HANDSHAKE_NOT_COMPLETED: -148, 1027 | ERR_SSL_KEY_USAGE_INCOMPATIBLE: -181, 1028 | ERR_SSL_NO_RENEGOTIATION: -123, 1029 | ERR_SSL_OBSOLETE_CIPHER: -172, 1030 | ERR_SSL_PINNED_KEY_NOT_IN_CERT_CHAIN: -150, 1031 | ERR_SSL_PROTOCOL_ERROR: -107, 1032 | ERR_SSL_RENEGOTIATION_REQUESTED: -114, 1033 | ERR_SSL_SERVER_CERT_BAD_FORMAT: -167, 1034 | ERR_SSL_SERVER_CERT_CHANGED: -156, 1035 | ERR_SSL_UNRECOGNIZED_NAME_ALERT: -159, 1036 | ERR_SSL_VERSION_OR_CIPHER_MISMATCH: -113, 1037 | ERR_SYN_REPLY_NOT_RECEIVED: -332, 1038 | ERR_TEMPORARILY_THROTTLED: -139, 1039 | ERR_TIMED_OUT: -7, 1040 | ERR_TLS13_DOWNGRADE_DETECTED: -180, 1041 | ERR_TOO_MANY_ACCEPT_CH_RESTARTS: -382, 1042 | ERR_TOO_MANY_REDIRECTS: -310, 1043 | ERR_TOO_MANY_RETRIES: -375, 1044 | ERR_TRUST_TOKEN_OPERATION_FAILED: -506, 1045 | ERR_TRUST_TOKEN_OPERATION_SUCCESS_WITHOUT_SENDING_REQUEST: -507, 1046 | ERR_TUNNEL_CONNECTION_FAILED: -111, 1047 | ERR_UNABLE_TO_REUSE_CONNECTION_FOR_PROXY_AUTH: -170, 1048 | ERR_UNDOCUMENTED_SECURITY_LIBRARY_STATUS: -344, 1049 | ERR_UNEXPECTED: -9, 1050 | ERR_UNEXPECTED_CONTENT_DICTIONARY_HEADER: -388, 1051 | ERR_UNEXPECTED_PROXY_AUTH: -323, 1052 | ERR_UNEXPECTED_SECURITY_LIBRARY_STATUS: -342, 1053 | ERR_UNKNOWN_URL_SCHEME: -302, 1054 | ERR_UNRECOGNIZED_FTP_DIRECTORY_LISTING_FORMAT: -334, 1055 | ERR_UNSAFE_PORT: -312, 1056 | ERR_UNSAFE_REDIRECT: -311, 1057 | ERR_UNSUPPORTED_AUTH_SCHEME: -339, 1058 | ERR_UPLOAD_FILE_CHANGED: -14, 1059 | ERR_UPLOAD_STREAM_REWIND_NOT_SUPPORTED: -25, 1060 | ERR_WINSOCK_UNEXPECTED_WRITTEN_BYTES: -124, 1061 | ERR_WRONG_VERSION_ON_EARLY_DATA: -179, 1062 | ERR_WS_PROTOCOL_ERROR: -145, 1063 | ERR_WS_THROTTLE_QUEUE_TOO_LARGE: -154, 1064 | ERR_WS_UPGRADE: -173, 1065 | ERR_ZSTD_WINDOW_SIZE_TOO_BIG: -386, 1066 | }, 1067 | quicError: { 1068 | IETF_QUIC_PROTOCOL_VIOLATION: 113, 1069 | INVALID_ERROR_CODE: 101, 1070 | QUIC_AEAD_LIMIT_REACHED: 173, 1071 | QUIC_ATTEMPT_TO_SEND_UNENCRYPTED_STREAM_DATA: 88, 1072 | QUIC_BAD_MULTIPATH_FLAG: 79, 1073 | QUIC_BAD_PACKET_LOSS_RATE: 71, 1074 | QUIC_CONNECTION_CANCELLED: 70, 1075 | QUIC_CONNECTION_ID_LIMIT_ERROR: 203, 1076 | QUIC_CONNECTION_IP_POOLED: 62, 1077 | QUIC_CONNECTION_MIGRATION_DISABLED_BY_CONFIG: 99, 1078 | QUIC_CONNECTION_MIGRATION_HANDSHAKE_UNCONFIRMED: 111, 1079 | QUIC_CONNECTION_MIGRATION_INTERNAL_ERROR: 100, 1080 | QUIC_CONNECTION_MIGRATION_NON_MIGRATABLE_STREAM: 84, 1081 | QUIC_CONNECTION_MIGRATION_NO_MIGRATABLE_STREAMS: 81, 1082 | QUIC_CONNECTION_MIGRATION_NO_NEW_NETWORK: 83, 1083 | QUIC_CONNECTION_MIGRATION_TOO_MANY_CHANGES: 82, 1084 | QUIC_CRYPTO_CHLO_TOO_LARGE: 90, 1085 | QUIC_CRYPTO_DUPLICATE_TAG: 43, 1086 | QUIC_CRYPTO_ENCRYPTION_LEVEL_INCORRECT: 44, 1087 | QUIC_CRYPTO_INTERNAL_ERROR: 38, 1088 | QUIC_CRYPTO_INVALID_VALUE_LENGTH: 31, 1089 | QUIC_CRYPTO_MESSAGE_AFTER_HANDSHAKE_COMPLETE: 32, 1090 | QUIC_CRYPTO_MESSAGE_INDEX_NOT_FOUND: 37, 1091 | QUIC_CRYPTO_MESSAGE_PARAMETER_NOT_FOUND: 35, 1092 | QUIC_CRYPTO_MESSAGE_PARAMETER_NO_OVERLAP: 36, 1093 | QUIC_CRYPTO_MESSAGE_WHILE_VALIDATING_CLIENT_HELLO: 54, 1094 | QUIC_CRYPTO_NO_SUPPORT: 40, 1095 | QUIC_CRYPTO_SERVER_CONFIG_EXPIRED: 45, 1096 | QUIC_CRYPTO_SYMMETRIC_KEY_SETUP_FAILED: 53, 1097 | QUIC_CRYPTO_TAGS_OUT_OF_ORDER: 29, 1098 | QUIC_CRYPTO_TOO_MANY_ENTRIES: 30, 1099 | QUIC_CRYPTO_TOO_MANY_REJECTS: 41, 1100 | QUIC_CRYPTO_UPDATE_BEFORE_HANDSHAKE_COMPLETE: 65, 1101 | QUIC_CRYPTO_VERSION_NOT_SUPPORTED: 39, 1102 | QUIC_DATA_RECEIVED_ON_WRITE_UNIDIRECTIONAL_STREAM: 115, 1103 | QUIC_DECOMPRESSION_FAILURE: 24, 1104 | QUIC_DECRYPTION_FAILURE: 12, 1105 | QUIC_EMPTY_STREAM_FRAME_NO_FIN: 50, 1106 | QUIC_ENCRYPTION_FAILURE: 13, 1107 | QUIC_ERROR_MIGRATING_ADDRESS: 26, 1108 | QUIC_ERROR_MIGRATING_PORT: 86, 1109 | QUIC_FAILED_TO_SERIALIZE_PACKET: 75, 1110 | QUIC_FLOW_CONTROL_INVALID_WINDOW: 64, 1111 | QUIC_FLOW_CONTROL_RECEIVED_TOO_MUCH_DATA: 59, 1112 | QUIC_FLOW_CONTROL_SENT_TOO_MUCH_DATA: 63, 1113 | QUIC_HANDSHAKE_FAILED: 28, 1114 | QUIC_HANDSHAKE_FAILED_PACKETS_BUFFERED_TOO_LONG: 214, 1115 | QUIC_HANDSHAKE_TIMEOUT: 67, 1116 | QUIC_HEADERS_STREAM_DATA_DECOMPRESS_FAILURE: 97, 1117 | QUIC_HPACK_COMPRESSED_HEADER_SIZE_EXCEEDS_LIMIT: 150, 1118 | QUIC_HPACK_DYNAMIC_TABLE_SIZE_UPDATE_NOT_ALLOWED: 145, 1119 | QUIC_HPACK_FRAGMENT_TOO_LONG: 149, 1120 | QUIC_HPACK_INDEX_VARINT_ERROR: 135, 1121 | QUIC_HPACK_INITIAL_TABLE_SIZE_UPDATE_IS_ABOVE_LOW_WATER_MARK: 146, 1122 | QUIC_HPACK_INVALID_INDEX: 143, 1123 | QUIC_HPACK_INVALID_NAME_INDEX: 144, 1124 | QUIC_HPACK_MISSING_DYNAMIC_TABLE_SIZE_UPDATE: 142, 1125 | QUIC_HPACK_NAME_HUFFMAN_ERROR: 140, 1126 | QUIC_HPACK_NAME_LENGTH_VARINT_ERROR: 136, 1127 | QUIC_HPACK_NAME_TOO_LONG: 138, 1128 | QUIC_HPACK_TABLE_SIZE_UPDATE_IS_ABOVE_ACKNOWLEDGED_SETTING: 147, 1129 | QUIC_HPACK_TRUNCATED_BLOCK: 148, 1130 | QUIC_HPACK_VALUE_HUFFMAN_ERROR: 141, 1131 | QUIC_HPACK_VALUE_LENGTH_VARINT_ERROR: 137, 1132 | QUIC_HPACK_VALUE_TOO_LONG: 139, 1133 | QUIC_HTTP_CLOSED_CRITICAL_STREAM: 156, 1134 | QUIC_HTTP_DECODER_ERROR: 120, 1135 | QUIC_HTTP_DUPLICATE_SETTING_IDENTIFIER: 158, 1136 | QUIC_HTTP_DUPLICATE_UNIDIRECTIONAL_STREAM: 153, 1137 | QUIC_HTTP_FRAME_ERROR: 132, 1138 | QUIC_HTTP_FRAME_TOO_LARGE: 131, 1139 | QUIC_HTTP_FRAME_UNEXPECTED_ON_CONTROL_STREAM: 134, 1140 | QUIC_HTTP_FRAME_UNEXPECTED_ON_SPDY_STREAM: 133, 1141 | QUIC_HTTP_GOAWAY_ID_LARGER_THAN_PREVIOUS: 167, 1142 | QUIC_HTTP_GOAWAY_INVALID_STREAM_ID: 166, 1143 | QUIC_HTTP_INVALID_FRAME_SEQUENCE_ON_CONTROL_STREAM: 152, 1144 | QUIC_HTTP_INVALID_FRAME_SEQUENCE_ON_SPDY_STREAM: 151, 1145 | QUIC_HTTP_INVALID_MAX_PUSH_ID: 159, 1146 | QUIC_HTTP_INVALID_SETTING_VALUE: 207, 1147 | QUIC_HTTP_MISSING_SETTINGS_FRAME: 157, 1148 | QUIC_HTTP_RECEIVE_SERVER_PUSH: 205, 1149 | QUIC_HTTP_RECEIVE_SPDY_FRAME: 171, 1150 | QUIC_HTTP_RECEIVE_SPDY_SETTING: 169, 1151 | QUIC_HTTP_SERVER_INITIATED_BIDIRECTIONAL_STREAM: 154, 1152 | QUIC_HTTP_STREAM_LIMIT_TOO_LOW: 160, 1153 | QUIC_HTTP_STREAM_WRONG_DIRECTION: 155, 1154 | QUIC_HTTP_ZERO_RTT_REJECTION_SETTINGS_MISMATCH: 165, 1155 | QUIC_HTTP_ZERO_RTT_RESUMPTION_SETTINGS_MISMATCH: 164, 1156 | QUIC_IETF_GQUIC_ERROR_MISSING: 122, 1157 | QUIC_INTERNAL_ERROR: 1, 1158 | QUIC_INVALID_0RTT_PACKET_NUMBER_OUT_OF_ORDER: 192, 1159 | QUIC_INVALID_ACK_DATA: 9, 1160 | QUIC_INVALID_BLOCKED_DATA: 58, 1161 | QUIC_INVALID_CHANNEL_ID_SIGNATURE: 52, 1162 | QUIC_INVALID_CHARACTER_IN_FIELD_VALUE: 206, 1163 | QUIC_INVALID_CONNECTION_CLOSE_DATA: 7, 1164 | QUIC_INVALID_CRYPTO_MESSAGE_PARAMETER: 34, 1165 | QUIC_INVALID_CRYPTO_MESSAGE_TYPE: 33, 1166 | QUIC_INVALID_FEC_DATA: 5, 1167 | QUIC_INVALID_FRAME_DATA: 4, 1168 | QUIC_INVALID_GOAWAY_DATA: 8, 1169 | QUIC_INVALID_HEADERS_STREAM_DATA: 56, 1170 | QUIC_INVALID_HEADER_ID: 22, 1171 | QUIC_INVALID_MAX_DATA_FRAME_DATA: 102, 1172 | QUIC_INVALID_MAX_STREAM_DATA_FRAME_DATA: 103, 1173 | QUIC_INVALID_MESSAGE_DATA: 112, 1174 | QUIC_INVALID_NEGOTIATED_VALUE: 23, 1175 | QUIC_INVALID_NEW_CONNECTION_ID_DATA: 107, 1176 | QUIC_INVALID_NEW_TOKEN: 114, 1177 | QUIC_INVALID_PACKET_HEADER: 3, 1178 | QUIC_INVALID_PATH_CHALLENGE_DATA: 109, 1179 | QUIC_INVALID_PATH_CLOSE_DATA: 78, 1180 | QUIC_INVALID_PATH_RESPONSE_DATA: 110, 1181 | QUIC_INVALID_PRIORITY: 49, 1182 | QUIC_INVALID_PRIORITY_UPDATE: 193, 1183 | QUIC_INVALID_PUBLIC_RST_PACKET: 11, 1184 | QUIC_INVALID_RETIRE_CONNECTION_ID_DATA: 117, 1185 | QUIC_INVALID_RST_STREAM_DATA: 6, 1186 | QUIC_INVALID_STOP_SENDING_FRAME_DATA: 108, 1187 | QUIC_INVALID_STOP_WAITING_DATA: 60, 1188 | QUIC_INVALID_STREAM_BLOCKED_DATA: 106, 1189 | QUIC_INVALID_STREAM_DATA: 46, 1190 | QUIC_INVALID_STREAM_ID: 17, 1191 | QUIC_INVALID_VERSION: 20, 1192 | QUIC_INVALID_VERSION_NEGOTIATION_PACKET: 10, 1193 | QUIC_INVALID_WINDOW_UPDATE_DATA: 57, 1194 | QUIC_IP_ADDRESS_CHANGED: 80, 1195 | QUIC_KEY_UPDATE_ERROR: 172, 1196 | QUIC_MAX_AGE_TIMEOUT: 191, 1197 | QUIC_MAX_STREAMS_DATA: 104, 1198 | QUIC_MAX_STREAMS_ERROR: 119, 1199 | QUIC_MAYBE_CORRUPTED_MEMORY: 89, 1200 | QUIC_MISSING_PAYLOAD: 48, 1201 | QUIC_MISSING_WRITE_KEYS: 170, 1202 | QUIC_MULTIPATH_PATH_DOES_NOT_EXIST: 91, 1203 | QUIC_MULTIPATH_PATH_NOT_ACTIVE: 92, 1204 | QUIC_NETWORK_IDLE_TIMEOUT: 25, 1205 | QUIC_NO_ERROR: 0, 1206 | QUIC_OVERLAPPING_STREAM_DATA: 87, 1207 | QUIC_PACKET_READ_ERROR: 51, 1208 | QUIC_PACKET_TOO_LARGE: 14, 1209 | QUIC_PACKET_WRITE_ERROR: 27, 1210 | QUIC_PACKET_WRONG_VERSION: 212, 1211 | QUIC_PEER_GOING_AWAY: 16, 1212 | QUIC_PEER_PORT_CHANGE_HANDSHAKE_UNCONFIRMED: 194, 1213 | QUIC_PROOF_INVALID: 42, 1214 | QUIC_PUBLIC_RESET: 19, 1215 | QUIC_PUBLIC_RESETS_POST_HANDSHAKE: 73, 1216 | QUIC_QPACK_DECODER_STREAM_ERROR: 128, 1217 | QUIC_QPACK_DECODER_STREAM_IMPOSSIBLE_INSERT_COUNT: 189, 1218 | QUIC_QPACK_DECODER_STREAM_INCORRECT_ACKNOWLEDGEMENT: 190, 1219 | QUIC_QPACK_DECODER_STREAM_INCREMENT_OVERFLOW: 188, 1220 | QUIC_QPACK_DECODER_STREAM_INTEGER_TOO_LARGE: 186, 1221 | QUIC_QPACK_DECODER_STREAM_INVALID_ZERO_INCREMENT: 187, 1222 | QUIC_QPACK_DECOMPRESSION_FAILED: 126, 1223 | QUIC_QPACK_ENCODER_STREAM_DUPLICATE_DYNAMIC_ENTRY_NOT_FOUND: 184, 1224 | QUIC_QPACK_ENCODER_STREAM_DUPLICATE_INVALID_RELATIVE_INDEX: 183, 1225 | QUIC_QPACK_ENCODER_STREAM_ERROR: 127, 1226 | QUIC_QPACK_ENCODER_STREAM_ERROR_INSERTING_DYNAMIC: 181, 1227 | QUIC_QPACK_ENCODER_STREAM_ERROR_INSERTING_LITERAL: 182, 1228 | QUIC_QPACK_ENCODER_STREAM_ERROR_INSERTING_STATIC: 178, 1229 | QUIC_QPACK_ENCODER_STREAM_HUFFMAN_ENCODING_ERROR: 176, 1230 | QUIC_QPACK_ENCODER_STREAM_INSERTION_DYNAMIC_ENTRY_NOT_FOUND: 180, 1231 | QUIC_QPACK_ENCODER_STREAM_INSERTION_INVALID_RELATIVE_INDEX: 179, 1232 | QUIC_QPACK_ENCODER_STREAM_INTEGER_TOO_LARGE: 174, 1233 | QUIC_QPACK_ENCODER_STREAM_INVALID_STATIC_ENTRY: 177, 1234 | QUIC_QPACK_ENCODER_STREAM_SET_DYNAMIC_TABLE_CAPACITY: 185, 1235 | QUIC_QPACK_ENCODER_STREAM_STRING_LITERAL_TOO_LONG: 175, 1236 | QUIC_SERVER_UNHEALTHY: 213, 1237 | QUIC_SILENT_IDLE_TIMEOUT: 168, 1238 | QUIC_STALE_CONNECTION_CANCELLED: 121, 1239 | QUIC_STREAMS_BLOCKED_DATA: 105, 1240 | QUIC_STREAMS_BLOCKED_ERROR: 118, 1241 | QUIC_STREAM_DATA_AFTER_TERMINATION: 2, 1242 | QUIC_STREAM_DATA_BEYOND_CLOSE_OFFSET: 129, 1243 | QUIC_STREAM_LENGTH_OVERFLOW: 98, 1244 | QUIC_STREAM_MULTIPLE_OFFSET: 130, 1245 | QUIC_STREAM_SEQUENCER_INVALID_STATE: 95, 1246 | QUIC_TLS_BAD_CERTIFICATE: 195, 1247 | QUIC_TLS_CERTIFICATE_EXPIRED: 198, 1248 | QUIC_TLS_CERTIFICATE_REQUIRED: 202, 1249 | QUIC_TLS_CERTIFICATE_REVOKED: 197, 1250 | QUIC_TLS_CERTIFICATE_UNKNOWN: 199, 1251 | QUIC_TLS_INTERNAL_ERROR: 200, 1252 | QUIC_TLS_KEYING_MATERIAL_EXPORTS_MISMATCH: 209, 1253 | QUIC_TLS_KEYING_MATERIAL_EXPORT_NOT_AVAILABLE: 210, 1254 | QUIC_TLS_UNEXPECTED_KEYING_MATERIAL_EXPORT_LABEL: 208, 1255 | QUIC_TLS_UNRECOGNIZED_NAME: 201, 1256 | QUIC_TLS_UNSUPPORTED_CERTIFICATE: 196, 1257 | QUIC_TOO_MANY_AVAILABLE_STREAMS: 76, 1258 | QUIC_TOO_MANY_BUFFERED_CONTROL_FRAMES: 124, 1259 | QUIC_TOO_MANY_CONNECTION_ID_WAITING_TO_RETIRE: 204, 1260 | QUIC_TOO_MANY_OPEN_STREAMS: 18, 1261 | QUIC_TOO_MANY_OUTSTANDING_RECEIVED_PACKETS: 69, 1262 | QUIC_TOO_MANY_OUTSTANDING_SENT_PACKETS: 68, 1263 | QUIC_TOO_MANY_RTOS: 85, 1264 | QUIC_TOO_MANY_SESSIONS_ON_SERVER: 96, 1265 | QUIC_TOO_MANY_STREAM_DATA_INTERVALS: 93, 1266 | QUIC_TRANSPORT_INVALID_CLIENT_INDICATION: 125, 1267 | QUIC_TRY_TO_WRITE_DATA_ON_READ_UNIDIRECTIONAL_STREAM: 116, 1268 | QUIC_UNENCRYPTED_FEC_DATA: 77, 1269 | QUIC_UNENCRYPTED_STREAM_DATA: 61, 1270 | QUIC_UNEXPECTED_DATA_BEFORE_ENCRYPTION_ESTABLISHED: 211, 1271 | QUIC_UNSUPPORTED_PROOF_DEMAND: 94, 1272 | QUIC_VERSION_NEGOTIATION_MISMATCH: 55, 1273 | QUIC_WINDOW_UPDATE_RECEIVED_ON_READ_UNIDIRECTIONAL_STREAM: 123, 1274 | QUIC_ZERO_RTT_REJECTION_LIMIT_REDUCED: 162, 1275 | QUIC_ZERO_RTT_RESUMPTION_LIMIT_REDUCED: 163, 1276 | QUIC_ZERO_RTT_UNRETRANSMITTABLE: 161, 1277 | }, 1278 | quicRstStreamError: { 1279 | QUIC_APPLICATION_DONE_WITH_STREAM: 38, 1280 | QUIC_BAD_APPLICATION_PAYLOAD: 3, 1281 | QUIC_DATA_AFTER_CLOSE_OFFSET: 17, 1282 | QUIC_DUPLICATE_PROMISE_URL: 11, 1283 | QUIC_ERROR_PROCESSING_STREAM: 1, 1284 | QUIC_HEADERS_TOO_LARGE: 15, 1285 | QUIC_INVALID_PROMISE_METHOD: 13, 1286 | QUIC_INVALID_PROMISE_URL: 9, 1287 | QUIC_MULTIPLE_TERMINATION_OFFSETS: 2, 1288 | QUIC_PROMISE_VARY_MISMATCH: 12, 1289 | QUIC_PUSH_STREAM_TIMED_OUT: 14, 1290 | QUIC_REFUSED_STREAM: 8, 1291 | QUIC_RST_ACKNOWLEDGEMENT: 7, 1292 | QUIC_STREAM_CANCELLED: 6, 1293 | QUIC_STREAM_CLOSED_CRITICAL_STREAM: 21, 1294 | QUIC_STREAM_CONNECTION_ERROR: 4, 1295 | QUIC_STREAM_CONNECT_ERROR: 30, 1296 | QUIC_STREAM_DECODER_STREAM_ERROR: 34, 1297 | QUIC_STREAM_DECOMPRESSION_FAILED: 32, 1298 | QUIC_STREAM_ENCODER_STREAM_ERROR: 33, 1299 | QUIC_STREAM_EXCESSIVE_LOAD: 24, 1300 | QUIC_STREAM_FRAME_ERROR: 23, 1301 | QUIC_STREAM_FRAME_UNEXPECTED: 22, 1302 | QUIC_STREAM_GENERAL_PROTOCOL_ERROR: 18, 1303 | QUIC_STREAM_ID_ERROR: 25, 1304 | QUIC_STREAM_INTERNAL_ERROR: 19, 1305 | QUIC_STREAM_MISSING_SETTINGS: 27, 1306 | QUIC_STREAM_NO_ERROR: 0, 1307 | QUIC_STREAM_PEER_GOING_AWAY: 5, 1308 | QUIC_STREAM_REQUEST_INCOMPLETE: 29, 1309 | QUIC_STREAM_REQUEST_REJECTED: 28, 1310 | QUIC_STREAM_SETTINGS_ERROR: 26, 1311 | QUIC_STREAM_STREAM_CREATION_ERROR: 20, 1312 | QUIC_STREAM_TTL_EXPIRED: 16, 1313 | QUIC_STREAM_UNKNOWN_APPLICATION_ERROR_CODE: 35, 1314 | QUIC_STREAM_VERSION_FALLBACK: 31, 1315 | QUIC_STREAM_WEBTRANSPORT_BUFFERED_STREAMS_LIMIT_EXCEEDED: 37, 1316 | QUIC_STREAM_WEBTRANSPORT_SESSION_GONE: 36, 1317 | QUIC_UNAUTHORIZED_PROMISE_URL: 10, 1318 | }, 1319 | secureDnsMode: { 1320 | Automatic: 1, 1321 | Off: 0, 1322 | Secure: 2, 1323 | }, 1324 | timeTickOffset: 1726201829726, 1325 | }; 1326 | } 1327 | --------------------------------------------------------------------------------