diff --git a/front_end/models/trace/extras/ThirdParties.test.ts b/front_end/models/trace/extras/ThirdParties.test.ts index 84ac08cbcd..6720ab01f0 100644 --- a/front_end/models/trace/extras/ThirdParties.test.ts +++ b/front_end/models/trace/extras/ThirdParties.test.ts @@ -6,44 +6,39 @@ import {describeWithEnvironment} from '../../../testing/EnvironmentHelpers.js'; import {TraceLoader} from '../../../testing/TraceLoader.js'; import * as Trace from '../trace.js'; +function extractUrlsFromSummaries(summaries: Trace.Extras.ThirdParties.Summary[]): Array<[string, string[]]> { + return summaries.map(s => { + const uniqueUrls = new Set(); + s.relatedEvents?.forEach(e => { + const url = e.args?.data?.url; + if (url) { + uniqueUrls.add(url); + } + }); + return [s.entity.name, Array.from(uniqueUrls)]; + }); +} + describeWithEnvironment('ThirdParties', function() { describe('byTraceBounds', function() { it('full trace bounds', async function() { const {parsedTrace} = await TraceLoader.traceEngine(this, 'load-simple.json.gz'); + const summaries = Trace.Extras.ThirdParties.summarizeThirdParties(parsedTrace, parsedTrace.Meta.traceBounds); - const requests = parsedTrace.NetworkRequests.byTime.filter( - request => Trace.Helpers.Timing.eventIsInBounds(request, parsedTrace.Meta.traceBounds)); - const thirdPartySummary = - Trace.Extras.ThirdParties.summarizeThirdParties(parsedTrace, parsedTrace.Meta.traceBounds, requests); - - const results = [...thirdPartySummary.byEntity.entries()].map(([entity, summary]) => [entity.name, summary]); + const results = summaries.map(s => [s.entity.name, s.mainThreadTime, s.transferSize]); assert.deepEqual(results, [ - ['localhost', {mainThreadTime: 26381, transferSize: 751}], - ['Google Fonts', {mainThreadTime: 0, transferSize: 0}], + ['localhost', 24.947999954223633, 2254], + ['Google Fonts', 0, 25325], ]); - - assert.deepEqual([...thirdPartySummary.byUrl.entries()], [ - ['extensions::SafeBuiltins', {mainThreadTime: 1448, transferSize: 0}], - ['http://localhost:8080/', {mainThreadTime: 21674, transferSize: 751}], - ['http://localhost:8080/blocking.js', {mainThreadTime: 2451, transferSize: 0}], - ['http://localhost:8080/module.js', {mainThreadTime: 2256, transferSize: 0}], - ['https://fonts.googleapis.com/css2?family=Orelega+One&display=swap', {mainThreadTime: 0, transferSize: 0}], - ['http://localhost:8080/styles.css', {mainThreadTime: 0, transferSize: 0}], - [ - 'https://fonts.gstatic.com/s/orelegaone/v1/3qTpojOggD2XtAdFb-QXZFt93kY.woff2', - {mainThreadTime: 0, transferSize: 0} - ], - ]); - - const urls = [...thirdPartySummary.urlsByEntity.entries()].map(([entity, urls]) => [entity.name, [...urls]]); + const urls = extractUrlsFromSummaries(summaries); assert.deepEqual(urls, [ [ 'localhost', [ 'http://localhost:8080/', + 'http://localhost:8080/styles.css', 'http://localhost:8080/blocking.js', 'http://localhost:8080/module.js', - 'http://localhost:8080/styles.css', ] ], [ @@ -56,38 +51,6 @@ describeWithEnvironment('ThirdParties', function() { ]); }); - it('works even without network requests', async function() { - const {parsedTrace} = await TraceLoader.traceEngine(this, 'load-simple.json.gz'); - - const thirdPartySummary = - Trace.Extras.ThirdParties.summarizeThirdParties(parsedTrace, parsedTrace.Meta.traceBounds, []); - - const results = [...thirdPartySummary.byEntity.entries()].map(([entity, summary]) => [entity.name, summary]); - assert.deepEqual(results, [ - // Since network requests were not given, there is no transfer size. - ['localhost', {mainThreadTime: 26381, transferSize: 0}], - ]); - - assert.deepEqual([...thirdPartySummary.byUrl.entries()], [ - ['extensions::SafeBuiltins', {mainThreadTime: 1448, transferSize: 0}], - ['http://localhost:8080/', {mainThreadTime: 21674, transferSize: 0}], - ['http://localhost:8080/blocking.js', {mainThreadTime: 2451, transferSize: 0}], - ['http://localhost:8080/module.js', {mainThreadTime: 2256, transferSize: 0}], - ]); - - const urls = [...thirdPartySummary.urlsByEntity.entries()].map(([entity, urls]) => [entity.name, [...urls]]); - assert.deepEqual(urls, [ - [ - 'localhost', - [ - 'http://localhost:8080/', - 'http://localhost:8080/blocking.js', - 'http://localhost:8080/module.js', - ] - ], - ]); - }); - it('partial trace bounds', async function() { const {parsedTrace} = await TraceLoader.traceEngine(this, 'load-simple.json.gz'); @@ -96,14 +59,12 @@ describeWithEnvironment('ThirdParties', function() { const max = Trace.Types.Timing.Micro(1634222320000); const bounds = {min, max, range: Trace.Types.Timing.Micro(max - min)}; - const requests = - parsedTrace.NetworkRequests.byTime.filter(request => Trace.Helpers.Timing.eventIsInBounds(request, bounds)); - const thirdPartySummary = Trace.Extras.ThirdParties.summarizeThirdParties(parsedTrace, bounds, requests); + const summaries = Trace.Extras.ThirdParties.summarizeThirdParties(parsedTrace, bounds); - const results = [...thirdPartySummary.byEntity.entries()].map(([entity, summary]) => [entity.name, summary]); + const results = summaries.map(s => [s.entity.name, s.mainThreadTime, s.transferSize]); assert.deepEqual(results, [ // No main thread during given bounds. Some network. - ['localhost', {mainThreadTime: 0, transferSize: 751}], + ['localhost', 0, 419], ]); }); @@ -113,12 +74,9 @@ describeWithEnvironment('ThirdParties', function() { const min = Trace.Types.Timing.Micro(1634230000000); const max = Trace.Types.Timing.Micro(1634231000000); const bounds = {min, max, range: Trace.Types.Timing.Micro(max - min)}; + const summaries = Trace.Extras.ThirdParties.summarizeThirdParties(parsedTrace, bounds); - const requests = - parsedTrace.NetworkRequests.byTime.filter(request => Trace.Helpers.Timing.eventIsInBounds(request, bounds)); - const thirdPartySummary = Trace.Extras.ThirdParties.summarizeThirdParties(parsedTrace, bounds, requests); - - const results = [...thirdPartySummary.byEntity.entries()].map(([entity, summary]) => [entity.name, summary]); + const results = summaries.map(s => [s.entity.name, s.mainThreadTime, s.transferSize]); assert.deepEqual(results, []); }); }); diff --git a/front_end/models/trace/extras/ThirdParties.ts b/front_end/models/trace/extras/ThirdParties.ts index a0b5ff0ca6..3cb4d7bde2 100644 --- a/front_end/models/trace/extras/ThirdParties.ts +++ b/front_end/models/trace/extras/ThirdParties.ts @@ -2,16 +2,21 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import * as ThirdPartyWeb from '../../../third_party/third-party-web/third-party-web.js'; -import * as Handlers from '../handlers/handlers.js'; +import type * as ThirdPartyWeb from '../../../third_party/third-party-web/third-party-web.js'; +import type * as Handlers from '../handlers/handlers.js'; import * as Helpers from '../helpers/helpers.js'; import * as Types from '../types/types.js'; +import * as TraceFilter from './TraceFilter.js'; +import * as TraceTree from './TraceTree.js'; + export type Entity = typeof ThirdPartyWeb.ThirdPartyWeb.entities[number]; export interface Summary { transferSize: number; - mainThreadTime: Types.Timing.Micro; + mainThreadTime: Types.Timing.Milli; + relatedEvents?: Types.Events.Event[]; + entity: Entity; } export interface ThirdPartySummary { @@ -22,190 +27,99 @@ export interface ThirdPartySummary { madeUpEntityCache: Map; } -function getOrMakeSummaryByEntity( - thirdPartySummary: ThirdPartySummary, event: Types.Events.Event, url: string): Summary|null { - const entity = ThirdPartyWeb.ThirdPartyWeb.getEntity(url) ?? - Handlers.Helpers.makeUpEntity(thirdPartySummary.madeUpEntityCache, url); - if (!entity) { - return null; +/** + * + * Returns Main frame main thread events. + * These events are inline with the ones used by selectedEvents() of TimelineTreeViews + */ +function collectMainThreadActivity(parsedTrace: Handlers.Types.ParsedTrace): Types.Events.Event[] { + // TODO: Note b/402658800 could be an issue here. + const mainFrameMainThread = parsedTrace.Renderer.processes.values() + .find(p => { + const url = p.url ?? ''; + // Frame url checked a la CompatibilityTracksAppenders's addThreadAppenders + return p.isOnMainFrame && !url.startsWith('about:') && !url.startsWith('chrome:'); + }) + ?.threads.values() + .find(t => t.name === 'CrRendererMain'); + + if (!mainFrameMainThread) { + return []; } - const urls = thirdPartySummary.urlsByEntity.get(entity) ?? new Set(); - urls.add(url); - thirdPartySummary.urlsByEntity.set(entity, urls); - - const events = thirdPartySummary.eventsByEntity.get(entity) ?? []; - events.push(event); - thirdPartySummary.eventsByEntity.set(entity, events); - - let summary = thirdPartySummary.byEntity.get(entity); - if (summary) { - return summary; - } - - summary = {transferSize: 0, mainThreadTime: Types.Timing.Micro(0)}; - thirdPartySummary.byEntity.set(entity, summary); - return summary; -} - -function getOrMakeSummaryByURL(thirdPartySummary: ThirdPartySummary, url: string): Summary|null { - let summary = thirdPartySummary.byUrl.get(url); - if (summary) { - return summary; - } - - summary = {transferSize: 0, mainThreadTime: Types.Timing.Micro(0)}; - thirdPartySummary.byUrl.set(url, summary); - return summary; -} - -// TODO: Remove and use the the BottomUpRootNode defined in ThirdPartyTreeView instead. -function collectMainThreadActivity( - thirdPartySummary: ThirdPartySummary, parsedTrace: Handlers.Types.ParsedTrace, - bounds: Types.Timing.TraceWindowMicro): void { - for (const process of parsedTrace.Renderer.processes.values()) { - if (!process.isOnMainFrame) { - continue; - } - - for (const thread of process.threads.values()) { - if (thread.name === 'CrRendererMain') { - if (!thread.tree) { - break; - } - - for (const event of thread.entries) { - if (!Helpers.Timing.eventIsInBounds(event, bounds)) { - continue; - } - - const node = parsedTrace.Renderer.entryToNode.get(event); - if (!node || !node.selfTime) { - continue; - } - - const url = Handlers.Helpers.getNonResolvedURL(event, parsedTrace); - if (!url) { - continue; - } - - let summary = getOrMakeSummaryByEntity(thirdPartySummary, event, url); - if (summary) { - summary.mainThreadTime = (summary.mainThreadTime + node.selfTime) as Types.Timing.Micro; - } - - summary = getOrMakeSummaryByURL(thirdPartySummary, url); - if (summary) { - summary.mainThreadTime = (summary.mainThreadTime + node.selfTime) as Types.Timing.Micro; - } - } - } - } - } -} - -function collectNetworkActivity( - thirdPartySummary: ThirdPartySummary, requests: Types.Events.SyntheticNetworkRequest[]): void { - for (const request of requests) { - const url = request.args.data.url; - - let summary = getOrMakeSummaryByEntity(thirdPartySummary, request, url); - if (summary) { - summary.transferSize += request.args.data.encodedDataLength; - } - - summary = getOrMakeSummaryByURL(thirdPartySummary, url); - if (summary) { - summary.transferSize += request.args.data.encodedDataLength; - } - } + return mainFrameMainThread.entries; } /** * @param networkRequests Won't be filtered by trace bounds, so callers should ensure it is filtered. */ export function summarizeThirdParties( - parsedTrace: Handlers.Types.ParsedTrace, traceBounds: Types.Timing.TraceWindowMicro, - networkRequests: Types.Events.SyntheticNetworkRequest[]): ThirdPartySummary { - const thirdPartySummary: ThirdPartySummary = { - byEntity: new Map(), - byUrl: new Map(), - urlsByEntity: new Map(), - eventsByEntity: new Map(), - madeUpEntityCache: new Map(), - }; + parsedTrace: Handlers.Types.ParsedTrace, traceBounds: Types.Timing.TraceWindowMicro): Summary[] { + const mainThreadEvents = collectMainThreadActivity(parsedTrace).sort(Helpers.Trace.eventTimeComparator); + const node = getBottomUpTree(mainThreadEvents, parsedTrace, traceBounds); + const summaries = summarizeBottomUp(node, parsedTrace); - collectMainThreadActivity(thirdPartySummary, parsedTrace, traceBounds); - collectNetworkActivity(thirdPartySummary, networkRequests); - - return thirdPartySummary; + return summaries; } -function getSummaryMapWithMapping( - events: Types.Events.Event[], entityByEvent: Map, - eventsByEntity: Map): ThirdPartySummary { - const byEvent = new Map(); - const byEntity = new Map(); - const defaultSummary: Summary = {transferSize: 0, mainThreadTime: Types.Timing.Micro(0)}; +function summarizeBottomUp( + thirdPartyBottomUp: TraceTree.BottomUpRootNode, parsedTrace: Handlers.Types.ParsedTrace): Summary[] { + const summaryForEntity: Map = new Map(); + const summaries: Summary[] = []; - for (const event of events) { - const urlSummary = byEvent.get(event) || {...defaultSummary}; - if (Types.Events.isSyntheticNetworkRequest(event)) { - urlSummary.transferSize += event.args.data.encodedDataLength; + // Our top nodes are the 3P entities. + // Tree nodes are built lazily, .children() is essential, it triggers the + // construction of the root node's child nodes. + const topNodes = [...thirdPartyBottomUp.children().values()].flat(); + for (const node of topNodes) { + if (node.id === '') { + continue; } - byEvent.set(event, urlSummary); - } - - // Map each request's stat to a particular entity. - for (const [request, requestSummary] of byEvent.entries()) { - const entity = entityByEvent.get(request); + const entity = parsedTrace.Renderer.entityMappings.entityByEvent.get(node.event); if (!entity) { - byEvent.delete(request); continue; } - const entitySummary = byEntity.get(entity) || {...defaultSummary}; - entitySummary.transferSize += requestSummary.transferSize; - byEntity.set(entity, entitySummary); + const summary: Summary = { + transferSize: node.transferSize, + mainThreadTime: Types.Timing.Milli(node.selfTime), + // Lets use the mapper events as our source of events, since we use the main thread to construct + // the bottom up tree. The mapper will give us all related events. + relatedEvents: parsedTrace.Renderer.entityMappings.eventsByEntity.get(entity) ?? [], + entity, + }; + summaryForEntity.set(entity, summary); + summaries.push(summary); } - - return {byEntity, eventsByEntity, madeUpEntityCache: new Map(), byUrl: new Map(), urlsByEntity: new Map()}; + return summaries; } -// TODO(crbug.com/352244718): Remove or refactor to use summarizeThirdParties/collectMainThreadActivity/etc. -/** - * Note: unlike summarizeThirdParties, this does not calculate mainThreadTime. The reason is that it is not - * needed for its one use case, and when dragging the trace bounds it takes a long time to calculate. - * If it is ever needed, we need to make getSelfTimeByUrl (see deleted code/blame) much faster (cache + bucket?). - */ -export function getSummariesAndEntitiesWithMapping( - parsedTrace: Handlers.Types.ParsedTrace, traceBounds: Types.Timing.TraceWindowMicro, - entityMapping: Handlers.Helpers.EntityMappings): { - summaries: ThirdPartySummary, - entityByEvent: Map, -} { - const entityByEvent = new Map(entityMapping.entityByEvent); - const eventsByEntity = new Map(entityMapping.eventsByEntity); +function getBottomUpTree( + mainThreadEvents: Types.Events.Event[], parsedTrace: Handlers.Types.ParsedTrace, + tracebounds: Types.Timing.TraceWindowMicro): TraceTree.BottomUpRootNode { + const mappings = parsedTrace.Renderer.entityMappings; - // Consider events only in bounds. - const entityByEventArr = Array.from(entityByEvent.entries()); - const filteredEntries = entityByEventArr.filter(([event]) => { - return Helpers.Timing.eventIsInBounds(event, traceBounds); - }); - const entityByEventFiltered = new Map(filteredEntries); + const groupingFunction = (event: Types.Events.Event): string => { + const entity = mappings?.entityByEvent.get(event); + return entity?.name ?? ''; + }; + // Use the same filtering as front_end/panels/timeline/TimelineTreeView.ts. + const visibleEvents = Helpers.Trace.VISIBLE_TRACE_EVENT_TYPES.values().toArray(); + const filter = + new TraceFilter.VisibleEventsFilter(visibleEvents.concat([Types.Events.Name.SYNTHETIC_NETWORK_REQUEST])); - // Consider events only in bounds. - const eventsByEntityArr = Array.from(eventsByEntity.entries()); - const filtered = eventsByEntityArr.filter(([, events]) => { - events.map(event => { - return Helpers.Timing.eventIsInBounds(event, traceBounds); - }); - return events.length > 0; - }); - const eventsByEntityFiltered = new Map(filtered); - - const allEvents = Array.from(entityByEvent.keys()); - const summaries = getSummaryMapWithMapping(allEvents, entityByEventFiltered, eventsByEntityFiltered); - - return {summaries, entityByEvent: entityByEventFiltered}; + // The bottom up root node handles all the "in Tracebounds" checks we need for the insight. + const startTime = Helpers.Timing.microToMilli(tracebounds.min); + const endTime = Helpers.Timing.microToMilli(tracebounds.max); + const node = new TraceTree.BottomUpRootNode(mainThreadEvents, { + textFilter: new TraceFilter.ExclusiveNameFilter([]), + filters: [filter], + startTime, + endTime, + eventGroupIdCallback: groupingFunction, + calculateTransferSize: true, + // Ensure we group by 3P alongside eventID for correct 3P grouping. + forceGroupIdCallback: true, + }) as TraceTree.BottomUpRootNode; + return node; } diff --git a/front_end/models/trace/extras/TraceTree.test.ts b/front_end/models/trace/extras/TraceTree.test.ts index 653f82741d..196c6a5524 100644 --- a/front_end/models/trace/extras/TraceTree.test.ts +++ b/front_end/models/trace/extras/TraceTree.test.ts @@ -427,6 +427,55 @@ describeWithEnvironment('TraceTree', () => { const eventId = TraceTree.generateEventID(profileCallEntry); assert.strictEqual(eventId, 'f:Compile@0'); }); + it('correctly groups events with eventGroupIdCallback when using forceGroupIdCallback', () => { + // This builds the following tree: + // |------------ROOT-----------| + // |-----A----| |-----B-----| + // |-C-| |-D-| |-E-| + + // Third party 1 + const eventC = makeProfileCall('func', 0, 10_000); + const eventD = makeCompleteEvent('event D', 10_000, 10_000); + + // Third party 2 + const eventA = makeProfileCall('func', 0, 40_000); + const eventB = makeCompleteEvent('event D', 50_000, 40_000); + const eventE = makeCompleteEvent('event D', 50_000, 5_000); + // Events must be in order. + const events = [ + eventA, + eventC, + eventD, + eventB, + eventE, + ]; + const root = new TraceTree.BottomUpRootNode(events, { + textFilter: new Trace.Extras.TraceFilter.InvisibleEventsFilter([]), + filters: [], + startTime: Trace.Types.Timing.Milli(0), + endTime: Trace.Types.Timing.Milli(200_000), + eventGroupIdCallback: event => { + if (event === eventC || event === eventD) { + return 'thirdParty1'; + } + return 'thirdParty2'; + }, + forceGroupIdCallback: true, + }); + const rootChildren = root.children(); + // 2 top nodes for each third party + assert.strictEqual(rootChildren.size, 2); + + const children = Array.from(rootChildren.values()) as TraceTree.BottomUpNode[]; + const first = children[0]; + const second = children[1]; + + assert.strictEqual(first.id, 'thirdParty1'); + assert.lengthOf(first.events, 2); + + assert.strictEqual(second.id, 'thirdParty2'); + assert.lengthOf(second.events, 3); + }); }); describe('eventStackFrame', () => { diff --git a/front_end/models/trace/extras/TraceTree.ts b/front_end/models/trace/extras/TraceTree.ts index aa4c51f5c3..76109b207c 100644 --- a/front_end/models/trace/extras/TraceTree.ts +++ b/front_end/models/trace/extras/TraceTree.ts @@ -324,9 +324,10 @@ export class BottomUpRootNode extends Node { readonly filter: (e: Types.Events.Event) => boolean; readonly startTime: Types.Timing.Milli; readonly endTime: Types.Timing.Milli; - private eventGroupIdCallback: ((arg0: Types.Events.Event) => string)|null|undefined; override totalTime: number; + eventGroupIdCallback: ((arg0: Types.Events.Event) => string)|null|undefined; private calculateTransferSize?: boolean; + private forceGroupIdCallback?: boolean; constructor(events: Types.Events.Event[], { textFilter, @@ -335,6 +336,7 @@ export class BottomUpRootNode extends Node { endTime, eventGroupIdCallback, calculateTransferSize, + forceGroupIdCallback, }: { textFilter: TraceFilter, filters: readonly TraceFilter[], @@ -342,6 +344,17 @@ export class BottomUpRootNode extends Node { endTime: Types.Timing.Milli, eventGroupIdCallback?: ((arg0: Types.Events.Event) => string)|null, calculateTransferSize?: boolean, + /** + * This forces using `eventGroupIdCallback` in combination with generateEventID + * to generate the ID of the node. + * + * This is used in the ThirdPartyTreeView and BottomUpTreeView, where we want to group all events + * related to a specific 3P entity together, regardless of the specific event name/type. + * There are cases where events under the same event name belong to different entities. But, because + * they get grouped first by event name/type, it throws off the 3P groupBy - grouping events of different + * 3P entities together. + */ + forceGroupIdCallback?: boolean, }) { super('', events[0]); this.childrenInternal = null; @@ -353,6 +366,7 @@ export class BottomUpRootNode extends Node { this.eventGroupIdCallback = eventGroupIdCallback; this.totalTime = endTime - startTime; this.calculateTransferSize = calculateTransferSize; + this.forceGroupIdCallback = forceGroupIdCallback; } override hasChildren(): boolean { @@ -388,11 +402,17 @@ export class BottomUpRootNode extends Node { const selfTimeStack: number[] = [endTime - startTime]; const firstNodeStack: boolean[] = []; const totalTimeById = new Map(); + // TODO(paulirish): rename to getGroupNodeId + const eventGroupIdCallback = this.eventGroupIdCallback; + const forceGroupIdCallback = this.forceGroupIdCallback; // encodedDataLength is provided solely on instant events. const sumTransferSizeOfInstantEvent = (e: Types.Events.Event): void => { if (Types.Events.isReceivedDataEvent(e)) { - const id = generateEventID(e); + let id = generateEventID(e); + if (this.forceGroupIdCallback && this.eventGroupIdCallback) { + id = `${id}-${this.eventGroupIdCallback(e)}`; + } let node = nodeById.get(id); if (!node) { node = new BottomUpNode(root, id, e, false, root); @@ -424,7 +444,10 @@ export class BottomUpRootNode extends Node { const duration = actualEndTime - Math.max(currentStartTime, startTime); selfTimeStack[selfTimeStack.length - 1] -= duration; selfTimeStack.push(duration); - const id = generateEventID(e); + let id = generateEventID(e); + if (forceGroupIdCallback && eventGroupIdCallback) { + id = `${id}-${eventGroupIdCallback(e)}`; + } const noNodeOnStack = !totalTimeById.has(id); if (noNodeOnStack) { totalTimeById.set(id, duration); @@ -433,7 +456,10 @@ export class BottomUpRootNode extends Node { } function onEndEvent(event: Types.Events.Event): void { - const id = generateEventID(event); + let id = generateEventID(event); + if (forceGroupIdCallback && eventGroupIdCallback) { + id = `${id}-${eventGroupIdCallback(event)}`; + } let node = nodeById.get(id); if (!node) { node = new BottomUpNode(root, id, event, false, root); @@ -628,6 +654,7 @@ export function eventStackFrame(event: Types.Events.Event): Protocol.Runtime.Cal return {...topFrame, scriptId: String(topFrame.scriptId) as Protocol.Runtime.ScriptId}; } +// TODO(paulirish): rename to generateNodeId export function generateEventID(event: Types.Events.Event): string { if (Types.Events.isProfileCall(event)) { const name = SamplesIntegrator.isNativeRuntimeFrame(event.callFrame) ? diff --git a/front_end/models/trace/helpers/Trace.ts b/front_end/models/trace/helpers/Trace.ts index b0050c7a7f..bba166e34c 100644 --- a/front_end/models/trace/helpers/Trace.ts +++ b/front_end/models/trace/helpers/Trace.ts @@ -708,3 +708,116 @@ export function extractSampleTraceId(event: Types.Events.Event): number|null { } return event.args?.sampleTraceId ?? event.args?.data?.sampleTraceId ?? null; } + +export const VISIBLE_TRACE_EVENT_TYPES = new Set([ + Types.Events.Name.ABORT_POST_TASK_CALLBACK, + Types.Events.Name.ANIMATION, + Types.Events.Name.ASYNC_TASK, + Types.Events.Name.BACKGROUND_DESERIALIZE, + Types.Events.Name.BEGIN_FRAME, + Types.Events.Name.BEGIN_MAIN_THREAD_FRAME, + Types.Events.Name.CACHE_MODULE, + Types.Events.Name.CACHE_SCRIPT, + Types.Events.Name.CANCEL_ANIMATION_FRAME, + Types.Events.Name.CANCEL_IDLE_CALLBACK, + Types.Events.Name.COMPILE, + Types.Events.Name.COMPILE_CODE, + Types.Events.Name.COMPILE_MODULE, + Types.Events.Name.COMMIT, + Types.Events.Name.COMPOSITE_LAYERS, + Types.Events.Name.COMPUTE_INTERSECTION, + Types.Events.Name.CONSOLE_TIME, + Types.Events.Name.CONSOLE_TIME_STAMP, + Types.Events.Name.CPPGC_SWEEP, + Types.Events.Name.CRYPTO_DO_DECRYPT, + Types.Events.Name.CRYPTO_DO_DECRYPT_REPLY, + Types.Events.Name.CRYPTO_DO_DIGEST, + Types.Events.Name.CRYPTO_DO_DIGEST_REPLY, + Types.Events.Name.CRYPTO_DO_ENCRYPT, + Types.Events.Name.CRYPTO_DO_ENCRYPT_REPLY, + Types.Events.Name.CRYPTO_DO_SIGN, + Types.Events.Name.CRYPTO_DO_SIGN_REPLY, + Types.Events.Name.CRYPTO_DO_VERIFY, + Types.Events.Name.CRYPTO_DO_VERIFY_REPLY, + Types.Events.Name.DECODE_IMAGE, + Types.Events.Name.DRAW_FRAME, + Types.Events.Name.EMBEDDER_CALLBACK, + Types.Events.Name.EVALUATE_MODULE, + Types.Events.Name.EVALUATE_SCRIPT, + Types.Events.Name.EVENT_DISPATCH, + Types.Events.Name.EVENT_TIMING, + Types.Events.Name.FINALIZE_DESERIALIZATION, + Types.Events.Name.FIRE_ANIMATION_FRAME, + Types.Events.Name.FIRE_IDLE_CALLBACK, + Types.Events.Name.FRAME_STARTED_LOADING, + Types.Events.Name.FUNCTION_CALL, + Types.Events.Name.GC, + Types.Events.Name.GC_COLLECT_GARBARGE, + Types.Events.Name.GPU_TASK, + Types.Events.Name.HANDLE_POST_MESSAGE, + Types.Events.Name.HIT_TEST, + Types.Events.Name.INVALIDATE_LAYOUT, + Types.Events.Name.JS_SAMPLE, + Types.Events.Name.LAYERIZE, + Types.Events.Name.LAYOUT, + Types.Events.Name.LAYOUT_SHIFT, + Types.Events.Name.MAJOR_GC, + Types.Events.Name.MARK_DOM_CONTENT, + Types.Events.Name.MARK_FIRST_PAINT, + Types.Events.Name.MARK_FCP, + Types.Events.Name.MARK_LCP_CANDIDATE, + Types.Events.Name.MARK_LOAD, + Types.Events.Name.MINOR_GC, + Types.Events.Name.OPTIMIZE_CODE, + Types.Events.Name.PAINT, + Types.Events.Name.PAINT_IMAGE, + Types.Events.Name.PAINT_SETUP, + Types.Events.Name.PARSE_AUTHOR_STYLE_SHEET, + Types.Events.Name.PARSE_HTML, + Types.Events.Name.PRE_PAINT, + Types.Events.Name.PROFILE_CALL, + Types.Events.Name.PROGRAM, + Types.Events.Name.RASTER_TASK, + Types.Events.Name.REQUEST_ANIMATION_FRAME, + Types.Events.Name.REQUEST_IDLE_CALLBACK, + Types.Events.Name.REQUEST_MAIN_THREAD_FRAME, + Types.Events.Name.RESOURCE_FINISH, + Types.Events.Name.RESOURCE_RECEIVE_DATA, + Types.Events.Name.RESOURCE_RECEIVE_RESPONSE, + Types.Events.Name.RESOURCE_SEND_REQUEST, + Types.Events.Name.RESOURCE_WILL_SEND_REQUEST, + Types.Events.Name.RUN_MICROTASKS, + Types.Events.Name.RUN_POST_TASK_CALLBACK, + Types.Events.Name.RUN_TASK, + Types.Events.Name.SCHEDULE_POST_MESSAGE, + Types.Events.Name.SCHEDULE_POST_TASK_CALLBACK, + Types.Events.Name.SCHEDULE_STYLE_RECALCULATION, + Types.Events.Name.SCROLL_LAYER, + Types.Events.Name.START_PROFILING, + Types.Events.Name.STREAMING_COMPILE_SCRIPT, + Types.Events.Name.STREAMING_COMPILE_SCRIPT_PARSING, + Types.Events.Name.STREAMING_COMPILE_SCRIPT_WAITING, + Types.Events.Name.SYNTHETIC_LAYOUT_SHIFT, + Types.Events.Name.SYNTHETIC_LAYOUT_SHIFT_CLUSTER, + Types.Events.Name.TIMER_FIRE, + Types.Events.Name.TIMER_INSTALL, + Types.Events.Name.TIMER_REMOVE, + Types.Events.Name.UPDATE_LAYER, + Types.Events.Name.UPDATE_LAYER_TREE, + Types.Events.Name.UPDATE_LAYOUT_TREE, + Types.Events.Name.USER_TIMING, + Types.Events.Name.V8_CONSOLE_RUN_TASK, + Types.Events.Name.WASM_CACHED_MODULE, + Types.Events.Name.WASM_COMPILED_MODULE, + Types.Events.Name.WASM_MODULE_CACHE_HIT, + Types.Events.Name.WASM_MODULE_CACHE_INVALID, + Types.Events.Name.WASM_STREAM_FROM_RESPONSE_CALLBACK, + Types.Events.Name.WEB_SOCKET_CREATE, + Types.Events.Name.WEB_SOCKET_DESTROY, + Types.Events.Name.WEB_SOCKET_RECEIVE, + Types.Events.Name.WEB_SOCKET_RECEIVE_HANDSHAKE_REQUEST, + Types.Events.Name.WEB_SOCKET_SEND, + Types.Events.Name.WEB_SOCKET_SEND_HANDSHAKE_REQUEST, + Types.Events.Name.XHR_LOAD, + Types.Events.Name.XHR_READY_STATE_CHANGED, +]); diff --git a/front_end/models/trace/insights/ThirdParties.test.ts b/front_end/models/trace/insights/ThirdParties.test.ts index f3ca7e4836..0c57e7e67f 100644 --- a/front_end/models/trace/insights/ThirdParties.test.ts +++ b/front_end/models/trace/insights/ThirdParties.test.ts @@ -4,7 +4,6 @@ import {describeWithEnvironment} from '../../../testing/EnvironmentHelpers.js'; import {getFirstOrError, getInsightOrError, processTrace} from '../../../testing/InsightHelpers.js'; -import * as Trace from '../../trace/trace.js'; describeWithEnvironment('ThirdParties', function() { it('categorizes third party web requests (simple)', async () => { @@ -13,52 +12,16 @@ describeWithEnvironment('ThirdParties', function() { const insight = getInsightOrError('ThirdParties', insights, getFirstOrError(data.Meta.navigationsByNavigationId.values())); - const entityNames = [...insight.summaryByEntity.keys()].map(entity => entity.name).sort(); + const entityNames = insight.summaries.map(s => s.entity.name); + assert.deepEqual([...new Set(entityNames)], [ - 'Google Fonts', 'localhost', + 'Google Fonts', ]); - - const results = [...insight.eventsByEntity.entries()].map(([entity, events]) => { - const requests = events.filter(event => Trace.Types.Events.isSyntheticNetworkRequest(event)); - return [ - entity.name, - [ - `Total events: ${events.length}`, - ...requests.map(request => request.args.data.url).sort(), - ] - ]; - }); - assert.deepEqual( - results, - [ - [ - 'localhost', - [ - 'Total events: 17', - 'http://localhost:8080/', - 'http://localhost:8080/blocking.js', - 'http://localhost:8080/module.js', - 'http://localhost:8080/styles.css', - ], - ], - [ - 'Google Fonts', - [ - 'Total events: 2', - 'https://fonts.googleapis.com/css2?family=Orelega+One&display=swap', - 'https://fonts.gstatic.com/s/orelegaone/v1/3qTpojOggD2XtAdFb-QXZFt93kY.woff2', - ], - ], - ], - ); - - const summaryResult = [...insight.summaryByEntity.entries()].map(([entity, summary]) => { - return [entity.name, summary]; - }); + const summaryResult = insight.summaries.map(s => [s.entity.name, s.transferSize, s.mainThreadTime.toFixed(2)]); assert.deepEqual(summaryResult, [ - ['localhost', {transferSize: 751, mainThreadTime: 26381}], - ['Google Fonts', {transferSize: 0, mainThreadTime: 0}], + ['localhost', 2254, '24.95'], + ['Google Fonts', 25325, '0.00'], ]); }); @@ -68,102 +31,24 @@ describeWithEnvironment('ThirdParties', function() { const insight = getInsightOrError('ThirdParties', insights, getFirstOrError(data.Meta.navigationsByNavigationId.values())); - const entityNames = [...insight.summaryByEntity.keys()].map(entity => entity.name).sort(); + const entityNames = insight.summaries.map(s => s.entity.name); assert.deepEqual([...new Set(entityNames)], [ - 'Disqus', - 'Firebase', - 'Google Analytics', + 'paulirish.com', 'Google Fonts', 'Google Tag Manager', - 'paulirish.com', + 'Google Analytics', + 'Disqus', + 'Firebase', ]); - const results = [...insight.eventsByEntity.entries()].map(([entity, events]) => { - const requests = events.filter(event => Trace.Types.Events.isSyntheticNetworkRequest(event)); - return [ - entity.name, - [ - `Total events: ${events.length}`, - ...requests.map(request => request.args.data.url).sort(), - ] - ]; - }); - assert.deepEqual( - results, - [ - [ - 'paulirish.com', - [ - 'Total events: 1460', - 'https://www.paulirish.com/', - 'https://www.paulirish.com/assets/wikipedia-flamechart.jpg', - 'https://www.paulirish.com/avatar150.jpg', - 'https://www.paulirish.com/favicon.ico', - 'https://www.paulirish.com/images/code_bg.png?1418840251', - 'https://www.paulirish.com/images/noise.png?1418840251', - 'https://www.paulirish.com/javascripts/ender.js', - 'https://www.paulirish.com/javascripts/firebase-performance-standalone.js', - 'https://www.paulirish.com/javascripts/modernizr-2.0.js', - 'https://www.paulirish.com/javascripts/octopress.js', - ], - ], - [ - 'Google Tag Manager', - [ - 'Total events: 25', - 'https://www.googletagmanager.com/gtag/js?id=G-PGXNGYWP8E', - ], - ], - [ - 'Disqus', - [ - 'Total events: 3', - 'https://paulirish.disqus.com/count.js', - ], - ], - [ - 'Google Analytics', - [ - 'Total events: 13', - 'https://www.google-analytics.com/analytics.js', - 'https://www.google-analytics.com/g/collect?v=2&tid=G-PGXNGYWP8E>m=45je4580v880158425za200&_p=1715625261583&gcd=13l3l3l3l1&npa=0&dma=0&cid=414801335.1715625262&ul=en-us&sr=412x823&uaa=&uab=64&uafvl=Not%252FA)Brand%3B8.0.0.0%7CChromium%3B126.0.6475.0%7CGoogle%2520Chrome%3B126.0.6475.0&uamb=1&uam=moto%20g%20power%20(2022)&uap=Android&uapv=11.0&uaw=0&are=1&frm=0&pscdl=noapi&_s=1&sid=1715625261&sct=1&seg=0&dl=https%3A%2F%2Fwww.paulirish.com%2F&dt=Paul%20Irish&en=page_view&_fv=1&_nsi=1&_ss=1&_ee=1&tfd=353', - 'https://www.google-analytics.com/j/collect?v=1&_v=j101&a=272264939&t=pageview&_s=1&dl=https%3A%2F%2Fwww.paulirish.com%2F&ul=en-us&de=UTF-8&dt=Paul%20Irish&sd=30-bit&sr=412x823&vp=412x823&je=0&_u=IADAAEABAAAAACAAI~&jid=1388679807&gjid=654531532&cid=414801335.1715625262&tid=UA-692547-2&_gid=1964734610.1715625262&_r=1&_slc=1&z=1746264594', - ], - ], - [ - 'Google Fonts', - [ - 'Total events: 7', - 'https://fonts.googleapis.com/css?family=PT+Serif:regular,italic,bold|PT+Sans:regular,italic,bold|Droid+Sans:400,700|Lato:700,900', - 'https://fonts.gstatic.com/s/droidsans/v18/SlGVmQWMvZQIdix7AFxXkHNSbRYXags.woff2', - 'https://fonts.gstatic.com/s/droidsans/v18/SlGWmQWMvZQIdix7AFxXmMh3eDs1ZyHKpWg.woff2', - 'https://fonts.gstatic.com/s/lato/v24/S6u9w4BMUTPHh6UVSwiPGQ3q5d0.woff2', - 'https://fonts.gstatic.com/s/ptsans/v17/jizaRExUiTo99u79D0KExcOPIDU.woff2', - 'https://fonts.gstatic.com/s/ptsans/v17/jizfRExUiTo99u79B_mh0O6tLR8a8zI.woff2', - 'https://fonts.gstatic.com/s/ptserif/v18/EJRVQgYoZZY2vCFuvAFWzr-_dSb_.woff2', - ], - ], - [ - 'Firebase', - [ - 'Total events: 2', - 'https://firebaseinstallations.googleapis.com/v1/projects/paulirishcom/installations', - 'https://firebaseremoteconfig.googleapis.com/v1/projects/paulirishcom/namespaces/fireperf:fetch?key=AIzaSyCGxLbbFQxH4BV1fY0RODlxTos9nJa2l_g', - ], - ], - ], - ); - - const summaryResult = [...insight.summaryByEntity.entries()].map(([entity, summary]) => { - return [entity.name, summary]; - }); + const summaryResult = insight.summaries.map(s => [s.entity.name, s.transferSize, s.mainThreadTime.toFixed(2)]); assert.deepEqual(summaryResult, [ - ['paulirish.com', {transferSize: 157130, mainThreadTime: 6626}], - ['Google Tag Manager', {transferSize: 95375, mainThreadTime: 83}], - ['Disqus', {transferSize: 1551, mainThreadTime: 23}], - ['Google Analytics', {transferSize: 20865, mainThreadTime: 97}], - ['Google Fonts', {transferSize: 80003, mainThreadTime: 0}], - ['Firebase', {transferSize: 2847, mainThreadTime: 0}], + ['paulirish.com', 439223, '85.54'], + ['Google Fonts', 169258, '0.00'], + ['Google Tag Manager', 367917, '19.95'], + ['Google Analytics', 75811, '5.86'], + ['Disqus', 3748, '0.34'], + ['Firebase', 6564, '0.00'], ]); }); }); diff --git a/front_end/models/trace/insights/ThirdParties.ts b/front_end/models/trace/insights/ThirdParties.ts index 55ae87b982..e9699cda73 100644 --- a/front_end/models/trace/insights/ThirdParties.ts +++ b/front_end/models/trace/insights/ThirdParties.ts @@ -6,7 +6,6 @@ import * as i18n from '../../../core/i18n/i18n.js'; import * as ThirdPartyWeb from '../../../third_party/third-party-web/third-party-web.js'; import * as Extras from '../extras/extras.js'; import * as Handlers from '../handlers/handlers.js'; -import * as Helpers from '../helpers/helpers.js'; import type * as Types from '../types/types.js'; import { @@ -42,21 +41,17 @@ const str_ = i18n.i18n.registerUIStrings('models/trace/insights/ThirdParties.ts' export const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); export type ThirdPartiesInsightModel = InsightModel, - summaryByEntity: Map, - summaryByUrl: Map, - urlsByEntity: Map>, /** The entity for this navigation's URL. Any other entity is from a third party. */ - firstPartyEntity?: Extras.ThirdParties.Entity, + firstPartyEntity?: Extras.ThirdParties.Entity, summaries: Extras.ThirdParties.Summary[], }>; function getRelatedEvents( - summaries: Extras.ThirdParties.ThirdPartySummary, + summaries: Extras.ThirdParties.Summary[], firstPartyEntity: Extras.ThirdParties.Entity|undefined): Types.Events.Event[] { const relatedEvents = []; - - for (const [entity, events] of summaries.eventsByEntity.entries()) { - if (entity !== firstPartyEntity) { + for (const summary of summaries) { + if (summary.entity !== firstPartyEntity) { + const events = summary.relatedEvents ?? []; relatedEvents.push(...events); } } @@ -71,40 +66,24 @@ function finalize(partialModel: PartialInsightModel): title: i18nString(UIStrings.title), description: i18nString(UIStrings.description), category: InsightCategory.ALL, - state: [...partialModel.summaryByEntity.entries()].find(kv => kv[0] !== partialModel.firstPartyEntity) ? - 'informative' : - 'pass', + state: partialModel.summaries.find(summary => summary.entity !== partialModel.firstPartyEntity) ? 'informative' : + 'pass', ...partialModel, }; } export function generateInsight( parsedTrace: Handlers.Types.ParsedTrace, context: InsightSetContext): ThirdPartiesInsightModel { - const networkRequests = parsedTrace.NetworkRequests.byTime.filter(event => { - if (!context.navigation) { - return false; - } - - if (event.args.data.frame !== context.frameId) { - return false; - } - - return Helpers.Timing.eventIsInBounds(event, context.bounds); - }); - - const thirdPartySummary = Extras.ThirdParties.summarizeThirdParties( - parsedTrace as Handlers.Types.ParsedTrace, context.bounds, networkRequests); + const summaries = + Extras.ThirdParties.summarizeThirdParties(parsedTrace as Handlers.Types.ParsedTrace, context.bounds); const firstPartyUrl = context.navigation?.args.data?.documentLoaderURL ?? parsedTrace.Meta.mainFrameURL; const firstPartyEntity = ThirdPartyWeb.ThirdPartyWeb.getEntity(firstPartyUrl) || - Handlers.Helpers.makeUpEntity(thirdPartySummary.madeUpEntityCache, firstPartyUrl); + Handlers.Helpers.makeUpEntity(parsedTrace.Renderer.entityMappings.createdEntityCache, firstPartyUrl); return finalize({ - relatedEvents: getRelatedEvents(thirdPartySummary, firstPartyEntity), - eventsByEntity: thirdPartySummary.eventsByEntity, - summaryByEntity: thirdPartySummary.byEntity, - summaryByUrl: thirdPartySummary.byUrl, - urlsByEntity: thirdPartySummary.urlsByEntity, + relatedEvents: getRelatedEvents(summaries, firstPartyEntity), firstPartyEntity, + summaries, }); } diff --git a/front_end/panels/timeline/README.md b/front_end/panels/timeline/README.md index fa9c5ffadf..d42a77c5cc 100644 --- a/front_end/panels/timeline/README.md +++ b/front_end/panels/timeline/README.md @@ -67,10 +67,11 @@ Because this component is used across multiple panels, it does not know about th The timeline minimap (which is only used in the Performance Panel) listens to the two OverviewPane events emitted. When an `OverviewPaneBreadcrumbAdded` event is emitted it will: + 1. Create a new breadcrumb and update the Breadcrumbs component. 2. Update the `TraceBounds` service, updating it with: - 1. `minimapBounds` which are set to the bounds of the breadcrumb - 2. `timelineVisibleWindow` which are set to the bounds of the breadcrumb + 1. `minimapBounds` which are set to the bounds of the breadcrumb + 2. `timelineVisibleWindow` which are set to the bounds of the breadcrumb If a breadcrumb is removed (which is handled via a `RemoveBreadcrumb` event dispatched by the Breadcrumbs UI component), the minimap does the exact same as above. @@ -99,7 +100,7 @@ This is used in: Serializing/Deserializing events allows for the creation of data (such as annotations) associated with events and enabling to save them to/load them from the trace file. Several classes handle the serialization and application of these serialized annotations: 1. `SyntheticEventsManager` - stores all synthetic events based on a raw event. They are stored in an array indexed by the position the corresponding raw events have in the `Model::rawEvents` array. The `SyntheticEventsManager` needs to be called by handlers as synthetic events are created. To enforce this we make use of a branded type called `SyntheticEntry`, which the `SyntheticEventsManager` adds to trace-event-like objects. -Having a single place where all synthetic events are stored allows us to easily map from a synthetic event key back to the event object. + Having a single place where all synthetic events are stored allows us to easily map from a synthetic event key back to the event object. 2. `EventsSerializer` - is responsible for event serialization. It generates the string key saved into the trace file and maps the key back to the corresponding Event (after reading keys from the trace file). To perform this mapping, it retrieves the raw event array registered by `SyntheticEventsManager` at the id extracted from the key. For profile calls, a binary search is conducted on the complete profile call list to efficiently find a match based on the sample index and node id retrieved from the string key. @@ -176,3 +177,38 @@ If you ever need to know how high the network canvas is, use `networkChartOffset | | +-------------------------------------------+ ``` + +## Timeline tree views + +The `TimelineTreeView` base class provides the foundation for creating various tree-based views within the Performance panel (e.g., Summary, Bottom-Up, Call Tree, Event Log). It handles core functionality like: + +- Data grid creation +- Filtering +- Hover actions +- Toolbar management +- Event handling + +The data grid is the core UI element, with each `GridNode` representing a row containing a name and associated values. + +### Tree Data Sources + +The Summary (ThirdParty), Bottom-Up, Call Tree, and Event Log views primarily use `this.selectedEvents()` as their data source. This method returns the events currently selected and in view by the user in the main timeline view. For example, if a user clicks on a track other than the Main track, `this.selectedEvents()` will represent that. + +**Important Considerations:** + +- **Lazily built:** trees are lazily built - child nodes are not created until + they are needed. In most cases, trees are fully built when `.children()` is called from `refreshTree()` +- **Single Track Focus:** `this.selectedEvents()` only captures events from a _single_ track at a time. Selecting the main track will not include what one would consider +"relevant events" from other tracks (e.g. a Frame's track). +- **No Synthetic Network Events:** Tree views do not consume `SyntheticNetworkEvents` due to their unique "overlapping" behavior, which differs from standard trace events. +- **Filters:** Filters can be applied to determine which events are included when building the tree. + +### Event aggregation + +`AggregatedTimelineTreeView` allows grouping similar events into single nodes. The `TraceTree.ts` module handles this aggregation. + +**Aggregation Logic for BottomUp tree views:** + +1. **Default Aggregation:** By default, aggregation is determined by the `generateEventID()` function, and optionally by `eventGroupIdCallback`. +2. **Pre-Grouping (`ungrouppedTopNodes`)**: Before explicit grouping, `ungrouppedTopNodes()` organizes events into a `ChildrenCache` map (``). Even without explicit `GroupBy` grouping, `ungrouppedTopNodes()` aggregates nodes by event name using `generateEventID()`. +3. **Third Party Grouping (`forceGroupIdCallback`)**: In `ThirdPartyTreeView`, `forceGroupIdCallback` is used to ensure that `eventGroupIdCallback` is used to generate the node ID. This is crucial because events with the same name can belong to different third parties. Without this, the initial aggregation by event name would lead to incorrect third-party grouping. diff --git a/front_end/panels/timeline/ThirdPartyTreeView.ts b/front_end/panels/timeline/ThirdPartyTreeView.ts index c6edfed0ce..06fe052456 100644 --- a/front_end/panels/timeline/ThirdPartyTreeView.ts +++ b/front_end/panels/timeline/ThirdPartyTreeView.ts @@ -87,6 +87,8 @@ export class ThirdPartyTreeViewWidget extends TimelineTreeView.TimelineTreeView endTime: this.endTime, eventGroupIdCallback: this.groupingFunction.bind(this), calculateTransferSize: true, + // Ensure we group by 3P alongside eventID for correct 3P grouping. + forceGroupIdCallback: true, }); return node; } diff --git a/front_end/panels/timeline/TimelineTreeView.test.ts b/front_end/panels/timeline/TimelineTreeView.test.ts index 4e1cc3a6ad..059ab3c356 100644 --- a/front_end/panels/timeline/TimelineTreeView.test.ts +++ b/front_end/panels/timeline/TimelineTreeView.test.ts @@ -264,6 +264,7 @@ describeWithEnvironment('TimelineTreeView', function() { assert.deepEqual(topLevelGroupNodes.map(node => node[0]), [ '25D2F12F1818C70B5BD4325CC9ACD8FF', '1094B71EC09B8BD3DD48B77D091D6024', + '75599398D66E8FE7AAD92D418D92FCE1', ]); }); diff --git a/front_end/panels/timeline/TimelineTreeView.ts b/front_end/panels/timeline/TimelineTreeView.ts index bbe4fdf0cc..b538b3fba5 100644 --- a/front_end/panels/timeline/TimelineTreeView.ts +++ b/front_end/panels/timeline/TimelineTreeView.ts @@ -1078,6 +1078,9 @@ export class AggregatedTimelineTreeView extends TimelineTreeView { } } + // This is our groupingFunction that returns the eventId in Domain, Subdomain, and ThirdParty groupBy scenarios. + // The eventid == the identity of a node that we expect in a bottomUp tree (either without grouping or with the groupBy grouping) + // A "top node" (in `ungrouppedTopNodes`) is aggregated by this. (But so are all the other nodes, except the `GroupNode`s) private domainByEvent(groupBy: AggregatedTimelineTreeView.GroupBy, event: Trace.Types.Events.Event): string { const parsedTrace = this.parsedTrace(); if (!parsedTrace) { @@ -1085,6 +1088,21 @@ export class AggregatedTimelineTreeView extends TimelineTreeView { } const url = Trace.Handlers.Helpers.getNonResolvedURL(event, parsedTrace); if (!url) { + // We could have receiveDataEvents (that don't have a url), but that have been + // attributed to an entity, let's check for these. This is used for ThirdParty grouping. + const entity = this.entityMapper()?.entityForEvent(event); + if (groupBy === AggregatedTimelineTreeView.GroupBy.ThirdParties && entity) { + if (!entity) { + return ''; + } + const firstDomain = entity.domains[0]; + const parsedURL = Common.ParsedURL.ParsedURL.fromString(firstDomain); + // chrome-extension check must come before entity.name. + if (parsedURL?.scheme === 'chrome-extension') { + return `${parsedURL.scheme}://${parsedURL.host}`; + } + return entity.name; + } return ''; } if (AggregatedTimelineTreeView.isExtensionInternalURL(url)) { @@ -1172,6 +1190,11 @@ export class BottomUpTimelineTreeView extends AggregatedTimelineTreeView { startTime: this.startTime, endTime: this.endTime, eventGroupIdCallback: this.groupingFunction(this.groupBySetting.get()), + // To include instant events. When this is set to true, instant events are + // considered (to calculate transfer size). This then includes these events in tree nodes. + calculateTransferSize: true, + // We should forceGroupIdCallback if filtering by 3P for correct 3P grouping. + forceGroupIdCallback: this.groupBySetting.get() === AggregatedTimelineTreeView.GroupBy.ThirdParties, }); } } diff --git a/front_end/panels/timeline/components/insights/ThirdParties.ts b/front_end/panels/timeline/components/insights/ThirdParties.ts index 7011dd16aa..fe96549c36 100644 --- a/front_end/panels/timeline/components/insights/ThirdParties.ts +++ b/front_end/panels/timeline/components/insights/ThirdParties.ts @@ -21,68 +21,65 @@ export class ThirdParties extends BaseInsightComponent static override readonly litTagName = Lit.StaticHtml.literal`devtools-performance-third-parties`; override internalName = 'third-parties'; - #overlaysForEntity = new Map(); - override createOverlays(): Overlays.Overlays.TimelineOverlay[] { - this.#overlaysForEntity.clear(); - if (!this.model) { return []; } const overlays: Overlays.Overlays.TimelineOverlay[] = []; - for (const [entity, events] of this.model.eventsByEntity) { - if (entity === this.model.firstPartyEntity) { + const summaries = this.model.summaries ?? []; + for (const summary of summaries) { + if (summary.entity === this.model.firstPartyEntity) { continue; } - const overlaysForThisEntity = []; - for (const event of events) { - const overlay: Overlays.Overlays.TimelineOverlay = { - type: 'ENTRY_OUTLINE', - entry: event, - outlineReason: 'INFO', - }; - overlaysForThisEntity.push(overlay); - overlays.push(overlay); - } - - this.#overlaysForEntity.set(entity, overlaysForThisEntity); + const summaryOverlays = this.#createOverlaysForSummary(summary); + overlays.push(...summaryOverlays); } - return overlays; } - #mainThreadTimeAggregator: - RowLimitAggregator<[Trace.Extras.ThirdParties.Entity, Trace.Extras.ThirdParties.Summary]> = { - mapToRow: ([entity, summary]) => ({ - values: [entity.name, i18n.TimeUtilities.formatMicroSecondsTime(summary.mainThreadTime)], - overlays: this.#overlaysForEntity.get(entity), - }), - createAggregatedTableRow: - remaining => { - const totalMainThreadTime = - remaining.reduce((acc, [, summary]) => acc + summary.mainThreadTime, 0) as Trace.Types.Timing.Micro; - return { - values: [ - renderOthersLabel(remaining.length), i18n.TimeUtilities.formatMicroSecondsTime(totalMainThreadTime) - ], - overlays: remaining.flatMap(([entity]) => this.#overlaysForEntity.get(entity) ?? []), - }; - }, + #createOverlaysForSummary(summary: Trace.Extras.ThirdParties.Summary): Overlays.Overlays.TimelineOverlay[] { + const overlays = []; + const events = summary.relatedEvents ?? []; + for (const event of events) { + const overlay: Overlays.Overlays.TimelineOverlay = { + type: 'ENTRY_OUTLINE', + entry: event, + outlineReason: 'INFO', }; - - #transferSizeAggregator: RowLimitAggregator<[Trace.Extras.ThirdParties.Entity, Trace.Extras.ThirdParties.Summary]> = { - mapToRow: ([entity, summary]) => ({ - values: [entity.name, i18n.ByteUtilities.bytesToString(summary.transferSize)], - overlays: this.#overlaysForEntity.get(entity), + overlays.push(overlay); + } + return overlays; + } + #mainThreadTimeAggregator: RowLimitAggregator = { + mapToRow: summary => ({ + values: [summary.entity.name, i18n.TimeUtilities.millisToString(summary.mainThreadTime)], + overlays: this.#createOverlaysForSummary(summary), }), createAggregatedTableRow: remaining => { - const totalBytes = remaining.reduce((acc, [, summary]) => acc + summary.transferSize, 0); + const totalMainThreadTime = + remaining.reduce((acc, summary) => acc + summary.mainThreadTime, 0) as Trace.Types.Timing.Micro; + return { + values: + [renderOthersLabel(remaining.length), i18n.TimeUtilities.formatMicroSecondsTime(totalMainThreadTime)], + overlays: remaining.flatMap(summary => this.#createOverlaysForSummary(summary) ?? []), + }; + }, + }; + + #transferSizeAggregator: RowLimitAggregator = { + mapToRow: summary => ({ + values: [summary.entity.name, i18n.ByteUtilities.bytesToString(summary.transferSize)], + overlays: this.#createOverlaysForSummary(summary), + }), + createAggregatedTableRow: + remaining => { + const totalBytes = remaining.reduce((acc, summary) => acc + summary.transferSize, 0); return { values: [renderOthersLabel(remaining.length), i18n.ByteUtilities.bytesToString(totalBytes)], - overlays: remaining.flatMap(([entity]) => this.#overlaysForEntity.get(entity) ?? []), + overlays: remaining.flatMap(summary => this.#createOverlaysForSummary(summary) ?? []), }; }, }; @@ -92,19 +89,21 @@ export class ThirdParties extends BaseInsightComponent return Lit.nothing; } - const entries = [...this.model.summaryByEntity.entries()].filter(kv => kv[0] !== this.model?.firstPartyEntity); - if (!entries.length) { + let result = this.model.summaries ?? []; + + if (this.model.firstPartyEntity) { + result = result.filter(s => s.entity !== this.model?.firstPartyEntity || null); + } + if (!result.length) { return html`
${i18nString(UIStrings.noThirdParties)}
`; } - const topTransferSizeEntries = entries.toSorted((a, b) => b[1].transferSize - a[1].transferSize); - const topMainThreadTimeEntries = entries.toSorted((a, b) => b[1].mainThreadTime - a[1].mainThreadTime); + const topTransferSizeEntries = result.toSorted((a, b) => b.transferSize - a.transferSize); + const topMainThreadTimeEntries = result.toSorted((a, b) => b.mainThreadTime - a.mainThreadTime); const sections = []; - if (topTransferSizeEntries.length) { const rows = createLimitedRows(topTransferSizeEntries, this.#transferSizeAggregator, MAX_TO_SHOW); - // clang-format off sections.push(html`
@@ -122,7 +121,6 @@ export class ThirdParties extends BaseInsightComponent if (topMainThreadTimeEntries.length) { const rows = createLimitedRows(topMainThreadTimeEntries, this.#mainThreadTimeAggregator, MAX_TO_SHOW); - // clang-format off sections.push(html`
diff --git a/front_end/panels/timeline/utils/EntryStyles.ts b/front_end/panels/timeline/utils/EntryStyles.ts index 67ade16719..df80e11d4f 100644 --- a/front_end/panels/timeline/utils/EntryStyles.ts +++ b/front_end/panels/timeline/utils/EntryStyles.ts @@ -1076,6 +1076,20 @@ export function maybeInitSylesMap(): EventStylesMap { [Trace.Types.Events.Name.V8_CONSOLE_RUN_TASK]: new TimelineRecordStyle(i18nString(UIStrings.consoleTaskRun), defaultCategoryStyles.scripting), }; + + // TODO: remove assertion after deduped eventStylesMap for VISIBLE_TRACE_EVENT_TYPES. + const visibleTraceEventsComplete = (Object.keys(eventStylesMap)).every(eventType => { + return Trace.Helpers.Trace.VISIBLE_TRACE_EVENT_TYPES.has(eventType as Trace.Types.Events.Name); + }); + + const eventStylesMapKeys = Object.keys(eventStylesMap) as Trace.Types.Events.Name[]; + const eventStylesComplete = Array.from(Trace.Helpers.Trace.VISIBLE_TRACE_EVENT_TYPES).every(eventType => { + return eventStylesMapKeys.includes(eventType); + }); + + if (!visibleTraceEventsComplete || !eventStylesComplete) { + throw new Error('eventStylesMap and VISIBLE_TRACE_EVENT_TYPES are out of sync!'); + } return eventStylesMap; } diff --git a/test/interactions/goldens/linux/performance/insights-lcp-request-discovery.png b/test/interactions/goldens/linux/performance/insights-lcp-request-discovery.png index 2b295ee26c..6f5d9cc760 100644 Binary files a/test/interactions/goldens/linux/performance/insights-lcp-request-discovery.png and b/test/interactions/goldens/linux/performance/insights-lcp-request-discovery.png differ diff --git a/test/interactions/goldens/linux/performance/main-thread-long-task-candy-stripe.png b/test/interactions/goldens/linux/performance/main-thread-long-task-candy-stripe.png index 49102210c0..ee27a4093d 100644 Binary files a/test/interactions/goldens/linux/performance/main-thread-long-task-candy-stripe.png and b/test/interactions/goldens/linux/performance/main-thread-long-task-candy-stripe.png differ diff --git a/test/interactions/goldens/linux/performance/timeline-long-task-candystripe.png b/test/interactions/goldens/linux/performance/timeline-long-task-candystripe.png index 49102210c0..ee27a4093d 100644 Binary files a/test/interactions/goldens/linux/performance/timeline-long-task-candystripe.png and b/test/interactions/goldens/linux/performance/timeline-long-task-candystripe.png differ diff --git a/test/interactions/goldens/linux/performance/timeline-web-dev-screenshot-frames.png b/test/interactions/goldens/linux/performance/timeline-web-dev-screenshot-frames.png index 602b9c467d..3a339a2a70 100644 Binary files a/test/interactions/goldens/linux/performance/timeline-web-dev-screenshot-frames.png and b/test/interactions/goldens/linux/performance/timeline-web-dev-screenshot-frames.png differ