Skip to content

Commit fcee2c8

Browse files
authored
Merge branch 'main' into depfu/batch_all/yarn/2025-12-10
2 parents 50e6530 + b0614b5 commit fcee2c8

File tree

12 files changed

+308729
-146
lines changed

12 files changed

+308729
-146
lines changed

src/actions/app.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,12 @@ export function setupInitialUrlState(
150150
return;
151151
}
152152

153-
// Validate the initial URL state. We can't refresh on a from-file URL.
154-
if (urlState.dataSource === 'from-file') {
153+
// Validate the initial URL state. We can't refresh on from-file or
154+
// unpublished URLs.
155+
if (
156+
urlState.dataSource === 'from-file' ||
157+
urlState.dataSource === 'unpublished'
158+
) {
155159
urlState = null;
156160
}
157161

src/actions/receive-profile.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1400,9 +1400,10 @@ export function retrieveProfileForRawUrl(
14001400
}
14011401

14021402
let dataSource = ensureIsValidDataSource(possibleDataSource);
1403-
if (dataSource === 'from-file') {
1404-
// Redirect to 'none' if `dataSource` is 'from-file' since initial urls can't
1405-
// be 'from-file' and needs to be redirected to home page.
1403+
// Redirect to 'none' for from-file and unpublished data sources since initial
1404+
// urls can't be 'from-file' or 'unpublished' and need to be redirected to
1405+
// home page. 'unpublished' is a transient state after profile deletion.
1406+
if (dataSource === 'from-file' || dataSource === 'unpublished') {
14061407
dataSource = 'none';
14071408
}
14081409
dispatch(setDataSource(dataSource));
@@ -1475,7 +1476,6 @@ export function retrieveProfileForRawUrl(
14751476
case 'uploaded-recordings':
14761477
case 'none':
14771478
case 'local':
1478-
case 'unpublished':
14791479
// There is no profile to download for these datasources.
14801480
break;
14811481
default:

src/components/tooltip/Marker.tsx

Lines changed: 132 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ import {
3737
import { Backtrace } from 'firefox-profiler/components/shared/Backtrace';
3838

3939
import {
40-
formatMarkupFromMarkerSchema,
4140
getSchemaFromMarker,
41+
formatFromMarkerSchema,
4242
} from 'firefox-profiler/profile-logic/marker-schema';
4343
import { computeScreenshotSize } from 'firefox-profiler/profile-logic/marker-data';
4444

@@ -52,13 +52,15 @@ import type {
5252
PageList,
5353
MarkerSchemaByName,
5454
MarkerIndex,
55+
MarkerFormatType,
5556
InnerWindowID,
5657
Page,
5758
Pid,
5859
Tid,
5960
IndexIntoStackTable,
6061
} from 'firefox-profiler/types';
6162

63+
import type { StringTable } from 'firefox-profiler/utils/string-table';
6264
import type { ConnectedProps } from 'firefox-profiler/utils/connect';
6365
import {
6466
getGCMinorDetails,
@@ -281,7 +283,7 @@ class MarkerTooltipContents extends React.PureComponent<Props> {
281283
const displayLabel = this.props.showKeys ? key : label || key;
282284
details.push(
283285
<TooltipDetail key={schema.name + '-' + key} label={displayLabel}>
284-
{formatMarkupFromMarkerSchema(
286+
{renderMarkerFieldValue(
285287
schema.name,
286288
format,
287289
value,
@@ -558,6 +560,134 @@ class MarkerTooltipContents extends React.PureComponent<Props> {
558560
}
559561
}
560562

563+
// This regexp is used to test for URLs and remove their scheme for display.
564+
const URL_SCHEME_REGEXP = /^http(s?):\/\//;
565+
566+
/**
567+
* This function may return structured markup for some types suchs as table,
568+
* list, or urls. For other types this falls back to formatFromMarkerSchema
569+
* above.
570+
*/
571+
export function renderMarkerFieldValue(
572+
markerType: string,
573+
format: MarkerFormatType,
574+
value: any,
575+
stringTable: StringTable,
576+
threadIdToNameMap?: Map<Tid, string>,
577+
processIdToNameMap?: Map<Pid, string>
578+
): React.ReactElement | string {
579+
if (value === undefined || value === null) {
580+
console.warn(`Formatting ${value} for ${JSON.stringify(markerType)}`);
581+
return '(empty)';
582+
}
583+
if (format !== 'url' && typeof format !== 'object' && format !== 'list') {
584+
return formatFromMarkerSchema(
585+
markerType,
586+
format,
587+
value,
588+
stringTable,
589+
threadIdToNameMap,
590+
processIdToNameMap
591+
);
592+
}
593+
if (typeof format === 'object') {
594+
switch (format.type) {
595+
case 'table': {
596+
const { columns } = format;
597+
if (!(value instanceof Array)) {
598+
throw new Error('Expected an array for table type');
599+
}
600+
const hasHeader = columns.some((column) => column.label);
601+
return (
602+
<table className="marker-table-value">
603+
{hasHeader ? (
604+
<thead>
605+
<tr>
606+
{columns.map((col, i) => (
607+
<th key={i}>{col.label || ''}</th>
608+
))}
609+
</tr>
610+
</thead>
611+
) : null}
612+
<tbody>
613+
{value.map((row, i) => {
614+
if (!(row instanceof Array)) {
615+
throw new Error('Expected an array for table row');
616+
}
617+
618+
if (row.length !== columns.length) {
619+
throw new Error(
620+
`Row ${i} length doesn't match column count (row: ${row.length}, cols: ${columns.length})`
621+
);
622+
}
623+
return (
624+
<tr key={i}>
625+
{row.map((cell, i) => {
626+
return (
627+
<td key={i}>
628+
{renderMarkerFieldValue(
629+
markerType,
630+
columns[i].type || 'string',
631+
cell,
632+
stringTable,
633+
threadIdToNameMap,
634+
processIdToNameMap
635+
)}
636+
</td>
637+
);
638+
})}
639+
</tr>
640+
);
641+
})}
642+
</tbody>
643+
</table>
644+
);
645+
}
646+
default:
647+
throw new Error(
648+
`Unknown format type ${JSON.stringify(format as never)}`
649+
);
650+
}
651+
}
652+
switch (format) {
653+
case 'list':
654+
if (!(value instanceof Array)) {
655+
throw new Error('Expected an array for list format');
656+
}
657+
return (
658+
<ul className="marker-list-value">
659+
{value.map((_entry, i) => (
660+
<li key={i}>
661+
{renderMarkerFieldValue(
662+
markerType,
663+
'string',
664+
value[i],
665+
stringTable
666+
)}
667+
</li>
668+
))}
669+
</ul>
670+
);
671+
case 'url': {
672+
if (!URL_SCHEME_REGEXP.test(value)) {
673+
return value;
674+
}
675+
return (
676+
<a
677+
href={value}
678+
target="_blank"
679+
rel="noreferrer"
680+
className="marker-link-value"
681+
>
682+
{value.replace(URL_SCHEME_REGEXP, '')}
683+
</a>
684+
);
685+
}
686+
default:
687+
throw new Error(`Unknown format type ${JSON.stringify(format as never)}`);
688+
}
689+
}
690+
561691
const ConnectedMarkerTooltipContents = explicitConnect<
562692
OwnProps,
563693
StateProps,

src/profile-logic/import/chrome.ts

Lines changed: 76 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,8 @@ function getThreadInfo(
339339
if (threadNameEvent) {
340340
thread.name = threadNameEvent.args.name;
341341
thread.isMainThread =
342-
thread.name.startsWith('Cr') && thread.name.endsWith('Main');
342+
(thread.name.startsWith('Cr') && thread.name.endsWith('Main')) ||
343+
(!!chunk.pid && chunk.pid === chunk.tid);
343344
}
344345

345346
const processNameEvent = findEvent<ProcessNameEvent>(
@@ -940,6 +941,14 @@ function extractMarkers(
940941
},
941942
];
942943

944+
// Map to store begin event detail field for pairing with end events.
945+
// For async events (b/e), key is "pid:tid:id:name"
946+
// For duration events (B/E), key is "pid:tid:name"
947+
const beginEventDetail: Map<string, string> = new Map();
948+
949+
// Track whether we've added the EventWithDetail schema
950+
let hasEventWithDetailSchema = false;
951+
943952
for (const [name, events] of eventsByName.entries()) {
944953
if (
945954
name === 'Profile' ||
@@ -988,11 +997,35 @@ function extractMarkers(
988997
const { thread } = threadInfo;
989998
const { markers } = thread;
990999
let argData:
991-
| (object & { type2?: unknown; category2?: unknown })
1000+
| (object & { type2?: unknown; category2?: unknown; detail?: string })
9921001
| null = null;
9931002
if ('args' in event && event.args && typeof event.args === 'object') {
994-
argData = event.args.data || null;
1003+
// Some trace events have args.data, but others have args fields directly
1004+
// (e.g., "Source" markers have args.detail).
1005+
if (event.args.data) {
1006+
argData = event.args.data;
1007+
} else if (
1008+
'detail' in event.args &&
1009+
typeof event.args.detail === 'string'
1010+
) {
1011+
argData = { detail: event.args.detail };
1012+
}
1013+
}
1014+
1015+
// For end events (E/e), try to use the detail from the corresponding begin event
1016+
if ((event.ph === 'E' || event.ph === 'e') && !argData) {
1017+
// Generate key for looking up the begin event detail
1018+
// For async events (b/e), use id; for duration events (B/E), use name only
1019+
const key =
1020+
event.ph === 'e' && 'id' in event
1021+
? `${event.pid}:${event.tid}:${event.id}:${name}`
1022+
: `${event.pid}:${event.tid}:${name}`;
1023+
const detail = beginEventDetail.get(key);
1024+
if (detail) {
1025+
argData = { detail };
1026+
}
9951027
}
1028+
9961029
markers.name.push(stringTable.indexForString(name));
9971030
markers.category.push(otherCategoryIndex);
9981031

@@ -1003,9 +1036,32 @@ function extractMarkers(
10031036
argData.category2 = argData.category;
10041037
}
10051038

1039+
// Add EventWithDetail schema the first time we encounter a detail field
1040+
if (argData?.detail && !hasEventWithDetailSchema) {
1041+
profile.meta.markerSchema.push({
1042+
// Generic schema for Chrome trace event markers with a detail field.
1043+
// This is used when compiling with clang -ftime-trace=file.json, which
1044+
// generates Source markers, ParseDeclarationOrFunctionDefinition markers,
1045+
// and similar compiler events with file paths or location details.
1046+
name: 'EventWithDetail',
1047+
chartLabel: '{marker.data.detail}',
1048+
tooltipLabel: '{marker.name}: {marker.data.detail}',
1049+
tableLabel: '{marker.data.detail}',
1050+
display: ['marker-chart', 'marker-table'],
1051+
fields: [
1052+
{
1053+
key: 'detail',
1054+
label: 'Details',
1055+
format: 'string',
1056+
},
1057+
],
1058+
});
1059+
hasEventWithDetailSchema = true;
1060+
}
1061+
10061062
const newData = {
10071063
...argData,
1008-
type: name,
1064+
type: argData?.detail ? 'EventWithDetail' : name,
10091065
category: event.cat,
10101066
};
10111067

@@ -1026,13 +1082,29 @@ function extractMarkers(
10261082
markers.startTime.push(time);
10271083
markers.endTime.push(null);
10281084
markers.phase.push(INTERVAL_START);
1085+
1086+
// Store the detail field from begin event so it can be used for the corresponding end event
1087+
if (argData?.detail) {
1088+
const key =
1089+
event.ph === 'b' && 'id' in event
1090+
? `${event.pid}:${event.tid}:${event.id}:${name}`
1091+
: `${event.pid}:${event.tid}:${name}`;
1092+
beginEventDetail.set(key, argData.detail);
1093+
}
10291094
} else if (event.ph === 'E' || event.ph === 'e') {
10301095
// Duration or Async Event End
10311096
// https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview#heading=h.nso4gcezn7n1
10321097
// The 'E' and 'e' phase stand for "end", and is the Chrome equivalent of IntervalEnd.
10331098
markers.startTime.push(null);
10341099
markers.endTime.push(time);
10351100
markers.phase.push(INTERVAL_END);
1101+
1102+
// Clean up the stored begin event detail
1103+
const key =
1104+
event.ph === 'e' && 'id' in event
1105+
? `${event.pid}:${event.tid}:${event.id}:${name}`
1106+
: `${event.pid}:${event.tid}:${name}`;
1107+
beginEventDetail.delete(key);
10361108
} else {
10371109
// Instant Event
10381110
// https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview#heading=h.lenwiilchoxp

0 commit comments

Comments
 (0)