diff --git a/front_end/models/trace/handlers/UserTimingsHandler.ts b/front_end/models/trace/handlers/UserTimingsHandler.ts index 588b99d2c38..8a0f6e7aa73 100644 --- a/front_end/models/trace/handlers/UserTimingsHandler.ts +++ b/front_end/models/trace/handlers/UserTimingsHandler.ts @@ -15,6 +15,7 @@ import {HandlerState} from './types.js'; let syntheticEvents: Types.Events.SyntheticEventPair[] = []; const performanceMeasureEvents: Types.Events.PerformanceMeasure[] = []; const performanceMarkEvents: Types.Events.PerformanceMark[] = []; +const performanceAttributionEvents: Types.Events.PerformanceAttribution[] = []; const consoleTimings: (Types.Events.ConsoleTimeBegin|Types.Events.ConsoleTimeEnd)[] = []; @@ -42,6 +43,12 @@ export interface UserTimingsData { * https://developer.mozilla.org/en-US/docs/Web/API/console/timeStamp */ timestampEvents: readonly Types.Events.TimeStamp[]; + + /** + * Attribution events triggered with the performance.mark() API + * https://developer.mozilla.org/en-US/docs/Web/API/Performance/attribution + */ + performanceAttributions: readonly Types.Events.PerformanceAttribution[]; } let handlerState = HandlerState.UNINITIALIZED; @@ -49,6 +56,7 @@ export function reset(): void { syntheticEvents.length = 0; performanceMeasureEvents.length = 0; performanceMarkEvents.length = 0; + performanceAttributionEvents.length = 0; consoleTimings.length = 0; timestampEvents.length = 0; handlerState = HandlerState.INITIALIZED; @@ -157,6 +165,10 @@ export function handleEvent(event: Types.Events.Event): void { } if (Types.Events.isPerformanceMark(event)) { performanceMarkEvents.push(event); + + if (Types.Events.isPerformanceAttribution(event)) { + performanceAttributionEvents.push(event); + } } if (Types.Events.isConsoleTime(event)) { consoleTimings.push(event); @@ -189,5 +201,6 @@ export function data(): UserTimingsData { // TODO(crbug/41484172): UserTimingsHandler.test.ts fails if this is not copied. performanceMarks: [...performanceMarkEvents], timestampEvents: [...timestampEvents], + performanceAttributions: [...performanceAttributionEvents], }; } diff --git a/front_end/models/trace/types/TraceEvents.ts b/front_end/models/trace/types/TraceEvents.ts index 8ef45783807..babada28082 100644 --- a/front_end/models/trace/types/TraceEvents.ts +++ b/front_end/models/trace/types/TraceEvents.ts @@ -95,6 +95,7 @@ export interface ArgsData { url?: string; navigationId?: string; frame?: string; + attribution?: string; } export interface CallFrame { @@ -1284,6 +1285,14 @@ export interface PerformanceMark extends UserTiming { ph: Phase.INSTANT|Phase.MARK|Phase.ASYNC_NESTABLE_INSTANT; } +export interface PerformanceAttribution extends UserTiming { + args: Args&{ + data: ArgsData & { + detail: string, + }, + }; +} + export interface ConsoleTimeBegin extends PairableAsyncBegin { cat: 'blink.console'; } @@ -2132,6 +2141,10 @@ export function isPerformanceMark(event: Event): event is PerformanceMark { return isUserTiming(event) && (event.ph === Phase.MARK || event.ph === Phase.INSTANT); } +export function isPerformanceAttribution(event: Event): event is PerformanceAttribution { + return event.name.startsWith('attribution::'); +} + export function isConsoleTime(event: Event): event is ConsoleTime { return event.cat === 'blink.console' && isPhaseAsync(event.ph); } diff --git a/front_end/panels/freestyler/AiAgent.ts b/front_end/panels/freestyler/AiAgent.ts index 05a1409a52e..518984b7395 100644 --- a/front_end/panels/freestyler/AiAgent.ts +++ b/front_end/panels/freestyler/AiAgent.ts @@ -3,6 +3,7 @@ // found in the LICENSE file. import * as Host from '../../core/host/host.js'; +import { TimelineUIUtils } from '../timeline/TimelineUIUtils.js'; export const enum ResponseType { CONTEXT = 'context', @@ -375,6 +376,17 @@ STOP`; yield response; } + // Potentially enhance the query with Attribution context. + if (options.selected?.selectedNode?.id === 'EvaluateScript' || options.selected?.selectedNode?.id === 'CompileScript') { + const url = options.selected?.selectedNode?.event?.args?.data?.url; + if (url) { + const attribution = TimelineUIUtils.getAttributionForUrl(url, [...options.selected.parsedTrace.UserTimings.performanceAttributions]); + if (attribution) { + query = `${query}\n\nNote:Attribution for this source: ${attribution}`; + } + } + } + query = await this.enhanceQuery(query, options.selected); for (let i = 0; i < MAX_STEP; i++) { diff --git a/front_end/panels/timeline/TimelineUIUtils.ts b/front_end/panels/timeline/TimelineUIUtils.ts index e389719f81a..052664d5272 100644 --- a/front_end/panels/timeline/TimelineUIUtils.ts +++ b/front_end/panels/timeline/TimelineUIUtils.ts @@ -191,6 +191,7 @@ const UIStrings = { *@description Text for a module, the programming concept */ module: 'Module', + /** *@description Label for a group of JavaScript files */ @@ -557,6 +558,22 @@ const UIStrings = { * @description Label for a string that describes the priority at which a task was scheduled, like 'background' for low-priority tasks, and 'user-blocking' for high priority. */ priority: 'Priority', + /** + *@description Text for a performance attribution event + */ + attribution: 'Attribution', + /** + *@description Text for a WordPress core attribution + */ + wordpressCore: 'Core', + /** + *@description Text for a WordPress plugin attribution + */ + wordpressPlugin: 'WordPress Plugin', + /** + *@description Text for a WordPress theme attribution + */ + wordpressTheme: 'WordPress Theme', }; const str_ = i18n.i18n.registerUIStrings('panels/timeline/TimelineUIUtils.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); @@ -1209,6 +1226,10 @@ export class TimelineUIUtils { if (url) { const {lineNumber, columnNumber} = Trace.Helpers.Trace.getZeroIndexedLineAndColumnForEvent(event); contentHelper.appendLocationRow(i18nString(UIStrings.script), url, lineNumber || 0, columnNumber); + const attribution = TimelineUIUtils.getAttributionForUrl(url, [...parsedTrace.UserTimings.performanceAttributions]); + if (attribution) { + contentHelper.appendTextRow(i18nString(UIStrings.attribution), attribution); + } } const isEager = Boolean(event.args.data?.eager); if (isEager) { @@ -1315,6 +1336,10 @@ export class TimelineUIUtils { if (url) { const {lineNumber, columnNumber} = Trace.Helpers.Trace.getZeroIndexedLineAndColumnForEvent(event); contentHelper.appendLocationRow(i18nString(UIStrings.script), url, lineNumber || 0, columnNumber); + const attribution = TimelineUIUtils.getAttributionForUrl(url, [...parsedTrace.UserTimings.performanceAttributions]); + if (attribution) { + contentHelper.appendTextRow(i18nString(UIStrings.attribution), attribution); + } } break; } @@ -1708,6 +1733,37 @@ export class TimelineUIUtils { return contentHelper.fragment; } + /** + * Get attribution data for a given URL. + * + * @param {string} url URL to check for attribution data. + * @param {Trace.Types.Events.PerformanceAttribution[]} attributions Array of performance attributions. Immutable. + * + * @return {string|null} Attribution name or null if no attribution is found. + */ + static getAttributionForUrl(url: string, attributions: Trace.Types.Events.PerformanceAttribution[]): string|null { + const attribution = attributions.find(attribution => { + const detail = JSON.parse(attribution.args.data.detail); + const parsedUrl = new URL(url); + return detail.path.includes(parsedUrl.pathname); + }); + if (attribution) { + const detail = JSON.parse(attribution.args.data.detail); + + // Determine the type of attribution is based on the attribution name. It can be either a plugin, theme or core enqueue. + let type = ''; + if (attribution.name.includes('core')) { + type = UIStrings.wordpressCore; + } else if (attribution.name.includes('plugin')) { + type = UIStrings.wordpressPlugin; + } else if (attribution.name.includes('theme')) { + type = UIStrings.wordpressTheme; + } + return `${type} - ${detail.name}`; + } + return null; + } + static statsForTimeRange( events: Trace.Types.Events.Event[], startTime: Trace.Types.Timing.MilliSeconds, endTime: Trace.Types.Timing.MilliSeconds): {