From d62dec81aeb531a0683c072c1e76423bbd4a3852 Mon Sep 17 00:00:00 2001 From: Julien Wajsberg Date: Fri, 6 Sep 2024 11:07:27 +0200 Subject: [PATCH 01/40] Use a TypedArray for a significant performance boost when switching to the marker chart --- src/components/marker-chart/Canvas.js | 15 +++++++++------ src/components/marker-chart/index.js | 4 ++++ src/selectors/per-thread/markers.js | 7 +++++++ 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/components/marker-chart/Canvas.js b/src/components/marker-chart/Canvas.js index a50539c0af..b85615972e 100644 --- a/src/components/marker-chart/Canvas.js +++ b/src/components/marker-chart/Canvas.js @@ -80,6 +80,7 @@ type OwnProps = {| +markerTimingAndBuckets: MarkerTimingAndBuckets, +rowHeight: CssPixels, +getMarker: (MarkerIndex) => Marker, + +markerListLength: number, +threadsKey: ThreadsKey, +updatePreviewSelection: WrapFunctionInDispatch, +changeMouseTimePosition: ChangeMouseTimePosition, @@ -164,11 +165,11 @@ class MarkerChartCanvasImpl extends React.PureComponent { const rightClickedRow: number | void = rightClickedMarkerIndex === null ? undefined - : markerIndexToTimingRow.get(rightClickedMarkerIndex); + : markerIndexToTimingRow[rightClickedMarkerIndex]; let newRow: number | void = hoveredMarker === null ? undefined - : markerIndexToTimingRow.get(hoveredMarker); + : markerIndexToTimingRow[hoveredMarker]; if ( timelineTrackOrganization.type === 'active-tab' && newRow === undefined && @@ -190,7 +191,7 @@ class MarkerChartCanvasImpl extends React.PureComponent { let oldRow: number | void = prevHoveredMarker === null ? undefined - : markerIndexToTimingRow.get(prevHoveredMarker); + : markerIndexToTimingRow[prevHoveredMarker]; if ( timelineTrackOrganization.type === 'active-tab' && oldRow === undefined && @@ -254,8 +255,10 @@ class MarkerChartCanvasImpl extends React.PureComponent { _getMarkerIndexToTimingRow = memoize( ( markerTimingAndBuckets: MarkerTimingAndBuckets - ): Map => { - const markerIndexToTimingRow = new Map(); + ): Uint32Array /* like Map */ => { + const markerIndexToTimingRow = new Uint32Array( + this.props.markerListLength + ); for ( let rowIndex = 0; rowIndex < markerTimingAndBuckets.length; @@ -270,7 +273,7 @@ class MarkerChartCanvasImpl extends React.PureComponent { timingIndex < markerTiming.length; timingIndex++ ) { - markerIndexToTimingRow.set(markerTiming.index[timingIndex], rowIndex); + markerIndexToTimingRow[markerTiming.index[timingIndex]] = rowIndex; } } return markerIndexToTimingRow; diff --git a/src/components/marker-chart/index.js b/src/components/marker-chart/index.js index 86a8d25bc1..2fad12c6ee 100644 --- a/src/components/marker-chart/index.js +++ b/src/components/marker-chart/index.js @@ -57,6 +57,7 @@ type StateProps = {| +getMarker: (MarkerIndex) => Marker, +markerTimingAndBuckets: MarkerTimingAndBuckets, +maxMarkerRows: number, + +markerListLength: number, +timeRange: StartEndRange, +threadsKey: ThreadsKey, +previewSelection: PreviewSelection, @@ -105,6 +106,7 @@ class MarkerChartImpl extends React.PureComponent { render() { const { maxMarkerRows, + markerListLength, timeRange, threadsKey, markerTimingAndBuckets, @@ -156,6 +158,7 @@ class MarkerChartImpl extends React.PureComponent { chartProps={{ markerTimingAndBuckets, getMarker, + markerListLength, // $FlowFixMe Error introduced by upgrading to v0.96.0. See issue #1936. updatePreviewSelection, changeMouseTimePosition, @@ -196,6 +199,7 @@ export const MarkerChart = explicitConnect<{||}, StateProps, DispatchProps>({ getMarker: selectedThreadSelectors.getMarkerGetter(state), markerTimingAndBuckets, maxMarkerRows: markerTimingAndBuckets.length, + markerListLength: selectedThreadSelectors.getMarkerListLength(state), timeRange: getCommittedRange(state), threadsKey: getSelectedThreadsKey(state), previewSelection: getPreviewSelection(state), diff --git a/src/selectors/per-thread/markers.js b/src/selectors/per-thread/markers.js index 66826cf505..574207c8b4 100644 --- a/src/selectors/per-thread/markers.js +++ b/src/selectors/per-thread/markers.js @@ -134,6 +134,12 @@ export function getMarkerSelectorsPerThread( ) ); + /** + * This returns the maximum marker index. + */ + const getMarkerListLength: Selector = (state) => + getFullMarkerList(state).length; + /** * This selector returns a function that's used to retrieve a marker object * from its MarkerIndex: @@ -739,6 +745,7 @@ export function getMarkerSelectorsPerThread( getMarkerIndexToRawMarkerIndexes, getFullMarkerList, getFullMarkerListIndexes, + getMarkerListLength, getNetworkMarkerIndexes, getSearchFilteredNetworkMarkerIndexes, getAreMarkerPanelsEmptyInFullRange, From 6d04c5fe708e0f4a4fe6b526fe585f89b4c97ec7 Mon Sep 17 00:00:00 2001 From: Julien Wajsberg Date: Fri, 6 Sep 2024 11:42:36 +0200 Subject: [PATCH 02/40] Lazy compute labels in the marker chart --- src/components/marker-chart/Canvas.js | 25 ++++++++++++++++--------- src/profile-logic/marker-timing.js | 2 +- src/types/profile-derived.js | 2 +- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/components/marker-chart/Canvas.js b/src/components/marker-chart/Canvas.js index b85615972e..0c47f80b2e 100644 --- a/src/components/marker-chart/Canvas.js +++ b/src/components/marker-chart/Canvas.js @@ -48,7 +48,7 @@ type MarkerDrawingInformation = {| +w: CssPixels, +h: CssPixels, +isInstantMarker: boolean, - +text: string, + +textGetter: () => string, |}; // We can hover over multiple items with Marker chart when we are in the active @@ -290,13 +290,13 @@ class MarkerChartCanvasImpl extends React.PureComponent { w: CssPixels, h: CssPixels, isInstantMarker: boolean, - text: string, + textGetter: () => string, isHighlighted: boolean = false ) { if (isInstantMarker) { this.drawOneInstantMarker(ctx, x, y, h, isHighlighted); } else { - this.drawOneIntervalMarker(ctx, x, y, w, h, text, isHighlighted); + this.drawOneIntervalMarker(ctx, x, y, w, h, textGetter, isHighlighted); } } @@ -306,7 +306,7 @@ class MarkerChartCanvasImpl extends React.PureComponent { y: CssPixels, w: CssPixels, h: CssPixels, - text: string, + textGetter: () => string, isHighlighted: boolean ) { const { marginLeft } = this.props; @@ -354,7 +354,7 @@ class MarkerChartCanvasImpl extends React.PureComponent { const w2: CssPixels = visibleWidth - 2 * TEXT_OFFSET_START; if (w2 > textMeasurement.minWidth) { - const fittedText = textMeasurement.getFittedText(text, w2); + const fittedText = textMeasurement.getFittedText(textGetter(), w2); if (fittedText) { ctx.fillStyle = isHighlighted ? 'white' : 'black'; ctx.fillText(fittedText, x2, y + TEXT_OFFSET_TOP); @@ -477,7 +477,7 @@ class MarkerChartCanvasImpl extends React.PureComponent { x = Math.round(x * devicePixelRatio) / devicePixelRatio; w = Math.round(w * devicePixelRatio) / devicePixelRatio; - const text = markerTiming.label[i]; + const textGetter = markerTiming.label[i]; const markerIndex = markerTiming.index[i]; const isHighlighted = @@ -486,7 +486,14 @@ class MarkerChartCanvasImpl extends React.PureComponent { selectedMarkerIndex === markerIndex; if (isHighlighted) { - highlightedMarkers.push({ x, y, w, h, isInstantMarker, text }); + highlightedMarkers.push({ + x, + y, + w, + h, + isInstantMarker, + textGetter, + }); } else if ( // Always render non-dot markers and markers that are larger than // one pixel. @@ -496,7 +503,7 @@ class MarkerChartCanvasImpl extends React.PureComponent { x !== previousMarkerDrawnAtX ) { previousMarkerDrawnAtX = x; - this.drawOneMarker(ctx, x, y, w, h, isInstantMarker, text); + this.drawOneMarker(ctx, x, y, w, h, isInstantMarker, textGetter); } } } @@ -512,7 +519,7 @@ class MarkerChartCanvasImpl extends React.PureComponent { highlightedMarker.w, highlightedMarker.h, highlightedMarker.isInstantMarker, - highlightedMarker.text, + highlightedMarker.textGetter, true /* isHighlighted */ ); }); diff --git a/src/profile-logic/marker-timing.js b/src/profile-logic/marker-timing.js index 8635198b75..25058b71dc 100644 --- a/src/profile-logic/marker-timing.js +++ b/src/profile-logic/marker-timing.js @@ -110,7 +110,7 @@ export function getMarkerTiming( // The chart will then be responsible for drawing this differently. marker.end === null ? marker.start : marker.end ); - markerTiming.label.push(getLabel(markerIndex)); + markerTiming.label.push(() => getLabel(markerIndex)); markerTiming.index.push(markerIndex); markerTiming.length++; }; diff --git a/src/types/profile-derived.js b/src/types/profile-derived.js index 03cc077102..c18e366b14 100644 --- a/src/types/profile-derived.js +++ b/src/types/profile-derived.js @@ -401,7 +401,7 @@ export type MarkerTiming = {| // End time in milliseconds. It will equals start for instant markers. end: number[], index: MarkerIndex[], - label: string[], + label: Array<() => string>, name: string, bucket: string, // True if this marker timing contains only instant markers. From 749080666cdc6baffcafe2ea5fe9082831cef21a Mon Sep 17 00:00:00 2001 From: Julien Wajsberg Date: Mon, 16 Sep 2024 17:12:06 +0200 Subject: [PATCH 03/40] Instead of storing a lambda for each marker, pass getMarkerLabel down to the Marker Chart canvas component --- src/components/marker-chart/Canvas.js | 23 +++++++++++++---------- src/components/marker-chart/index.js | 4 ++++ src/profile-logic/marker-timing.js | 10 ---------- src/selectors/per-thread/markers.js | 9 +++++---- src/types/profile-derived.js | 1 - 5 files changed, 22 insertions(+), 25 deletions(-) diff --git a/src/components/marker-chart/Canvas.js b/src/components/marker-chart/Canvas.js index 0c47f80b2e..da9f33062b 100644 --- a/src/components/marker-chart/Canvas.js +++ b/src/components/marker-chart/Canvas.js @@ -48,7 +48,7 @@ type MarkerDrawingInformation = {| +w: CssPixels, +h: CssPixels, +isInstantMarker: boolean, - +textGetter: () => string, + +markerIndex: MarkerIndex, |}; // We can hover over multiple items with Marker chart when we are in the active @@ -80,6 +80,7 @@ type OwnProps = {| +markerTimingAndBuckets: MarkerTimingAndBuckets, +rowHeight: CssPixels, +getMarker: (MarkerIndex) => Marker, + +getMarkerLabel: (MarkerIndex) => string, +markerListLength: number, +threadsKey: ThreadsKey, +updatePreviewSelection: WrapFunctionInDispatch, @@ -290,13 +291,13 @@ class MarkerChartCanvasImpl extends React.PureComponent { w: CssPixels, h: CssPixels, isInstantMarker: boolean, - textGetter: () => string, + markerIndex: MarkerIndex, isHighlighted: boolean = false ) { if (isInstantMarker) { this.drawOneInstantMarker(ctx, x, y, h, isHighlighted); } else { - this.drawOneIntervalMarker(ctx, x, y, w, h, textGetter, isHighlighted); + this.drawOneIntervalMarker(ctx, x, y, w, h, markerIndex, isHighlighted); } } @@ -306,10 +307,10 @@ class MarkerChartCanvasImpl extends React.PureComponent { y: CssPixels, w: CssPixels, h: CssPixels, - textGetter: () => string, + markerIndex: MarkerIndex, isHighlighted: boolean ) { - const { marginLeft } = this.props; + const { marginLeft, getMarkerLabel } = this.props; if (w <= 2) { // This is an interval marker small enough that if we drew it as a @@ -354,7 +355,10 @@ class MarkerChartCanvasImpl extends React.PureComponent { const w2: CssPixels = visibleWidth - 2 * TEXT_OFFSET_START; if (w2 > textMeasurement.minWidth) { - const fittedText = textMeasurement.getFittedText(textGetter(), w2); + const fittedText = textMeasurement.getFittedText( + getMarkerLabel(markerIndex), + w2 + ); if (fittedText) { ctx.fillStyle = isHighlighted ? 'white' : 'black'; ctx.fillText(fittedText, x2, y + TEXT_OFFSET_TOP); @@ -477,7 +481,6 @@ class MarkerChartCanvasImpl extends React.PureComponent { x = Math.round(x * devicePixelRatio) / devicePixelRatio; w = Math.round(w * devicePixelRatio) / devicePixelRatio; - const textGetter = markerTiming.label[i]; const markerIndex = markerTiming.index[i]; const isHighlighted = @@ -492,7 +495,7 @@ class MarkerChartCanvasImpl extends React.PureComponent { w, h, isInstantMarker, - textGetter, + markerIndex, }); } else if ( // Always render non-dot markers and markers that are larger than @@ -503,7 +506,7 @@ class MarkerChartCanvasImpl extends React.PureComponent { x !== previousMarkerDrawnAtX ) { previousMarkerDrawnAtX = x; - this.drawOneMarker(ctx, x, y, w, h, isInstantMarker, textGetter); + this.drawOneMarker(ctx, x, y, w, h, isInstantMarker, markerIndex); } } } @@ -519,7 +522,7 @@ class MarkerChartCanvasImpl extends React.PureComponent { highlightedMarker.w, highlightedMarker.h, highlightedMarker.isInstantMarker, - highlightedMarker.textGetter, + highlightedMarker.markerIndex, true /* isHighlighted */ ); }); diff --git a/src/components/marker-chart/index.js b/src/components/marker-chart/index.js index 2fad12c6ee..17abbb99cf 100644 --- a/src/components/marker-chart/index.js +++ b/src/components/marker-chart/index.js @@ -55,6 +55,7 @@ type DispatchProps = {| type StateProps = {| +getMarker: (MarkerIndex) => Marker, + +getMarkerLabel: (MarkerIndex) => string, +markerTimingAndBuckets: MarkerTimingAndBuckets, +maxMarkerRows: number, +markerListLength: number, @@ -111,6 +112,7 @@ class MarkerChartImpl extends React.PureComponent { threadsKey, markerTimingAndBuckets, getMarker, + getMarkerLabel, previewSelection, updatePreviewSelection, changeMouseTimePosition, @@ -158,6 +160,7 @@ class MarkerChartImpl extends React.PureComponent { chartProps={{ markerTimingAndBuckets, getMarker, + getMarkerLabel, markerListLength, // $FlowFixMe Error introduced by upgrading to v0.96.0. See issue #1936. updatePreviewSelection, @@ -197,6 +200,7 @@ export const MarkerChart = explicitConnect<{||}, StateProps, DispatchProps>({ selectedThreadSelectors.getMarkerChartTimingAndBuckets(state); return { getMarker: selectedThreadSelectors.getMarkerGetter(state), + getMarkerLabel: selectedThreadSelectors.getMarkerChartLabelGetter(state), markerTimingAndBuckets, maxMarkerRows: markerTimingAndBuckets.length, markerListLength: selectedThreadSelectors.getMarkerListLength(state), diff --git a/src/profile-logic/marker-timing.js b/src/profile-logic/marker-timing.js index 25058b71dc..c74118b0ad 100644 --- a/src/profile-logic/marker-timing.js +++ b/src/profile-logic/marker-timing.js @@ -30,7 +30,6 @@ const MAX_STACKING_DEPTH = 300; * start: [0, 23, 35, 65, 75], * end: [1, 25, 37, 67, 77], * index: [0, 2, 5, 6, 8], - * label: ["Aye", "Aye", "Aye", "Aye", "Aye"], * bucket: "DOM", * instantOnly: false, * length: 5, @@ -40,7 +39,6 @@ const MAX_STACKING_DEPTH = 300; * start: [1, 28, 39, 69, 70], * end: [2, 29, 49, 70, 77], * index: [1, 3, 7, 9, 10], - * label: ["Bee", "Bee", "Bee", "Bee", "Bee"], * bucket: "DOM", * instantOnly: false, * length: 5, @@ -50,7 +48,6 @@ const MAX_STACKING_DEPTH = 300; * start: [1, 28, 39, 69, 70], * end: [2, 29, 49, 70, 77], * index: [1, 3, 7, 9, 10], - * label: ["Bee", "Bee", "Bee", "Bee", "Bee"], * bucket: "DOM", * instantOnly: false, * length: 5, @@ -60,7 +57,6 @@ const MAX_STACKING_DEPTH = 300; * start: [10, 33, 45, 75, 85], * end: [11, 35, 47, 77, 87], * index: [4, 11, 12, 13, 14], - * label: ["Sea", "Sea", "Sea", "Sea", "Sea"], * bucket: "Other", * instantOnly: false, * length: 5, @@ -110,7 +106,6 @@ export function getMarkerTiming( // The chart will then be responsible for drawing this differently. marker.end === null ? marker.start : marker.end ); - markerTiming.label.push(() => getLabel(markerIndex)); markerTiming.index.push(markerIndex); markerTiming.length++; }; @@ -129,7 +124,6 @@ export function getMarkerTiming( start: [], end: [], index: [], - label: [], name: markerLineName, bucket: bucketName, instantOnly, @@ -254,7 +248,6 @@ export function getMarkerTiming( * start: [0, 23, 35, 65, 75], * end: [1, 25, 37, 67, 77], * index: [0, 2, 5, 6, 8], - * label: ["Aye", "Aye", "Aye", "Aye", "Aye"], * bucket: "DOM", * instantOnly: false, * length: 5, @@ -264,7 +257,6 @@ export function getMarkerTiming( * start: [1, 28, 39, 69, 70], * end: [2, 29, 49, 70, 77], * index: [1, 3, 7, 9, 10], - * label: ["Bee", "Bee", "Bee", "Bee", "Bee"], * bucket: "DOM", * instantOnly: false, * length: 5, @@ -274,7 +266,6 @@ export function getMarkerTiming( * start: [1, 28, 39, 69, 70], * end: [2, 29, 49, 70, 77], * index: [1, 3, 7, 9, 10], - * label: ["Bee", "Bee", "Bee", "Bee", "Bee"], * bucket: "DOM", * instantOnly: false, * length: 5, @@ -285,7 +276,6 @@ export function getMarkerTiming( * start: [10, 33, 45, 75, 85], * end: [11, 35, 47, 77, 87], * index: [4, 11, 12, 13, 14], - * label: ["Sea", "Sea", "Sea", "Sea", "Sea"], * bucket: "Other", * instantOnly: false, * length: 5, diff --git a/src/selectors/per-thread/markers.js b/src/selectors/per-thread/markers.js index 574207c8b4..6cc4053978 100644 --- a/src/selectors/per-thread/markers.js +++ b/src/selectors/per-thread/markers.js @@ -452,7 +452,7 @@ export function getMarkerSelectorsPerThread( /** * This getter uses the marker schema to decide on the labels for the marker chart. */ - const _getMarkerChartLabelGetter: Selector<(MarkerIndex) => string> = + const getMarkerChartLabelGetter: Selector<(MarkerIndex) => string> = createSelector( getMarkerGetter, ProfileSelectors.getMarkerSchema, @@ -487,7 +487,7 @@ export function getMarkerSelectorsPerThread( createSelector( getMarkerGetter, getMarkerChartMarkerIndexes, - _getMarkerChartLabelGetter, + getMarkerChartLabelGetter, ProfileSelectors.getCategories, MarkerTimingLogic.getMarkerTimingAndBuckets ); @@ -541,7 +541,7 @@ export function getMarkerSelectorsPerThread( const getNetworkTrackTiming: Selector = createSelector( getMarkerGetter, getNetworkMarkerIndexes, - _getMarkerChartLabelGetter, + getMarkerChartLabelGetter, MarkerTimingLogic.getMarkerTiming ); @@ -552,7 +552,7 @@ export function getMarkerSelectorsPerThread( const getUserTimingMarkerTiming: Selector = createSelector( getMarkerGetter, getUserTimingMarkerIndexes, - _getMarkerChartLabelGetter, + getMarkerChartLabelGetter, MarkerTimingLogic.getMarkerTiming ); @@ -751,6 +751,7 @@ export function getMarkerSelectorsPerThread( getAreMarkerPanelsEmptyInFullRange, getMarkerTableMarkerIndexes, getMarkerChartMarkerIndexes, + getMarkerChartLabelGetter, getMarkerTooltipLabelGetter, getMarkerTableLabelGetter, getMarkerLabelToCopyGetter, diff --git a/src/types/profile-derived.js b/src/types/profile-derived.js index c18e366b14..3d1d84301a 100644 --- a/src/types/profile-derived.js +++ b/src/types/profile-derived.js @@ -401,7 +401,6 @@ export type MarkerTiming = {| // End time in milliseconds. It will equals start for instant markers. end: number[], index: MarkerIndex[], - label: Array<() => string>, name: string, bucket: string, // True if this marker timing contains only instant markers. From 9bbb0e7c576db1c6c6cbffd9d4c6bc0b3bc85a40 Mon Sep 17 00:00:00 2001 From: Julien Wajsberg Date: Mon, 16 Sep 2024 17:15:08 +0200 Subject: [PATCH 04/40] Clean-up --- src/profile-logic/marker-timing.js | 3 --- src/selectors/per-thread/markers.js | 3 --- 2 files changed, 6 deletions(-) diff --git a/src/profile-logic/marker-timing.js b/src/profile-logic/marker-timing.js index c74118b0ad..9bf6b13f0d 100644 --- a/src/profile-logic/marker-timing.js +++ b/src/profile-logic/marker-timing.js @@ -86,7 +86,6 @@ export function getMarkerTiming( markerIndexes: MarkerIndex[], // Categories can be null for things like Network Markers, where we don't care to // break things up by category. - getLabel: (MarkerIndex) => string, categories: ?CategoryList ): MarkerTiming[] { // Each marker type will have it's own timing information, later collapse these into @@ -287,13 +286,11 @@ export function getMarkerTimingAndBuckets( markerIndexes: MarkerIndex[], // Categories can be null for things like Network Markers, where we don't care to // break things up by category. - getLabel: (MarkerIndex) => string, categories: ?CategoryList ): MarkerTimingAndBuckets { const allMarkerTimings = getMarkerTiming( getMarker, markerIndexes, - getLabel, categories ); diff --git a/src/selectors/per-thread/markers.js b/src/selectors/per-thread/markers.js index 6cc4053978..393ee3f168 100644 --- a/src/selectors/per-thread/markers.js +++ b/src/selectors/per-thread/markers.js @@ -487,7 +487,6 @@ export function getMarkerSelectorsPerThread( createSelector( getMarkerGetter, getMarkerChartMarkerIndexes, - getMarkerChartLabelGetter, ProfileSelectors.getCategories, MarkerTimingLogic.getMarkerTimingAndBuckets ); @@ -541,7 +540,6 @@ export function getMarkerSelectorsPerThread( const getNetworkTrackTiming: Selector = createSelector( getMarkerGetter, getNetworkMarkerIndexes, - getMarkerChartLabelGetter, MarkerTimingLogic.getMarkerTiming ); @@ -552,7 +550,6 @@ export function getMarkerSelectorsPerThread( const getUserTimingMarkerTiming: Selector = createSelector( getMarkerGetter, getUserTimingMarkerIndexes, - getMarkerChartLabelGetter, MarkerTimingLogic.getMarkerTiming ); From 390fe35fdd9c844db28c8cdce441194e163c343c Mon Sep 17 00:00:00 2001 From: Julien Wajsberg Date: Mon, 16 Sep 2024 17:18:16 +0200 Subject: [PATCH 05/40] Update tests --- .../store/__snapshots__/profile-view.test.js.snap | 13 ------------- src/test/store/actions.test.js | 4 ---- src/test/store/markers.test.js | 11 ----------- 3 files changed, 28 deletions(-) diff --git a/src/test/store/__snapshots__/profile-view.test.js.snap b/src/test/store/__snapshots__/profile-view.test.js.snap index 9b4663e43e..34be6786a1 100644 --- a/src/test/store/__snapshots__/profile-view.test.js.snap +++ b/src/test/store/__snapshots__/profile-view.test.js.snap @@ -2007,9 +2007,6 @@ Array [ 3, ], "instantOnly": true, - "label": Array [ - "", - ], "length": 1, "name": "D", "start": Array [ @@ -2025,9 +2022,6 @@ Array [ 4, ], "instantOnly": true, - "label": Array [ - "", - ], "length": 1, "name": "E", "start": Array [ @@ -2043,9 +2037,6 @@ Array [ 5, ], "instantOnly": true, - "label": Array [ - "", - ], "length": 1, "name": "F", "start": Array [ @@ -2063,10 +2054,6 @@ Array [ 7, ], "instantOnly": false, - "label": Array [ - "https://mozilla.org", - "https://mozilla.org", - ], "length": 2, "name": "Network Requests", "start": Array [ diff --git a/src/test/store/actions.test.js b/src/test/store/actions.test.js index eb6670a187..31a0ed22be 100644 --- a/src/test/store/actions.test.js +++ b/src/test/store/actions.test.js @@ -460,7 +460,6 @@ describe('selectors/getCombinedTimingRows', function () { start: [0], end: [10], index: [0], - label: ['renderFunction'], name: 'A', bucket: 'None', instantOnly: false, @@ -470,7 +469,6 @@ describe('selectors/getCombinedTimingRows', function () { start: [1], end: [9], index: [1], - label: ['componentA'], name: 'A', bucket: 'None', instantOnly: false, @@ -480,7 +478,6 @@ describe('selectors/getCombinedTimingRows', function () { start: [2], end: [6], index: [2], - label: ['componentB'], name: 'A', bucket: 'None', instantOnly: false, @@ -490,7 +487,6 @@ describe('selectors/getCombinedTimingRows', function () { bucket: 'None', end: [4], index: [3], - label: ['componentC'], length: 1, name: 'A', start: [3], diff --git a/src/test/store/markers.test.js b/src/test/store/markers.test.js index 8ec23095c3..61450e3768 100644 --- a/src/test/store/markers.test.js +++ b/src/test/store/markers.test.js @@ -118,7 +118,6 @@ describe('selectors/getMarkerChartTimingAndBuckets', function () { start: [1], end: [1], index: [0], - label: [''], bucket: 'Other', instantOnly: true, length: 1, @@ -169,10 +168,6 @@ describe('selectors/getMarkerChartTimingAndBuckets', function () { start: [1, 6], end: [5, 9], index: [0, 1], - label: [ - 'https://www.mozilla.org/', - 'https://www.mozilla.org/image.jpg', - ], instantOnly: false, length: 2, }, @@ -192,7 +187,6 @@ describe('selectors/getMarkerChartTimingAndBuckets', function () { start: [3], end: [3], index: [1], - label: [''], instantOnly: true, length: 1, }, @@ -202,7 +196,6 @@ describe('selectors/getMarkerChartTimingAndBuckets', function () { start: [0], end: [1], index: [0], - label: ['https://mozilla.org'], instantOnly: false, length: 1, }, @@ -284,7 +277,6 @@ describe('selectors/getUserTimingMarkerTiming', function () { start: [6], end: [6], index: [3], - label: ['pointInTime'], name: 'UserTiming', bucket: 'None', instantOnly: true, @@ -294,7 +286,6 @@ describe('selectors/getUserTimingMarkerTiming', function () { start: [0], end: [10], index: [0], - label: ['renderFunction'], name: 'UserTiming', bucket: 'None', instantOnly: false, @@ -304,7 +295,6 @@ describe('selectors/getUserTimingMarkerTiming', function () { start: [1], end: [9], index: [1], - label: ['componentA'], name: 'UserTiming', bucket: 'None', instantOnly: false, @@ -314,7 +304,6 @@ describe('selectors/getUserTimingMarkerTiming', function () { start: [2, 7], end: [5, 9], index: [2, 4], - label: ['componentB', 'componentC'], name: 'UserTiming', bucket: 'None', instantOnly: false, From caaa4e07997996665c71b968e08cc278bfb4325f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Naz=C4=B1m=20Can=20Alt=C4=B1nova?= Date: Thu, 22 Aug 2024 16:24:52 +0200 Subject: [PATCH 06/40] Also check if innerWindowIDs are zero during tab to thread index map creation Because Firefox backend always assumes the zero value is null in their internal represantion. --- src/profile-logic/profile-data.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/profile-logic/profile-data.js b/src/profile-logic/profile-data.js index 59dc3cce65..2720689c31 100644 --- a/src/profile-logic/profile-data.js +++ b/src/profile-logic/profile-data.js @@ -3709,7 +3709,8 @@ export function computeTabToThreadIndexesMap( // First go over the innerWindowIDs of the samples. for (let i = 0; i < thread.frameTable.length; i++) { const innerWindowID = thread.frameTable.innerWindowID[i]; - if (innerWindowID === null) { + if (innerWindowID === null || innerWindowID === 0) { + // Zero value also means null for innerWindowID. continue; } @@ -3741,7 +3742,9 @@ export function computeTabToThreadIndexesMap( if ( markerData.innerWindowID !== null && - markerData.innerWindowID !== undefined + markerData.innerWindowID !== undefined && + // Zero value also means null for innerWindowID. + markerData.innerWindowID !== 0 ) { const innerWindowID = markerData.innerWindowID; const tabID = innerWindowIDToTabMap.get(innerWindowID); From 3498a6d0b7dd06133c487505ca048d7dcb5f3f0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Naz=C4=B1m=20Can=20Alt=C4=B1nova?= Date: Thu, 22 Aug 2024 17:38:59 +0200 Subject: [PATCH 07/40] Add the TabSelectorMenu component Currently we are adding this only, it will be used in the following commits. --- locales/en-US/app.ftl | 7 ++ src/components/shared/TabSelectorMenu.css | 15 ++++ src/components/shared/TabSelectorMenu.js | 95 +++++++++++++++++++++++ 3 files changed, 117 insertions(+) create mode 100644 src/components/shared/TabSelectorMenu.css create mode 100644 src/components/shared/TabSelectorMenu.js diff --git a/locales/en-US/app.ftl b/locales/en-US/app.ftl index 8c493ab4a2..94a68b44a8 100644 --- a/locales/en-US/app.ftl +++ b/locales/en-US/app.ftl @@ -807,6 +807,13 @@ TabBar--marker-table-tab = Marker Table TabBar--network-tab = Network TabBar--js-tracer-tab = JS Tracer +## TabSelectorMenu +## This component is a context menu that's opened when you click on the root +## range at the top left corner for profiler analysis view. It's used to switch +## between tabs that were captured in the profile. + +TabSelectorMenu--full-profile = Full Profile + ## TrackContextMenu ## This is used as a context menu for timeline to organize the tracks in the ## analysis UI. diff --git a/src/components/shared/TabSelectorMenu.css b/src/components/shared/TabSelectorMenu.css new file mode 100644 index 0000000000..9bf56ed2fe --- /dev/null +++ b/src/components/shared/TabSelectorMenu.css @@ -0,0 +1,15 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +.tabSelectorMenuItem.checkable { + padding-right: 10px; + padding-left: 20px; +} + +.react-contextmenu-item.tabSelectorMenuItem.checked:not( + .react-contextmenu-item--disabled + )::before { + /* Move the checkmark to the left instead of right, as it's logically better. */ + left: 8px; +} diff --git a/src/components/shared/TabSelectorMenu.js b/src/components/shared/TabSelectorMenu.js new file mode 100644 index 0000000000..60fd7f8d2d --- /dev/null +++ b/src/components/shared/TabSelectorMenu.js @@ -0,0 +1,95 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// @flow +import * as React from 'react'; +import { MenuItem } from '@firefox-devtools/react-contextmenu'; +import { Localized } from '@fluent/react'; +import classNames from 'classnames'; + +import { ContextMenu } from './ContextMenu'; +import explicitConnect from 'firefox-profiler/utils/connect'; +import { getTabFilter } from '../../selectors/url-state'; +import { getProfileFilterPageDataByTabID } from 'firefox-profiler/selectors/profile'; + +import type { TabID, ProfileFilterPageData } from 'firefox-profiler/types'; +import type { ConnectedProps } from 'firefox-profiler/utils/connect'; + +type StateProps = {| + +tabFilter: TabID | null, + +pageDataByTabID: Map | null, +|}; + +type DispatchProps = {||}; + +type Props = ConnectedProps<{||}, StateProps, DispatchProps>; + +import './TabSelectorMenu.css'; + +class TabSelectorMenuImpl extends React.PureComponent { + _handleClick = (_event: SyntheticEvent<>, _data: {| id: TabID |}): void => { + // FIXME: Implement tab switching. + }; + + renderTabSelectorMenuContents() { + const { pageDataByTabID, tabFilter } = this.props; + if (!pageDataByTabID || pageDataByTabID.size === 0) { + // There is no page data, return early. + return null; + } + + return ( + <> + + Full Profile + + {[...pageDataByTabID].map(([tabID, pageData]) => ( + + {pageData.hostname} + + ))} + + ); + } + + render() { + return ( + + {this.renderTabSelectorMenuContents()} + + ); + } +} + +export const TabSelectorMenu = explicitConnect<{||}, StateProps, DispatchProps>( + { + mapStateToProps: (state) => ({ + tabFilter: getTabFilter(state), + pageDataByTabID: getProfileFilterPageDataByTabID(state), + }), + component: TabSelectorMenuImpl, + } +); From 28cc4fc8a743b0b922353cb68969ff5ac9e9f924 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Naz=C4=B1m=20Can=20Alt=C4=B1nova?= Date: Fri, 23 Aug 2024 00:52:29 +0200 Subject: [PATCH 08/40] Add some proper urls to the fixture pages array --- src/test/components/TooltipMarker.test.js | 4 ++-- .../ActiveTabTimeline.test.js.snap | 8 +++---- .../__snapshots__/MarkerChart.test.js.snap | 16 +++++++++++++- .../fixtures/profiles/processed-profile.js | 22 +++++++++---------- src/test/store/active-tab.test.js | 2 +- src/test/url-handling.test.js | 2 +- 6 files changed, 34 insertions(+), 20 deletions(-) diff --git a/src/test/components/TooltipMarker.test.js b/src/test/components/TooltipMarker.test.js index b977dd20c6..e95d3761e7 100644 --- a/src/test/components/TooltipMarker.test.js +++ b/src/test/components/TooltipMarker.test.js @@ -673,7 +673,7 @@ describe('TooltipMarker', function () { }) ); - expect(getValueForProperty('Page')).toBe('Page #1'); + expect(getValueForProperty('Page')).toBe('https://www.cnn.com/'); }); it('renders page information for private pages in network markers', () => { @@ -696,7 +696,7 @@ describe('TooltipMarker', function () { ); expect(getValueForProperty('Page')).toBe( - 'Page #4 (id: 11111111114) (private)' + 'https://profiler.firefox.com/ (private)' ); expect(getValueForProperty('Private Browsing')).toBe('Yes'); }); diff --git a/src/test/components/__snapshots__/ActiveTabTimeline.test.js.snap b/src/test/components/__snapshots__/ActiveTabTimeline.test.js.snap index 55822df1a7..c41951b619 100644 --- a/src/test/components/__snapshots__/ActiveTabTimeline.test.js.snap +++ b/src/test/components/__snapshots__/ActiveTabTimeline.test.js.snap @@ -106,7 +106,7 @@ exports[`ActiveTabTimeline ActiveTabResourceTrack with a thread/sub-frame track IFrame: - Page #3 + https://www.google.com/

Activity Graph for - Page #3 + https://www.google.com/

This graph shows a visual chart of thread activity. @@ -285,7 +285,7 @@ exports[`ActiveTabTimeline ActiveTabResourcesPanel matches the snapshot of a res IFrame: - Page #2 + https://www.youtube.com/

Activity Graph for - Page #2 + https://www.youtube.com/

This graph shows a visual chart of thread activity. diff --git a/src/test/components/__snapshots__/MarkerChart.test.js.snap b/src/test/components/__snapshots__/MarkerChart.test.js.snap index 66c50df862..49ee30f297 100644 --- a/src/test/components/__snapshots__/MarkerChart.test.js.snap +++ b/src/test/components/__snapshots__/MarkerChart.test.js.snap @@ -1441,7 +1441,21 @@ exports[`MarkerChart with active tab renders the hovered marker properly 2`] = ` Page :

- Page #1 +
+ + https:// + + www.cnn.com + + / + +
diff --git a/src/test/fixtures/profiles/processed-profile.js b/src/test/fixtures/profiles/processed-profile.js index 7ce77937be..d7f6807968 100644 --- a/src/test/fixtures/profiles/processed-profile.js +++ b/src/test/fixtures/profiles/processed-profile.js @@ -1828,11 +1828,11 @@ export function getProfileWithBalancedNativeAllocations() { * Pages array has the following relationship: * Tab #1 Tab #2 * -------------- -------------- - * Page #1 Page #4 - * |- Page #2 | - * | |- Page #3 Page #6 + * cnn.com profiler.firefox.com + * |- youtube.com | + * | |- google.com google.com * | - * Page #5 + * mozilla.org */ export function addActiveTabInformationToProfile( profile: Profile, @@ -1859,28 +1859,28 @@ export function addActiveTabInformationToProfile( { tabID: firstTabTabID, innerWindowID: parentInnerWindowIDsWithChildren, - url: 'Page #1', + url: 'https://www.cnn.com/', embedderInnerWindowID: 0, }, // An iframe page inside the previous page { tabID: firstTabTabID, innerWindowID: iframeInnerWindowIDsWithChild, - url: 'Page #2', + url: 'https://www.youtube.com/', embedderInnerWindowID: parentInnerWindowIDsWithChildren, }, // Another iframe page inside the previous iframe { tabID: firstTabTabID, innerWindowID: firstTabInnerWindowIDs[2], - url: 'Page #3', + url: 'https://www.google.com/', embedderInnerWindowID: iframeInnerWindowIDsWithChild, }, // A top most frame from the second tab { tabID: secondTabTabID, innerWindowID: secondTabInnerWindowIDs[0], - url: 'Page #4', + url: 'https://profiler.firefox.com/', embedderInnerWindowID: 0, }, // Another top most frame from the first tab @@ -1888,15 +1888,15 @@ export function addActiveTabInformationToProfile( { tabID: firstTabTabID, innerWindowID: firstTabInnerWindowIDs[3], - url: 'Page #5', + url: 'https://mozilla.org/', embedderInnerWindowID: 0, }, // Another top most frame from the second tab { tabID: secondTabTabID, innerWindowID: secondTabInnerWindowIDs[1], - url: 'Page #4', - embedderInnerWindowID: 0, + url: 'https://www.google.com/', + embedderInnerWindowID: secondTabInnerWindowIDs[0], }, ]; diff --git a/src/test/store/active-tab.test.js b/src/test/store/active-tab.test.js index 753956ca8f..830397db90 100644 --- a/src/test/store/active-tab.test.js +++ b/src/test/store/active-tab.test.js @@ -174,7 +174,7 @@ describe('ActiveTab', function () { const { getState } = setup(profile, false); expect(getHumanReadableActiveTabTracks(getState())).toEqual([ 'main track [tab] SELECTED', - ' - iframe: Page #2', + ' - iframe: https://www.youtube.com/', ]); }); diff --git a/src/test/url-handling.test.js b/src/test/url-handling.test.js index 3fffcbea50..0dbcb179ea 100644 --- a/src/test/url-handling.test.js +++ b/src/test/url-handling.test.js @@ -596,7 +596,7 @@ describe('ctxId', function () { const resourceTracks = getActiveTabResourceTracks(getState()); expect(resourceTracks).toEqual([ { - name: 'Page #2', + name: 'https://www.youtube.com/', type: 'sub-frame', threadIndex: 1, }, From 22ebc4df212937e08ccf94f9bcaa331d5b0f8ba4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Naz=C4=B1m=20Can=20Alt=C4=B1nova?= Date: Fri, 23 Aug 2024 01:26:06 +0200 Subject: [PATCH 09/40] Add simple tests for TabSelectorMenu --- src/test/components/TabSelectorMenu.test.js | 51 +++++++++++++++++++ .../TabSelectorMenu.test.js.snap | 49 ++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 src/test/components/TabSelectorMenu.test.js create mode 100644 src/test/components/__snapshots__/TabSelectorMenu.test.js.snap diff --git a/src/test/components/TabSelectorMenu.test.js b/src/test/components/TabSelectorMenu.test.js new file mode 100644 index 0000000000..16e3797b5d --- /dev/null +++ b/src/test/components/TabSelectorMenu.test.js @@ -0,0 +1,51 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// @flow + +import React from 'react'; +import { Provider } from 'react-redux'; + +import { render } from 'firefox-profiler/test/fixtures/testing-library'; +import { TabSelectorMenu } from 'firefox-profiler/components/shared/TabSelectorMenu'; +import { addActiveTabInformationToProfile } from '../fixtures/profiles/processed-profile'; +import { getProfileWithNiceTracks } from '../fixtures/profiles/tracks'; +import { storeWithProfile } from '../fixtures/stores'; + +describe('app/TabSelectorMenu', () => { + function setup() { + const { profile, ...extraPageData } = addActiveTabInformationToProfile( + getProfileWithNiceTracks() + ); + + const store = storeWithProfile(profile); + const renderResults = render( + + + + ); + + return { + profile, + ...renderResults, + ...extraPageData, + ...store, + }; + } + + it('should render properly', () => { + const { container } = setup(); + expect(container.firstChild).toMatchSnapshot(); + }); + + it('should not render when the profile does not contain any page data', () => { + const store = storeWithProfile(getProfileWithNiceTracks()); + const { container } = render( + + + + ); + expect(container.firstChild).toMatchSnapshot(); + }); +}); diff --git a/src/test/components/__snapshots__/TabSelectorMenu.test.js.snap b/src/test/components/__snapshots__/TabSelectorMenu.test.js.snap new file mode 100644 index 0000000000..bdac668dc2 --- /dev/null +++ b/src/test/components/__snapshots__/TabSelectorMenu.test.js.snap @@ -0,0 +1,49 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`app/TabSelectorMenu should not render when the profile does not contain any page data 1`] = ` + +`; + +exports[`app/TabSelectorMenu should render properly 1`] = ` + +`; From 70703668338dd9bcddc62d8d7452ea420f0a758f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Naz=C4=B1m=20Can=20Alt=C4=B1nova?= Date: Thu, 22 Aug 2024 17:44:53 +0200 Subject: [PATCH 10/40] Add TabSelectorMenu to FilterNavigatorBar --- src/components/app/ProfileFilterNavigator.css | 33 +++++ src/components/app/ProfileFilterNavigator.js | 139 ++++++++++++++---- .../components/FilterNavigatorBar.test.js | 2 +- .../FilterNavigatorBar.test.js.snap | 18 ++- 4 files changed, 159 insertions(+), 33 deletions(-) create mode 100644 src/components/app/ProfileFilterNavigator.css diff --git a/src/components/app/ProfileFilterNavigator.css b/src/components/app/ProfileFilterNavigator.css new file mode 100644 index 0000000000..7beab5dfd7 --- /dev/null +++ b/src/components/app/ProfileFilterNavigator.css @@ -0,0 +1,33 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +.profileFilterNavigator--tab-selector { + display: flex; + align-items: center; + + /* Padding for the arrow on the left side. */ + padding-left: 20px; + column-gap: 5px; +} + +/* This is the dropdown arrow on the left of the button. */ +.profileFilterNavigator--tab-selector::before { + position: absolute; + top: 4px; + left: 4px; + border-top: 6px solid; + border-right: 4px solid transparent; + border-bottom: 0 solid transparent; + border-left: 4px solid transparent; + margin-top: 5px; + margin-right: 5px; + margin-left: 5px; + color: var(--internal-selected-color); + content: ''; +} + +.profileFilterNavigator--tab-selector.disabled::before { + /* Disabled tab selector indicates this with a grayed out arrow. */ + color: var(--grey-30); +} diff --git a/src/components/app/ProfileFilterNavigator.js b/src/components/app/ProfileFilterNavigator.js index 1c4fd9bb7f..de72e421ae 100644 --- a/src/components/app/ProfileFilterNavigator.js +++ b/src/components/app/ProfileFilterNavigator.js @@ -7,33 +7,47 @@ import React from 'react'; import memoize from 'memoize-immutable'; import { Localized } from '@fluent/react'; +import { showMenu } from '@firefox-devtools/react-contextmenu'; +import classNames from 'classnames'; import explicitConnect from 'firefox-profiler/utils/connect'; import { popCommittedRanges } from 'firefox-profiler/actions/profile-view'; import { getPreviewSelection, getProfileFilterPageData, + getProfileFilterPageDataByTabID, getProfileRootRange, } from 'firefox-profiler/selectors/profile'; -import { getCommittedRangeLabels } from 'firefox-profiler/selectors/url-state'; +import { + getCommittedRangeLabels, + getTabFilter, +} from 'firefox-profiler/selectors/url-state'; import { getFormattedTimeLength } from 'firefox-profiler/profile-logic/committed-ranges'; import { FilterNavigatorBar } from 'firefox-profiler/components/shared/FilterNavigatorBar'; import { Icon } from 'firefox-profiler/components/shared/Icon'; +import { TabSelectorMenu } from '../shared/TabSelectorMenu'; import type { ElementProps } from 'react'; import type { ProfileFilterPageData, StartEndRange, + TabID, } from 'firefox-profiler/types'; +import './ProfileFilterNavigator.css'; + type Props = {| - +filterPageData: ProfileFilterPageData | null, + +filterPageDataForActiveTab: ProfileFilterPageData | null, + +pageDataByTabID: Map | null, + +tabFilter: TabID | null, +rootRange: StartEndRange, ...ElementProps, |}; + type DispatchProps = {| +onPop: $PropertyType, |}; + type StateProps = $ReadOnly<$Exact<$Diff>>; class ProfileFilterNavigatorBarImpl extends React.PureComponent { @@ -44,6 +58,22 @@ class ProfileFilterNavigatorBarImpl extends React.PureComponent { } ); + _showTabSelectorMenu = (event: SyntheticMouseEvent) => { + if (this.props.items.length > 0) { + // Do nothing if there are committed ranges. We only allow users to change + // the tab if they are on root range. + return; + } + + const rect = event.currentTarget.getBoundingClientRect(); + showMenu({ + data: null, + id: 'TabSelectorMenu', + position: { x: rect.left, y: rect.bottom }, + target: event.target, + }); + }; + render() { const { className, @@ -51,36 +81,76 @@ class ProfileFilterNavigatorBarImpl extends React.PureComponent { selectedItem, uncommittedItem, onPop, - filterPageData, rootRange, + filterPageDataForActiveTab, + pageDataByTabID, + tabFilter, } = this.props; let firstItem; - if (filterPageData) { + if (filterPageDataForActiveTab) { + // TODO: Remove this once we ship the tab selector and remove the active tab view. firstItem = ( <> - {filterPageData.favicon ? ( - + {filterPageDataForActiveTab.favicon ? ( + ) : null} - - {filterPageData.hostname} ( + + {filterPageDataForActiveTab.hostname} ( {getFormattedTimeLength(rootRange.end - rootRange.start)}) ); } else { - firstItem = ( - - Full Range - - ); + // This is for the tab selector. + if (pageDataByTabID && pageDataByTabID.size > 0) { + const pageData = + tabFilter !== null ? pageDataByTabID.get(tabFilter) : null; + + firstItem = ( + 0, + })} + > + {/* Show the page data if the profile is filtered by tab */} + {pageData ? ( + <> + {pageData.favicon ? : null} + + {pageData?.hostname} ( + {getFormattedTimeLength(rootRange.end - rootRange.start)}) + + + ) : ( + + Full Range + + )} + + ); + } else { + firstItem = ( + + Full Range + + ); + } } const itemsWithFirstElement = this._getItemsWithFirstElement( @@ -88,13 +158,18 @@ class ProfileFilterNavigatorBarImpl extends React.PureComponent { items ); return ( - + <> + + {pageDataByTabID && pageDataByTabID.size > 0 ? ( + + ) : null} + ); } } @@ -112,7 +187,11 @@ export const ProfileFilterNavigator = explicitConnect< previewSelection.selectionEnd - previewSelection.selectionStart ) : undefined; - const filterPageData = getProfileFilterPageData(state); + + // TODO: Remove this once we ship the tab selector and remove the active tab view. + const filterPageDataForActiveTab = getProfileFilterPageData(state); + const pageDataByTabID = getProfileFilterPageDataByTabID(state); + const tabFilter = getTabFilter(state); const rootRange = getProfileRootRange(state); return { className: 'profileFilterNavigator', @@ -121,7 +200,9 @@ export const ProfileFilterNavigator = explicitConnect< // array's length by adding the first element. selectedItem: items.length, uncommittedItem, - filterPageData, + filterPageDataForActiveTab, + pageDataByTabID, + tabFilter, rootRange, }; }, diff --git a/src/test/components/FilterNavigatorBar.test.js b/src/test/components/FilterNavigatorBar.test.js index 4a439a61de..c9c82e16b1 100644 --- a/src/test/components/FilterNavigatorBar.test.js +++ b/src/test/components/FilterNavigatorBar.test.js @@ -159,6 +159,6 @@ describe('app/ProfileFilterNavigator', () => { }); expect(queryByText(/Full Range/)).not.toBeInTheDocument(); // Using regexp because searching for a partial text. - expect(getByText(/developer\.mozilla\.org/)).toBeInTheDocument(); + expect(getByText(/developer\.mozilla\.org \(/)).toBeInTheDocument(); }); }); diff --git a/src/test/components/__snapshots__/FilterNavigatorBar.test.js.snap b/src/test/components/__snapshots__/FilterNavigatorBar.test.js.snap index 4b996c5b3f..a63ad869dd 100644 --- a/src/test/components/__snapshots__/FilterNavigatorBar.test.js.snap +++ b/src/test/components/__snapshots__/FilterNavigatorBar.test.js.snap @@ -10,7 +10,11 @@ exports[`app/ProfileFilterNavigator renders ProfileFilterNavigator properly 1`] - Full Range (⁨51ms⁩) + + Full Range (⁨51ms⁩) + @@ -27,7 +31,11 @@ exports[`app/ProfileFilterNavigator renders ProfileFilterNavigator properly 2`] class="filterNavigatorBarItemContent" type="button" > - Full Range (⁨51ms⁩) + + Full Range (⁨51ms⁩) +
  • - Full Range (⁨51ms⁩) + + Full Range (⁨51ms⁩) +
  • Date: Sun, 25 Aug 2024 22:43:32 +0200 Subject: [PATCH 11/40] Change the cursor of tab selector --- src/components/app/ProfileFilterNavigator.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/app/ProfileFilterNavigator.css b/src/components/app/ProfileFilterNavigator.css index 7beab5dfd7..216ddf50f2 100644 --- a/src/components/app/ProfileFilterNavigator.css +++ b/src/components/app/ProfileFilterNavigator.css @@ -11,6 +11,10 @@ column-gap: 5px; } +.profileFilterNavigator--tab-selector:not(.disabled) { + cursor: pointer; +} + /* This is the dropdown arrow on the left of the button. */ .profileFilterNavigator--tab-selector::before { position: absolute; From 2d49cc1a469068f7b78d5bcc9bfd1c58b14f976f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Naz=C4=B1m=20Can=20Alt=C4=B1nova?= Date: Thu, 22 Aug 2024 18:01:30 +0200 Subject: [PATCH 12/40] Implement tab switching on tab selector --- src/actions/app.js | 14 --- src/actions/receive-profile.js | 119 +++++++++++++++++++- src/components/shared/TabSelectorMenu.js | 12 ++- src/profile-logic/tracks.js | 132 ++++++++++++++++++++++- src/reducers/app.js | 2 + src/reducers/profile-view.js | 2 + src/reducers/url-state.js | 6 ++ src/test/url-handling.test.js | 7 +- src/types/actions.js | 8 ++ 9 files changed, 273 insertions(+), 29 deletions(-) diff --git a/src/actions/app.js b/src/actions/app.js index fdc215fcad..2ba3166a7f 100644 --- a/src/actions/app.js +++ b/src/actions/app.js @@ -50,7 +50,6 @@ import type { UrlState, UploadedProfileInformation, IndexIntoCategoryList, - TabID, } from 'firefox-profiler/types'; import type { TabSlug } from 'firefox-profiler/app-logic/tabs-handling'; import type { @@ -429,16 +428,3 @@ export function toggleOpenCategoryInSidebar( category, }; } - -/** - * Change the selected browser tab filter for the profile. - * TabID here means the unique ID for a give browser tab and corresponds to - * multiple pages in the `profile.pages` array. - * If it's null it will undo the filter and will show the full profile. - */ -export function changeTabFilter(tabID: TabID | null): Action { - return { - type: 'CHANGE_TAB_FILTER', - tabID, - }; -} diff --git a/src/actions/receive-profile.js b/src/actions/receive-profile.js index 00461eb039..78d8ed6ffa 100644 --- a/src/actions/receive-profile.js +++ b/src/actions/receive-profile.js @@ -40,7 +40,11 @@ import { getActiveTabID, getMarkerSchemaByName, } from 'firefox-profiler/selectors'; -import { getSelectedTab } from 'firefox-profiler/selectors/url-state'; +import { + getSelectedTab, + getTabFilter, +} from 'firefox-profiler/selectors/url-state'; +import { getTabToThreadIndexesMap } from 'firefox-profiler/selectors/profile'; import { withHistoryReplaceStateAsync, withHistoryReplaceStateSync, @@ -288,9 +292,15 @@ export function finalizeFullProfileView( return (dispatch, getState) => { const hasUrlInfo = maybeSelectedThreadIndexes !== null; - const globalTracks = computeGlobalTracks(profile); + const tabToThreadIndexesMap = getTabToThreadIndexesMap(getState()); + const globalTracks = computeGlobalTracks( + profile, + hasUrlInfo ? getTabFilter(getState()) : null, + tabToThreadIndexesMap + ); const localTracksByPid = computeLocalTracksByPid( profile, + globalTracks, getMarkerSchemaByName(getState()) ); @@ -1733,3 +1743,108 @@ export function retrieveProfileForRawUrl( return getProfileOrNull(getState()); }; } + +/** + * Change the selected browser tab filter for the profile. + * TabID here means the unique ID for a give browser tab and corresponds to + * multiple pages in the `profile.pages` array. + * If it's null it will undo the filter and will show the full profile. + */ +export function changeTabFilter(tabID: TabID | null): ThunkAction { + return (dispatch, getState) => { + const profile = getProfile(getState()); + const tabToThreadIndexesMap = getTabToThreadIndexesMap(getState()); + // Compute the global tracks, they will be filtered by tabID if it's + // non-null and will not filter if it's null. + const globalTracks = computeGlobalTracks( + profile, + tabID, + tabToThreadIndexesMap + ); + // Passing the global tracks to see which ones we have. + const localTracksByPid = computeLocalTracksByPid( + profile, + globalTracks, + getMarkerSchemaByName(getState()) + ); + + const legacyThreadOrder = getLegacyThreadOrder(getState()); + const globalTrackOrder = initializeGlobalTrackOrder( + globalTracks, + null, // Passing null to urlGlobalTrackOrder to reinitilize it. + legacyThreadOrder + ); + const localTrackOrderByPid = initializeLocalTrackOrderByPid( + null, // Passing null to urlTrackOrderByPid to reinitilize it. + localTracksByPid, + legacyThreadOrder, + profile + ); + + const tracksWithOrder = { + globalTracks, + globalTrackOrder, + localTracksByPid, + localTrackOrderByPid, + }; + + let hiddenTracks = null; + + // For non-initial profile loads, initialize the set of hidden tracks from + // information in the URL. + const legacyHiddenThreads = getLegacyHiddenThreads(getState()); + if (legacyHiddenThreads !== null) { + hiddenTracks = tryInitializeHiddenTracksLegacy( + tracksWithOrder, + legacyHiddenThreads, + profile + ); + } + if (hiddenTracks === null) { + // Compute a default set of hidden tracks. + // This is the case for the initial profile load. + // We also get here if the URL info was ignored, for example if + // respecting it would have caused all threads to become hidden. + hiddenTracks = computeDefaultHiddenTracks(tracksWithOrder, profile); + } + + const selectedThreadIndexes = initializeSelectedThreadIndex( + null, // maybeSelectedThreadIndexes + getVisibleThreads(tracksWithOrder, hiddenTracks), + profile + ); + + // If the currently selected tab is only visible when the selected track + // has samples, verify that the selected track has samples, and if not + // select the marker chart. + let selectedTab = getSelectedTab(getState()); + if (tabsShowingSampleData.includes(selectedTab)) { + let hasSamples = false; + for (const threadIndex of selectedThreadIndexes) { + const thread = profile.threads[threadIndex]; + const { samples, jsAllocations, nativeAllocations } = thread; + hasSamples = [samples, jsAllocations, nativeAllocations].some((table) => + hasUsefulSamples(table, thread) + ); + if (hasSamples) { + break; + } + } + if (!hasSamples) { + selectedTab = 'marker-chart'; + } + } + + dispatch({ + type: 'CHANGE_TAB_FILTER', + tabID, + selectedThreadIndexes, + selectedTab, + globalTracks, + globalTrackOrder, + localTracksByPid, + localTrackOrderByPid, + ...hiddenTracks, + }); + }; +} diff --git a/src/components/shared/TabSelectorMenu.js b/src/components/shared/TabSelectorMenu.js index 60fd7f8d2d..35f457b7d1 100644 --- a/src/components/shared/TabSelectorMenu.js +++ b/src/components/shared/TabSelectorMenu.js @@ -10,6 +10,7 @@ import classNames from 'classnames'; import { ContextMenu } from './ContextMenu'; import explicitConnect from 'firefox-profiler/utils/connect'; +import { changeTabFilter } from 'firefox-profiler/actions/receive-profile'; import { getTabFilter } from '../../selectors/url-state'; import { getProfileFilterPageDataByTabID } from 'firefox-profiler/selectors/profile'; @@ -21,15 +22,17 @@ type StateProps = {| +pageDataByTabID: Map | null, |}; -type DispatchProps = {||}; +type DispatchProps = {| + +changeTabFilter: typeof changeTabFilter, +|}; type Props = ConnectedProps<{||}, StateProps, DispatchProps>; import './TabSelectorMenu.css'; class TabSelectorMenuImpl extends React.PureComponent { - _handleClick = (_event: SyntheticEvent<>, _data: {| id: TabID |}): void => { - // FIXME: Implement tab switching. + _handleClick = (_event: SyntheticEvent<>, data: {| id: TabID |}): void => { + this.props.changeTabFilter(data.id); }; renderTabSelectorMenuContents() { @@ -90,6 +93,9 @@ export const TabSelectorMenu = explicitConnect<{||}, StateProps, DispatchProps>( tabFilter: getTabFilter(state), pageDataByTabID: getProfileFilterPageDataByTabID(state), }), + mapDispatchToProps: { + changeTabFilter, + }, component: TabSelectorMenuImpl, } ); diff --git a/src/profile-logic/tracks.js b/src/profile-logic/tracks.js index 2fca93c0e7..69ca4569d9 100644 --- a/src/profile-logic/tracks.js +++ b/src/profile-logic/tracks.js @@ -16,6 +16,7 @@ import type { Tid, TrackReference, MarkerSchemaByName, + TabID, } from 'firefox-profiler/types'; import { defaultThreadOrder, getFriendlyThreadName } from './profile-data'; @@ -252,13 +253,26 @@ export function initializeLocalTrackOrderByPid( /** * Take a profile and figure out all of the local tracks, and organize them by PID. + * availableGlobalTracks is being sent by the caller to see which globalTracks + * are present. They could have been filtered out by the tab selector so we + * should ignore them. */ export function computeLocalTracksByPid( profile: Profile, + availableGlobalTracks: GlobalTrack[], markerSchemaByName: MarkerSchemaByName ): Map { const localTracksByPid = new Map(); + // Create a new set of avaiable pids, so we can filter out the local tracks + // if their globalTracks are also filtered out by the tab selector. + const availablePids = new Set(); + for (const globalTrack of availableGlobalTracks) { + if (globalTrack.type === 'process') { + availablePids.add(globalTrack.pid); + } + } + // find markers that might have their own track. const markerSchemasWithGraphs = (profile.meta.markerSchema || []).filter( (schema) => Array.isArray(schema.graphs) && schema.graphs.length > 0 @@ -271,6 +285,10 @@ export function computeLocalTracksByPid( ) { const thread = profile.threads[threadIndex]; const { pid, markers } = thread; + if (!availablePids.has(pid)) { + // If the global track is filtered out ignore it here too. + continue; + } // Get or create the tracks and trackOrder. let tracks = localTracksByPid.get(pid); if (tracks === undefined) { @@ -339,6 +357,11 @@ export function computeLocalTracksByPid( if (counters) { for (let counterIndex = 0; counterIndex < counters.length; counterIndex++) { const { pid, category, samples } = counters[counterIndex]; + if (!availablePids.has(pid)) { + // If the global track is filtered out ignore it here too. + continue; + } + if (['Memory', 'power', 'Bandwidth'].includes(category)) { if (category === 'power' && samples.length <= 2) { // If we have only 2 samples, they are likely both 0 and we don't have a real counter. @@ -439,7 +462,11 @@ export function addProcessCPUTracksForProcess( /** * Take a profile and figure out what GlobalTracks it contains. */ -export function computeGlobalTracks(profile: Profile): GlobalTrack[] { +export function computeGlobalTracks( + profile: Profile, + tabID: TabID | null = null, + tabToThreadIndexesMap: Map> +): GlobalTrack[] { // Defining this ProcessTrack type here helps flow understand the intent of // the internals of this function, otherwise each GlobalTrack usage would need // to check that it's a process type. @@ -534,7 +561,71 @@ export function computeGlobalTracks(profile: Profile): GlobalTrack[] { GLOBAL_TRACK_INDEX_ORDER[a.type] - GLOBAL_TRACK_INDEX_ORDER[b.type] ); - return globalTracks; + // At the end, we need to filter global tracks by current tab. + return filterGlobalTracksByTab( + globalTracks, + profile, + tabID, + tabToThreadIndexesMap + ); +} + +/** + * Filter the global tracks by the current selected tab if it's specified. + */ +function filterGlobalTracksByTab( + globalTracks: GlobalTrack[], + profile: Profile, + tabID: TabID | null, + tabToThreadIndexesMap: Map> +): GlobalTrack[] { + if (tabID === null) { + // Return the global tracks if there is no tab filter. + return globalTracks; + } + + const threadIndexes = tabToThreadIndexesMap.get(tabID); + if (!threadIndexes) { + return globalTracks; + } + + // Filter the tracks by the tab filter. + const newGlobalTracks = []; + for (const globalTrack of globalTracks) { + switch (globalTrack.type) { + case 'process': { + const { mainThreadIndex } = globalTrack; + if (mainThreadIndex === null) { + // Do not incldue the global track if it doesn't have any main thread + // index. + continue; + } + + const thread = profile.threads[mainThreadIndex]; + if ( + // Always add the parent process main thread. + (thread.isMainThread && thread.processType === 'default') || + threadIndexes.has(mainThreadIndex) + ) { + newGlobalTracks.push(globalTrack); + } + break; + } + // Always include the screenshots. + case 'screenshots': + // Also always add the visual progress tracks without looking at the tab + // filter. (fallthrough) + case 'visual-progress': + case 'perceptual-visual-progress': + case 'contentful-visual-progress': + newGlobalTracks.push(globalTrack); + break; + default: + throw new Error('Unhandled globalTack type.'); + } + } + + return newGlobalTracks; } /** @@ -726,7 +817,7 @@ export function computeDefaultHiddenTracks( ): HiddenTracks { return _computeHiddenTracksForVisibleThreads( profile, - computeDefaultVisibleThreads(profile), + computeDefaultVisibleThreads(profile, tracksWithOrder), tracksWithOrder ); } @@ -917,7 +1008,8 @@ const IDLE_THRESHOLD_FRACTION = 0.05; // Return a non-empty set of threads that should be shown by default. export function computeDefaultVisibleThreads( - profile: Profile + profile: Profile, + tracksWithOrder: TracksWithOrder ): Set { const threads = profile.threads; if (threads.length === 0) { @@ -932,9 +1024,36 @@ export function computeDefaultVisibleThreads( return new Set(profile.meta.initialVisibleThreads); } + const allTrackThreads = new Set(); + for (const globalTrack of tracksWithOrder.globalTracks) { + switch (globalTrack.type) { + case 'process': + if (globalTrack.mainThreadIndex !== null) { + allTrackThreads.add(globalTrack.mainThreadIndex); + } + break; + default: + break; + } + } + + // $FlowExpectError Flow doesn't know about Array.prototype.flat. + const localTracks = Array.from( + tracksWithOrder.localTracksByPid.values() + ).flat(); + for (const localTrack of localTracks) { + switch (localTrack.type) { + case 'thread': + allTrackThreads.add(localTrack.threadIndex); + break; + default: + break; + } + } + // First, compute a score for every thread. const maxCpuDeltaPerInterval = computeMaxCPUDeltaPerInterval(profile); - const scores = threads.map((thread, threadIndex) => { + let scores = threads.map((thread, threadIndex) => { const score = _computeThreadDefaultVisibilityScore( profile, thread, @@ -943,6 +1062,9 @@ export function computeDefaultVisibleThreads( return { threadIndex, score }; }); + // Next, filter the tracks by the tab selector threads. + scores = scores.filter(({ threadIndex }) => allTrackThreads.has(threadIndex)); + // Next, sort the threads by score. scores.sort(({ score: a }, { score: b }) => { // Return: diff --git a/src/reducers/app.js b/src/reducers/app.js index ac36d28ffc..5166c45993 100644 --- a/src/reducers/app.js +++ b/src/reducers/app.js @@ -52,6 +52,7 @@ const view: Reducer = ( case 'VIEW_FULL_PROFILE': case 'VIEW_ORIGINS_PROFILE': case 'VIEW_ACTIVE_TAB_PROFILE': + case 'CHANGE_TAB_FILTER': return { phase: 'DATA_LOADED' }; default: return state; @@ -147,6 +148,7 @@ const panelLayoutGeneration: Reducer = (state = 0, action) => { // Bottom box: (fallthrough) case 'UPDATE_BOTTOM_BOX': case 'CLOSE_BOTTOM_BOX_FOR_TAB': + // case 'CHANGE_TAB_FILTER': return state + 1; default: return state; diff --git a/src/reducers/profile-view.js b/src/reducers/profile-view.js index 0c4711a319..5a10ca65b7 100644 --- a/src/reducers/profile-view.js +++ b/src/reducers/profile-view.js @@ -85,6 +85,7 @@ const profile: Reducer = (state = null, action) => { const globalTracks: Reducer = (state = [], action) => { switch (action.type) { case 'VIEW_FULL_PROFILE': + case 'CHANGE_TAB_FILTER': return action.globalTracks; default: return state; @@ -103,6 +104,7 @@ const localTracksByPid: Reducer> = ( case 'VIEW_FULL_PROFILE': case 'ENABLE_EVENT_DELAY_TRACKS': case 'ENABLE_EXPERIMENTAL_PROCESS_CPU_TRACKS': + case 'CHANGE_TAB_FILTER': return action.localTracksByPid; default: return state; diff --git a/src/reducers/url-state.js b/src/reducers/url-state.js index 00b1d6b66a..1d66a4a006 100644 --- a/src/reducers/url-state.js +++ b/src/reducers/url-state.js @@ -97,6 +97,7 @@ const selectedTab: Reducer = (state = 'calltree', action) => { case 'CHANGE_SELECTED_TAB': case 'SELECT_TRACK': case 'VIEW_FULL_PROFILE': + case 'CHANGE_TAB_FILTER': return action.selectedTab; case 'FOCUS_CALL_TREE': return 'calltree'; @@ -138,6 +139,7 @@ const selectedThreads: Reducer | null> = ( case 'HIDE_PROVIDED_TRACKS': case 'ISOLATE_LOCAL_TRACK': case 'TOGGLE_RESOURCES_PANEL': + case 'CHANGE_TAB_FILTER': // Only switch to non-null selected threads. return (action.selectedThreadIndexes: Set); case 'SANITIZED_PROFILE_PUBLISHED': { @@ -325,6 +327,7 @@ const globalTrackOrder: Reducer = (state = [], action) => { switch (action.type) { case 'VIEW_FULL_PROFILE': case 'CHANGE_GLOBAL_TRACK_ORDER': + case 'CHANGE_TAB_FILTER': return action.globalTrackOrder; case 'SANITIZED_PROFILE_PUBLISHED': // If some threads were removed, do not even attempt to figure this out. It's @@ -345,6 +348,7 @@ const hiddenGlobalTracks: Reducer> = ( case 'ISOLATE_PROCESS': case 'ISOLATE_PROCESS_MAIN_THREAD': case 'ISOLATE_SCREENSHOT_TRACK': + case 'CHANGE_TAB_FILTER': return action.hiddenGlobalTracks; case 'HIDE_GLOBAL_TRACK': { const hiddenGlobalTracks = new Set(state); @@ -389,6 +393,7 @@ const hiddenLocalTracksByPid: Reducer>> = ( ) => { switch (action.type) { case 'VIEW_FULL_PROFILE': + case 'CHANGE_TAB_FILTER': return action.hiddenLocalTracksByPid; case 'HIDE_LOCAL_TRACK': { const hiddenLocalTracksByPid = new Map(state); @@ -475,6 +480,7 @@ const localTrackOrderByPid: Reducer> = ( case 'VIEW_FULL_PROFILE': case 'ENABLE_EVENT_DELAY_TRACKS': case 'ENABLE_EXPERIMENTAL_PROCESS_CPU_TRACKS': + case 'CHANGE_TAB_FILTER': return action.localTrackOrderByPid; case 'CHANGE_LOCAL_TRACK_ORDER': { const localTrackOrderByPid = new Map(state); diff --git a/src/test/url-handling.test.js b/src/test/url-handling.test.js index 0dbcb179ea..61ef454c79 100644 --- a/src/test/url-handling.test.js +++ b/src/test/url-handling.test.js @@ -21,11 +21,7 @@ import { updateBottomBoxContentsAndMaybeOpen, closeBottomBox, } from '../actions/profile-view'; -import { - changeSelectedTab, - changeProfilesToCompare, - changeTabFilter, -} from '../actions/app'; +import { changeSelectedTab, changeProfilesToCompare } from '../actions/app'; import { stateFromLocation, getQueryStringFromUrlState, @@ -38,6 +34,7 @@ import { blankStore } from './fixtures/stores'; import { viewProfile, changeTimelineTrackOrganization, + changeTabFilter, } from '../actions/receive-profile'; import type { Profile, diff --git a/src/types/actions.js b/src/types/actions.js index 0bdd85e222..cc25d06f05 100644 --- a/src/types/actions.js +++ b/src/types/actions.js @@ -561,6 +561,14 @@ type UrlStateAction = | {| +type: 'CHANGE_TAB_FILTER', +tabID: TabID | null, + +selectedThreadIndexes: Set, + +globalTracks: GlobalTrack[], + +globalTrackOrder: TrackIndex[], + +hiddenGlobalTracks: Set, + +localTracksByPid: Map, + +hiddenLocalTracksByPid: Map>, + +localTrackOrderByPid: Map, + +selectedTab: TabSlug, |}; type IconsAction = From 509d3f88f680f74beea8a6b40e466ce41dbe9e62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Naz=C4=B1m=20Can=20Alt=C4=B1nova?= Date: Sun, 25 Aug 2024 20:22:25 +0200 Subject: [PATCH 13/40] Add some tests for tab switching behavior --- src/test/components/TabSelectorMenu.test.js | 103 +++++++++++++++++++- 1 file changed, 102 insertions(+), 1 deletion(-) diff --git a/src/test/components/TabSelectorMenu.test.js b/src/test/components/TabSelectorMenu.test.js index 16e3797b5d..190f31dc43 100644 --- a/src/test/components/TabSelectorMenu.test.js +++ b/src/test/components/TabSelectorMenu.test.js @@ -10,8 +10,13 @@ import { Provider } from 'react-redux'; import { render } from 'firefox-profiler/test/fixtures/testing-library'; import { TabSelectorMenu } from 'firefox-profiler/components/shared/TabSelectorMenu'; import { addActiveTabInformationToProfile } from '../fixtures/profiles/processed-profile'; -import { getProfileWithNiceTracks } from '../fixtures/profiles/tracks'; +import { + getProfileWithNiceTracks, + getHumanReadableTracks, +} from '../fixtures/profiles/tracks'; import { storeWithProfile } from '../fixtures/stores'; +import { fireFullClick } from '../fixtures/utils'; +import { getTabFilter } from '../../selectors/url-state'; describe('app/TabSelectorMenu', () => { function setup() { @@ -19,6 +24,19 @@ describe('app/TabSelectorMenu', () => { getProfileWithNiceTracks() ); + // Add some frames with innerWindowIDs now. Note that we only expand the + // innerWindowID array and not the others as we don't check them at all. + // + // Thread 0 will be present in firstTabTabID. + // Thread 1 be present in secondTabTabID. + profile.threads[0].frameTable.innerWindowID[0] = + extraPageData.parentInnerWindowIDsWithChildren; + profile.threads[0].frameTable.length++; + + profile.threads[1].frameTable.innerWindowID[0] = + extraPageData.secondTabInnerWindowIDs[0]; + profile.threads[1].frameTable.length++; + const store = storeWithProfile(profile); const renderResults = render( @@ -48,4 +66,87 @@ describe('app/TabSelectorMenu', () => { ); expect(container.firstChild).toMatchSnapshot(); }); + + it('should switch tabs properly', () => { + const { getState, getByText, firstTabTabID, secondTabTabID } = setup(); + + // Check that there is no tab filter at first. + expect(getTabFilter(getState())).toBe(null); + + // Change the tab filter by clicking on the menu item. + const mozillaTab = getByText('mozilla.org'); + fireFullClick(mozillaTab); + + // Check the tab filter again, it should match the first tab in the profile. + expect(getTabFilter(getState())).toBe(firstTabTabID); + + // Change the tab filter again. + const profilerTab = getByText('profiler.firefox.com'); + fireFullClick(profilerTab); + + // Check the tab filter again, it should match the second tab in the profile. + expect(getTabFilter(getState())).toBe(secondTabTabID); + + // Change the tab filter to the full profile. + const fullProfile = getByText('Full Profile'); + fireFullClick(fullProfile); + + // Check the tab filter again, it should be null, meaning full profile. + expect(getTabFilter(getState())).toBe(null); + }); + + it('should display the relevant threads after tab switch', () => { + const { getState, getByText, firstTabTabID, secondTabTabID } = setup(); + + // Check that there is no tab filter at first. + expect(getTabFilter(getState())).toBe(null); + // Also make sure that we have all the threads currently. + expect(getHumanReadableTracks(getState())).toEqual([ + 'show [thread GeckoMain default]', + 'show [thread GeckoMain tab] SELECTED', + ' - show [thread DOM Worker]', + ' - show [thread Style]', + ]); + + // Change the tab filter by clicking on the menu item. + const profilerTab = getByText('profiler.firefox.com'); + fireFullClick(profilerTab); + + // Check the tab filter again, it should match the second tab in the profile. + expect(getTabFilter(getState())).toBe(secondTabTabID); + // Make sure that the second process group is visible. + // Note that the first thread will be visible too, because it's the parent + // process which we always include. + expect(getHumanReadableTracks(getState())).toEqual([ + 'show [thread GeckoMain default]', + 'show [thread GeckoMain tab] SELECTED', + ' - show [thread DOM Worker]', + ' - show [thread Style]', + ]); + + // Change the tab filter again. + const mozillaTab = getByText('mozilla.org'); + fireFullClick(mozillaTab); + + // Check the tab filter again, it should match the first tab in the profile. + expect(getTabFilter(getState())).toBe(firstTabTabID); + // Also make sure that the first process is visible. + expect(getHumanReadableTracks(getState())).toEqual([ + 'show [thread GeckoMain default] SELECTED', + ]); + + // Change the tab filter to the full profile. + const fullProfile = getByText('Full Profile'); + fireFullClick(fullProfile); + + // Check the tab filter again, it should be null, meaning full profile. + expect(getTabFilter(getState())).toBe(null); + // It should show the full thread list again. + expect(getHumanReadableTracks(getState())).toEqual([ + 'show [thread GeckoMain default]', + 'show [thread GeckoMain tab] SELECTED', + ' - show [thread DOM Worker]', + ' - show [thread Style]', + ]); + }); }); From 4d37bcd66cdb45892b02626513921aaf5e9530e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Naz=C4=B1m=20Can=20Alt=C4=B1nova?= Date: Tue, 27 Aug 2024 12:27:30 +0200 Subject: [PATCH 14/40] Make the tab selector scrollable if the list is too long --- src/components/shared/TabSelectorMenu.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/shared/TabSelectorMenu.css b/src/components/shared/TabSelectorMenu.css index 9bf56ed2fe..e44b137622 100644 --- a/src/components/shared/TabSelectorMenu.css +++ b/src/components/shared/TabSelectorMenu.css @@ -2,6 +2,11 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +.TabSelectorMenu { + /* Make it scrollable if the tab list is too long. */ + overflow-y: auto; +} + .tabSelectorMenuItem.checkable { padding-right: 10px; padding-left: 20px; From 6c9d3ba2628e994e07bead4fdd2fa6fbe4704ae0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Naz=C4=B1m=20Can=20Alt=C4=B1nova?= Date: Fri, 30 Aug 2024 11:27:46 +0200 Subject: [PATCH 15/40] Address the review comments for ProfileFilterNavigator --- src/components/app/ProfileFilterNavigator.css | 16 +++++----------- src/components/app/ProfileFilterNavigator.js | 7 +++++-- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/src/components/app/ProfileFilterNavigator.css b/src/components/app/ProfileFilterNavigator.css index 216ddf50f2..f6e634864e 100644 --- a/src/components/app/ProfileFilterNavigator.css +++ b/src/components/app/ProfileFilterNavigator.css @@ -5,30 +5,24 @@ .profileFilterNavigator--tab-selector { display: flex; align-items: center; - - /* Padding for the arrow on the left side. */ - padding-left: 20px; column-gap: 5px; -} -.profileFilterNavigator--tab-selector:not(.disabled) { - cursor: pointer; + /* Padding for the arrow on the left side. */ + padding-inline-start: 20px; } /* This is the dropdown arrow on the left of the button. */ .profileFilterNavigator--tab-selector::before { position: absolute; - top: 4px; - left: 4px; border-top: 6px solid; border-right: 4px solid transparent; border-bottom: 0 solid transparent; border-left: 4px solid transparent; - margin-top: 5px; - margin-right: 5px; - margin-left: 5px; + margin: 5px 5px 0; color: var(--internal-selected-color); content: ''; + inset-block-start: 4px; + inset-inline-start: 4px; } .profileFilterNavigator--tab-selector.disabled::before { diff --git a/src/components/app/ProfileFilterNavigator.js b/src/components/app/ProfileFilterNavigator.js index de72e421ae..f894a49254 100644 --- a/src/components/app/ProfileFilterNavigator.js +++ b/src/components/app/ProfileFilterNavigator.js @@ -102,7 +102,10 @@ class ProfileFilterNavigatorBarImpl extends React.PureComponent { ); } else { - // This is for the tab selector. + // pageDataByTabID will be empty if there is no page information in the + // profile or when the page information is empty. This could happen for + // older profiles and profiles from external importers that don't have + // this information. if (pageDataByTabID && pageDataByTabID.size > 0) { const pageData = tabFilter !== null ? pageDataByTabID.get(tabFilter) : null; @@ -119,7 +122,7 @@ class ProfileFilterNavigatorBarImpl extends React.PureComponent { <> {pageData.favicon ? : null} - {pageData?.hostname} ( + {pageData.hostname} ( {getFormattedTimeLength(rootRange.end - rootRange.start)}) From c2f1e3e55f4d94525cbae075e2be33c59a777fc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Naz=C4=B1m=20Can=20Alt=C4=B1nova?= Date: Fri, 30 Aug 2024 11:28:22 +0200 Subject: [PATCH 16/40] Address the minor review comments --- locales/en-US/app.ftl | 2 +- src/actions/receive-profile.js | 1 - src/components/shared/TabSelectorMenu.css | 7 +++---- src/components/shared/TabSelectorMenu.js | 5 +++-- src/profile-logic/tracks.js | 11 +++++++---- src/reducers/app.js | 1 - src/test/components/TabSelectorMenu.test.js | 14 +++++++------- .../__snapshots__/TabSelectorMenu.test.js.snap | 2 +- 8 files changed, 22 insertions(+), 21 deletions(-) diff --git a/locales/en-US/app.ftl b/locales/en-US/app.ftl index 94a68b44a8..5946fc9a2d 100644 --- a/locales/en-US/app.ftl +++ b/locales/en-US/app.ftl @@ -812,7 +812,7 @@ TabBar--js-tracer-tab = JS Tracer ## range at the top left corner for profiler analysis view. It's used to switch ## between tabs that were captured in the profile. -TabSelectorMenu--full-profile = Full Profile +TabSelectorMenu--all-tabs-and-windows = All tabs and windows ## TrackContextMenu ## This is used as a context menu for timeline to organize the tracks in the diff --git a/src/actions/receive-profile.js b/src/actions/receive-profile.js index 78d8ed6ffa..fe6ddec989 100644 --- a/src/actions/receive-profile.js +++ b/src/actions/receive-profile.js @@ -1761,7 +1761,6 @@ export function changeTabFilter(tabID: TabID | null): ThunkAction { tabID, tabToThreadIndexesMap ); - // Passing the global tracks to see which ones we have. const localTracksByPid = computeLocalTracksByPid( profile, globalTracks, diff --git a/src/components/shared/TabSelectorMenu.css b/src/components/shared/TabSelectorMenu.css index e44b137622..bcceefc581 100644 --- a/src/components/shared/TabSelectorMenu.css +++ b/src/components/shared/TabSelectorMenu.css @@ -8,13 +8,12 @@ } .tabSelectorMenuItem.checkable { - padding-right: 10px; - padding-left: 20px; + padding-inline: 20px 10px; } .react-contextmenu-item.tabSelectorMenuItem.checked:not( .react-contextmenu-item--disabled )::before { - /* Move the checkmark to the left instead of right, as it's logically better. */ - left: 8px; + /* Move the checkmark to inline-start instead of right, as it's logically better. */ + inset-inline-start: 8px; } diff --git a/src/components/shared/TabSelectorMenu.js b/src/components/shared/TabSelectorMenu.js index 35f457b7d1..532a1aa252 100644 --- a/src/components/shared/TabSelectorMenu.js +++ b/src/components/shared/TabSelectorMenu.js @@ -45,7 +45,6 @@ class TabSelectorMenuImpl extends React.PureComponent { return ( <> { 'aria-checked': tabFilter === null ? 'false' : 'true', }} > - Full Profile + + All tabs and windows + {[...pageDataByTabID].map(([tabID, pageData]) => ( { const localTracksByPid = new Map(); - // Create a new set of avaiable pids, so we can filter out the local tracks + // Create a new set of available pids, so we can filter out the local tracks // if their globalTracks are also filtered out by the tab selector. const availablePids = new Set(); for (const globalTrack of availableGlobalTracks) { @@ -586,6 +586,9 @@ function filterGlobalTracksByTab( const threadIndexes = tabToThreadIndexesMap.get(tabID); if (!threadIndexes) { + // This is not really a possible path. It might indicate a bug on the frontend + // or backend. + console.warn(`Failed to find the thread indexes for given tab ${tabID}`); return globalTracks; } @@ -596,7 +599,7 @@ function filterGlobalTracksByTab( case 'process': { const { mainThreadIndex } = globalTrack; if (mainThreadIndex === null) { - // Do not incldue the global track if it doesn't have any main thread + // Do not include the global track if it doesn't have any main thread // index. continue; } diff --git a/src/reducers/app.js b/src/reducers/app.js index 5166c45993..9e0be11d12 100644 --- a/src/reducers/app.js +++ b/src/reducers/app.js @@ -148,7 +148,6 @@ const panelLayoutGeneration: Reducer = (state = 0, action) => { // Bottom box: (fallthrough) case 'UPDATE_BOTTOM_BOX': case 'CLOSE_BOTTOM_BOX_FOR_TAB': - // case 'CHANGE_TAB_FILTER': return state + 1; default: return state; diff --git a/src/test/components/TabSelectorMenu.test.js b/src/test/components/TabSelectorMenu.test.js index 190f31dc43..5183ba2385 100644 --- a/src/test/components/TabSelectorMenu.test.js +++ b/src/test/components/TabSelectorMenu.test.js @@ -87,11 +87,11 @@ describe('app/TabSelectorMenu', () => { // Check the tab filter again, it should match the second tab in the profile. expect(getTabFilter(getState())).toBe(secondTabTabID); - // Change the tab filter to the full profile. - const fullProfile = getByText('Full Profile'); - fireFullClick(fullProfile); + // Change the tab filter to all tabs and windows + const allTabs = getByText('All tabs and windows'); + fireFullClick(allTabs); - // Check the tab filter again, it should be null, meaning full profile. + // Check the tab filter again, it should be null, meaning all tabs and windows. expect(getTabFilter(getState())).toBe(null); }); @@ -135,9 +135,9 @@ describe('app/TabSelectorMenu', () => { 'show [thread GeckoMain default] SELECTED', ]); - // Change the tab filter to the full profile. - const fullProfile = getByText('Full Profile'); - fireFullClick(fullProfile); + // Change the tab filter to all tabs and windows. + const allTabs = getByText('All tabs and windows'); + fireFullClick(allTabs); // Check the tab filter again, it should be null, meaning full profile. expect(getTabFilter(getState())).toBe(null); diff --git a/src/test/components/__snapshots__/TabSelectorMenu.test.js.snap b/src/test/components/__snapshots__/TabSelectorMenu.test.js.snap index bdac668dc2..6616aa49f5 100644 --- a/src/test/components/__snapshots__/TabSelectorMenu.test.js.snap +++ b/src/test/components/__snapshots__/TabSelectorMenu.test.js.snap @@ -25,7 +25,7 @@ exports[`app/TabSelectorMenu should render properly 1`] = ` role="menuitem" tabindex="-1" > - Full Profile + All tabs and windows
    Date: Fri, 30 Aug 2024 11:45:43 +0200 Subject: [PATCH 17/40] Do no use container in tests anymore --- src/test/components/TabSelectorMenu.test.js | 28 +++--- .../TabSelectorMenu.test.js.snap | 90 ++++++++++--------- 2 files changed, 63 insertions(+), 55 deletions(-) diff --git a/src/test/components/TabSelectorMenu.test.js b/src/test/components/TabSelectorMenu.test.js index 5183ba2385..02c9fab4fa 100644 --- a/src/test/components/TabSelectorMenu.test.js +++ b/src/test/components/TabSelectorMenu.test.js @@ -6,6 +6,7 @@ import React from 'react'; import { Provider } from 'react-redux'; +import { screen } from '@testing-library/react'; import { render } from 'firefox-profiler/test/fixtures/testing-library'; import { TabSelectorMenu } from 'firefox-profiler/components/shared/TabSelectorMenu'; @@ -38,7 +39,7 @@ describe('app/TabSelectorMenu', () => { profile.threads[1].frameTable.length++; const store = storeWithProfile(profile); - const renderResults = render( + render( @@ -46,49 +47,48 @@ describe('app/TabSelectorMenu', () => { return { profile, - ...renderResults, ...extraPageData, ...store, }; } it('should render properly', () => { - const { container } = setup(); - expect(container.firstChild).toMatchSnapshot(); + setup(); + expect(document.body).toMatchSnapshot(); }); it('should not render when the profile does not contain any page data', () => { const store = storeWithProfile(getProfileWithNiceTracks()); - const { container } = render( + render( ); - expect(container.firstChild).toMatchSnapshot(); + expect(document.body).toMatchSnapshot(); }); it('should switch tabs properly', () => { - const { getState, getByText, firstTabTabID, secondTabTabID } = setup(); + const { getState, firstTabTabID, secondTabTabID } = setup(); // Check that there is no tab filter at first. expect(getTabFilter(getState())).toBe(null); // Change the tab filter by clicking on the menu item. - const mozillaTab = getByText('mozilla.org'); + const mozillaTab = screen.getByText('mozilla.org'); fireFullClick(mozillaTab); // Check the tab filter again, it should match the first tab in the profile. expect(getTabFilter(getState())).toBe(firstTabTabID); // Change the tab filter again. - const profilerTab = getByText('profiler.firefox.com'); + const profilerTab = screen.getByText('profiler.firefox.com'); fireFullClick(profilerTab); // Check the tab filter again, it should match the second tab in the profile. expect(getTabFilter(getState())).toBe(secondTabTabID); // Change the tab filter to all tabs and windows - const allTabs = getByText('All tabs and windows'); + const allTabs = screen.getByText('All tabs and windows'); fireFullClick(allTabs); // Check the tab filter again, it should be null, meaning all tabs and windows. @@ -96,7 +96,7 @@ describe('app/TabSelectorMenu', () => { }); it('should display the relevant threads after tab switch', () => { - const { getState, getByText, firstTabTabID, secondTabTabID } = setup(); + const { getState, firstTabTabID, secondTabTabID } = setup(); // Check that there is no tab filter at first. expect(getTabFilter(getState())).toBe(null); @@ -109,7 +109,7 @@ describe('app/TabSelectorMenu', () => { ]); // Change the tab filter by clicking on the menu item. - const profilerTab = getByText('profiler.firefox.com'); + const profilerTab = screen.getByText('profiler.firefox.com'); fireFullClick(profilerTab); // Check the tab filter again, it should match the second tab in the profile. @@ -125,7 +125,7 @@ describe('app/TabSelectorMenu', () => { ]); // Change the tab filter again. - const mozillaTab = getByText('mozilla.org'); + const mozillaTab = screen.getByText('mozilla.org'); fireFullClick(mozillaTab); // Check the tab filter again, it should match the first tab in the profile. @@ -136,7 +136,7 @@ describe('app/TabSelectorMenu', () => { ]); // Change the tab filter to all tabs and windows. - const allTabs = getByText('All tabs and windows'); + const allTabs = screen.getByText('All tabs and windows'); fireFullClick(allTabs); // Check the tab filter again, it should be null, meaning full profile. diff --git a/src/test/components/__snapshots__/TabSelectorMenu.test.js.snap b/src/test/components/__snapshots__/TabSelectorMenu.test.js.snap index 6616aa49f5..6dd1aa39ad 100644 --- a/src/test/components/__snapshots__/TabSelectorMenu.test.js.snap +++ b/src/test/components/__snapshots__/TabSelectorMenu.test.js.snap @@ -1,49 +1,57 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`app/TabSelectorMenu should not render when the profile does not contain any page data 1`] = ` - + +
    + +
    + `; exports[`app/TabSelectorMenu should render properly 1`] = ` - + `; From 7997eae93bfe10e0756b30b195059d400ababaa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Naz=C4=B1m=20Can=20Alt=C4=B1nova?= Date: Fri, 30 Aug 2024 13:30:27 +0200 Subject: [PATCH 18/40] Convert the tab selector span into a button --- src/components/app/ProfileFilterNavigator.css | 6 +- src/components/app/ProfileFilterNavigator.js | 78 ++++++++++++------- src/components/shared/FilterNavigatorBar.css | 3 +- .../FilterNavigatorBar.test.js.snap | 11 +-- 4 files changed, 60 insertions(+), 38 deletions(-) diff --git a/src/components/app/ProfileFilterNavigator.css b/src/components/app/ProfileFilterNavigator.css index f6e634864e..9fb73fee2f 100644 --- a/src/components/app/ProfileFilterNavigator.css +++ b/src/components/app/ProfileFilterNavigator.css @@ -7,8 +7,8 @@ align-items: center; column-gap: 5px; - /* Padding for the arrow on the left side. */ - padding-inline-start: 20px; + /* Padding for the arrow on the left side and a bit on the other for hover. */ + padding-inline: 20px 5px; } /* This is the dropdown arrow on the left of the button. */ @@ -25,7 +25,7 @@ inset-inline-start: 4px; } -.profileFilterNavigator--tab-selector.disabled::before { +span.profileFilterNavigator--tab-selector::before { /* Disabled tab selector indicates this with a grayed out arrow. */ color: var(--grey-30); } diff --git a/src/components/app/ProfileFilterNavigator.js b/src/components/app/ProfileFilterNavigator.js index f894a49254..f4e1d1bed6 100644 --- a/src/components/app/ProfileFilterNavigator.js +++ b/src/components/app/ProfileFilterNavigator.js @@ -59,7 +59,7 @@ class ProfileFilterNavigatorBarImpl extends React.PureComponent { ); _showTabSelectorMenu = (event: SyntheticMouseEvent) => { - if (this.props.items.length > 0) { + if (this.props.items.length > 0 || this.props.uncommittedItem) { // Do nothing if there are committed ranges. We only allow users to change // the tab if they are on root range. return; @@ -110,36 +110,56 @@ class ProfileFilterNavigatorBarImpl extends React.PureComponent { const pageData = tabFilter !== null ? pageDataByTabID.get(tabFilter) : null; - firstItem = ( - 0, - })} - > + const itemContents = pageData ? ( + <> {/* Show the page data if the profile is filtered by tab */} - {pageData ? ( - <> - {pageData.favicon ? : null} - - {pageData.hostname} ( - {getFormattedTimeLength(rootRange.end - rootRange.start)}) - - - ) : ( - - Full Range - - )} - + {pageData.favicon ? : null} + + {pageData.hostname} ( + {getFormattedTimeLength(rootRange.end - rootRange.start)}) + + + ) : ( + + Full Range + ); + + if (items.length === 0 && !uncommittedItem) { + // It should be a clickable button if there are no committed ranges. + firstItem = ( + + ); + } else { + // There are committed ranges, don't make it button because this will + // be wrapped with a button. + firstItem = ( + + {itemContents} + + ); + } } else { firstItem = ( - Full Range (⁨51ms⁩) - +
  • @@ -32,7 +33,7 @@ exports[`app/ProfileFilterNavigator renders ProfileFilterNavigator properly 2`] type="button" > Full Range (⁨51ms⁩) @@ -62,7 +63,7 @@ exports[`app/ProfileFilterNavigator renders ProfileFilterNavigator properly 3`] type="button" > Full Range (⁨51ms⁩) From 749683e3099232e50b0489b5ed58e6da36abe545 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Naz=C4=B1m=20Can=20Alt=C4=B1nova?= Date: Fri, 30 Aug 2024 14:02:49 +0200 Subject: [PATCH 19/40] Filter first the global tracks before sorting them --- src/profile-logic/tracks.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/profile-logic/tracks.js b/src/profile-logic/tracks.js index fc310929db..63a94bdf69 100644 --- a/src/profile-logic/tracks.js +++ b/src/profile-logic/tracks.js @@ -476,7 +476,7 @@ export function computeGlobalTracks( mainThreadIndex: number | null, }; const globalTracksByPid: Map = new Map(); - const globalTracks: GlobalTrack[] = []; + let globalTracks: GlobalTrack[] = []; // Create the global tracks. for ( @@ -553,6 +553,14 @@ export function computeGlobalTracks( } } + // Filter the global tracks by current tab. + globalTracks = filterGlobalTracksByTab( + globalTracks, + profile, + tabID, + tabToThreadIndexesMap + ); + // When adding a new track type, this sort ensures that the newer tracks are added // at the end so that the global track indexes are stable and backwards compatible. globalTracks.sort( @@ -561,13 +569,7 @@ export function computeGlobalTracks( GLOBAL_TRACK_INDEX_ORDER[a.type] - GLOBAL_TRACK_INDEX_ORDER[b.type] ); - // At the end, we need to filter global tracks by current tab. - return filterGlobalTracksByTab( - globalTracks, - profile, - tabID, - tabToThreadIndexesMap - ); + return globalTracks; } /** From 5b1b2791e4f654e0a7a4298b4712d9e16f5b4887 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Naz=C4=B1m=20Can=20Alt=C4=B1nova?= Date: Wed, 18 Sep 2024 12:43:27 +0200 Subject: [PATCH 20/40] Address the last review comments --- src/components/shared/TabSelectorMenu.css | 2 +- src/profile-logic/tracks.js | 62 +++++++++++++---------- 2 files changed, 37 insertions(+), 27 deletions(-) diff --git a/src/components/shared/TabSelectorMenu.css b/src/components/shared/TabSelectorMenu.css index bcceefc581..5da4b4a96a 100644 --- a/src/components/shared/TabSelectorMenu.css +++ b/src/components/shared/TabSelectorMenu.css @@ -15,5 +15,5 @@ .react-contextmenu-item--disabled )::before { /* Move the checkmark to inline-start instead of right, as it's logically better. */ - inset-inline-start: 8px; + inset-inline: 8px 0; } diff --git a/src/profile-logic/tracks.js b/src/profile-logic/tracks.js index 63a94bdf69..207788bdc4 100644 --- a/src/profile-logic/tracks.js +++ b/src/profile-logic/tracks.js @@ -1008,6 +1008,41 @@ export function getLocalTrackName( } } +// Return a Set of all possible track threads. We can't just rely on the +// profile.threads, because some of them could be already filtered out by the +// tab selector. +function computeAllTrackThreads( + tracksWithOrder: TracksWithOrder +): Set { + const allTrackThreads = new Set(); + + for (const globalTrack of tracksWithOrder.globalTracks) { + switch (globalTrack.type) { + case 'process': + if (globalTrack.mainThreadIndex !== null) { + allTrackThreads.add(globalTrack.mainThreadIndex); + } + break; + default: + break; + } + } + + for (const [, localTracks] of tracksWithOrder.localTracksByPid) { + for (const localTrack of localTracks) { + switch (localTrack.type) { + case 'thread': + allTrackThreads.add(localTrack.threadIndex); + break; + default: + break; + } + } + } + + return allTrackThreads; +} + // Consider threads whose sample score is less than 5% of the maximum sample score to be idle. const IDLE_THRESHOLD_FRACTION = 0.05; @@ -1029,32 +1064,7 @@ export function computeDefaultVisibleThreads( return new Set(profile.meta.initialVisibleThreads); } - const allTrackThreads = new Set(); - for (const globalTrack of tracksWithOrder.globalTracks) { - switch (globalTrack.type) { - case 'process': - if (globalTrack.mainThreadIndex !== null) { - allTrackThreads.add(globalTrack.mainThreadIndex); - } - break; - default: - break; - } - } - - // $FlowExpectError Flow doesn't know about Array.prototype.flat. - const localTracks = Array.from( - tracksWithOrder.localTracksByPid.values() - ).flat(); - for (const localTrack of localTracks) { - switch (localTrack.type) { - case 'thread': - allTrackThreads.add(localTrack.threadIndex); - break; - default: - break; - } - } + const allTrackThreads = computeAllTrackThreads(tracksWithOrder); // First, compute a score for every thread. const maxCpuDeltaPerInterval = computeMaxCPUDeltaPerInterval(profile); From 2b2b4347cc18cde3f8744f02a839a429b75ba824 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Naz=C4=B1m=20Can=20Alt=C4=B1nova?= Date: Fri, 30 Aug 2024 14:32:00 +0200 Subject: [PATCH 21/40] Hide the tab selector for now --- src/components/app/ProfileFilterNavigator.js | 3 ++- .../FilterNavigatorBar.test.js.snap | 19 +++---------------- 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/src/components/app/ProfileFilterNavigator.js b/src/components/app/ProfileFilterNavigator.js index f4e1d1bed6..baf2b6b0d9 100644 --- a/src/components/app/ProfileFilterNavigator.js +++ b/src/components/app/ProfileFilterNavigator.js @@ -106,7 +106,8 @@ class ProfileFilterNavigatorBarImpl extends React.PureComponent { // profile or when the page information is empty. This could happen for // older profiles and profiles from external importers that don't have // this information. - if (pageDataByTabID && pageDataByTabID.size > 0) { + // eslint-disable-next-line no-constant-condition + if (false && pageDataByTabID && pageDataByTabID.size > 0) { const pageData = tabFilter !== null ? pageDataByTabID.get(tabFilter) : null; diff --git a/src/test/components/__snapshots__/FilterNavigatorBar.test.js.snap b/src/test/components/__snapshots__/FilterNavigatorBar.test.js.snap index 212d55d210..4b996c5b3f 100644 --- a/src/test/components/__snapshots__/FilterNavigatorBar.test.js.snap +++ b/src/test/components/__snapshots__/FilterNavigatorBar.test.js.snap @@ -10,12 +10,7 @@ exports[`app/ProfileFilterNavigator renders ProfileFilterNavigator properly 1`] - + Full Range (⁨51ms⁩) @@ -32,11 +27,7 @@ exports[`app/ProfileFilterNavigator renders ProfileFilterNavigator properly 2`] class="filterNavigatorBarItemContent" type="button" > - - Full Range (⁨51ms⁩) - + Full Range (⁨51ms⁩)
  • - - Full Range (⁨51ms⁩) - + Full Range (⁨51ms⁩)
  • Date: Thu, 19 Sep 2024 08:21:18 +0000 Subject: [PATCH 22/40] Pontoon: Update French (fr) localization of Firefox Profiler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Théo Chevalier --- locales/fr/app.ftl | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/locales/fr/app.ftl b/locales/fr/app.ftl index 4b5941b65f..c2721701c9 100644 --- a/locales/fr/app.ftl +++ b/locales/fr/app.ftl @@ -96,7 +96,7 @@ CallNodeContextMenu--copy-stack = Copier la pile CallTree--tracing-ms-total = Temps d’exécution (ms) .title = Le temps d’exécution « total » comprend un résumé de tout le temps où cette fonction a été observée sur la pile. Cela inclut le temps pendant lequel la fonction était réellement en cours d’exécution et le temps passé dans le code appelant cette fonction. CallTree--tracing-ms-self = Individuel (ms) - .title = Le temps « individuel » n’inclut que le temps où la fonction était en haut de la pile. Si cette fonction a fait appel à d’autres fonctions, alors le temps des « autres » fonctions n’est pas inclus. Le temps « individuel » est utile pour comprendre où le temps a été réellement passé dans un programme. + .title = Le temps « individuel » n’inclut que le temps où la fonction était en haut de la pile. Si cette fonction a fait appel à d’autres fonctions, alors le temps des « autres » fonctions n’est pas inclus. Le temps « individuel » est utile pour comprendre où le temps a été réellement passé dans un programme. CallTree--samples-total = Total (échantillons) .title = Le nombre d’échantillons « total » comprend un résumé de chaque échantillon où cette fonction a été observée sur la pile. Cela inclut le temps où la fonction était réellement en cours d’exécution et le temps passé dans le code appelant cette fonction. CallTree--samples-self = Individuel @@ -650,6 +650,12 @@ TabBar--marker-table-tab = Tableau des marqueurs TabBar--network-tab = Réseau TabBar--js-tracer-tab = Traceur JS +## TabSelectorMenu +## This component is a context menu that's opened when you click on the root +## range at the top left corner for profiler analysis view. It's used to switch +## between tabs that were captured in the profile. + + ## TrackContextMenu ## This is used as a context menu for timeline to organize the tracks in the ## analysis UI. From 2e7bcf1e31cfe1d6a52b6ada77476d2bc32be682 Mon Sep 17 00:00:00 2001 From: Pin-guang Chen Date: Thu, 19 Sep 2024 08:31:41 +0000 Subject: [PATCH 23/40] Pontoon: Update Chinese (Taiwan) (zh-TW) localization of Firefox Profiler Co-authored-by: Pin-guang Chen --- locales/zh-TW/app.ftl | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/locales/zh-TW/app.ftl b/locales/zh-TW/app.ftl index c822282ae0..755dc7f503 100644 --- a/locales/zh-TW/app.ftl +++ b/locales/zh-TW/app.ftl @@ -662,6 +662,13 @@ TabBar--marker-table-tab = 標記表 TabBar--network-tab = 網路 TabBar--js-tracer-tab = JS 追蹤器 +## TabSelectorMenu +## This component is a context menu that's opened when you click on the root +## range at the top left corner for profiler analysis view. It's used to switch +## between tabs that were captured in the profile. + +TabSelectorMenu--all-tabs-and-windows = 所有分頁與視窗 + ## TrackContextMenu ## This is used as a context menu for timeline to organize the tracks in the ## analysis UI. From 3967d191a666f193bf7ad7b6ab994cea9898b225 Mon Sep 17 00:00:00 2001 From: Mark Heijl Date: Thu, 19 Sep 2024 09:21:51 +0000 Subject: [PATCH 24/40] Pontoon: Update Dutch (nl) localization of Firefox Profiler Co-authored-by: Mark Heijl --- locales/nl/app.ftl | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/locales/nl/app.ftl b/locales/nl/app.ftl index 3d63fab2bb..48923a85de 100644 --- a/locales/nl/app.ftl +++ b/locales/nl/app.ftl @@ -750,6 +750,13 @@ TabBar--marker-table-tab = Markeringstabel TabBar--network-tab = Netwerk TabBar--js-tracer-tab = JS-tracer +## TabSelectorMenu +## This component is a context menu that's opened when you click on the root +## range at the top left corner for profiler analysis view. It's used to switch +## between tabs that were captured in the profile. + +TabSelectorMenu--all-tabs-and-windows = Alle tabbladen en vensters + ## TrackContextMenu ## This is used as a context menu for timeline to organize the tracks in the ## analysis UI. From 6c3a3494c8e255fc5d32157e42be25656db82b89 Mon Sep 17 00:00:00 2001 From: "Francesco Lodolo [:flod]" Date: Thu, 19 Sep 2024 10:11:26 +0000 Subject: [PATCH 25/40] Pontoon: Update Italian (it) localization of Firefox Profiler Co-authored-by: Francesco Lodolo [:flod] --- locales/it/app.ftl | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/locales/it/app.ftl b/locales/it/app.ftl index 136b005b21..5a58e394a4 100644 --- a/locales/it/app.ftl +++ b/locales/it/app.ftl @@ -668,6 +668,13 @@ TabBar--marker-table-tab = Tabella marker TabBar--network-tab = Rete TabBar--js-tracer-tab = Tracer JS +## TabSelectorMenu +## This component is a context menu that's opened when you click on the root +## range at the top left corner for profiler analysis view. It's used to switch +## between tabs that were captured in the profile. + +TabSelectorMenu--all-tabs-and-windows = Tutte le schede e le finestre + ## TrackContextMenu ## This is used as a context menu for timeline to organize the tracks in the ## analysis UI. From 32e1789d48692cc8f65760961924c89753b0065d Mon Sep 17 00:00:00 2001 From: Melo46 Date: Thu, 19 Sep 2024 11:02:32 +0000 Subject: [PATCH 26/40] Pontoon: Update Interlingua (ia) localization of Firefox Profiler Co-authored-by: Melo46 --- locales/ia/app.ftl | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/locales/ia/app.ftl b/locales/ia/app.ftl index 1f301d19ad..7ee59bfba4 100644 --- a/locales/ia/app.ftl +++ b/locales/ia/app.ftl @@ -739,6 +739,13 @@ TabBar--marker-table-tab = Tabula marcatores TabBar--network-tab = Rete TabBar--js-tracer-tab = Traciator JS +## TabSelectorMenu +## This component is a context menu that's opened when you click on the root +## range at the top left corner for profiler analysis view. It's used to switch +## between tabs that were captured in the profile. + +TabSelectorMenu--all-tabs-and-windows = Tote schedas e fenestras + ## TrackContextMenu ## This is used as a context menu for timeline to organize the tracks in the ## analysis UI. From de58f536ce8834e6e7aa6f3d71aa040d7f930a4f Mon Sep 17 00:00:00 2001 From: Fjoerfoks Date: Thu, 19 Sep 2024 11:31:09 +0000 Subject: [PATCH 27/40] Pontoon: Update Frisian (fy-NL) localization of Firefox Profiler Co-authored-by: Fjoerfoks --- locales/fy-NL/app.ftl | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/locales/fy-NL/app.ftl b/locales/fy-NL/app.ftl index 9545715564..ae7d2669d5 100644 --- a/locales/fy-NL/app.ftl +++ b/locales/fy-NL/app.ftl @@ -750,6 +750,13 @@ TabBar--marker-table-tab = Markearingstabel TabBar--network-tab = Netwurk TabBar--js-tracer-tab = JS-tracer +## TabSelectorMenu +## This component is a context menu that's opened when you click on the root +## range at the top left corner for profiler analysis view. It's used to switch +## between tabs that were captured in the profile. + +TabSelectorMenu--all-tabs-and-windows = Alle ljepblêden en finsters + ## TrackContextMenu ## This is used as a context menu for timeline to organize the tracks in the ## analysis UI. From 9ed370ff8113e2a8858ef3f8a6cba5614b5dbfd4 Mon Sep 17 00:00:00 2001 From: Marcelo Ghelman Date: Thu, 19 Sep 2024 13:02:24 +0000 Subject: [PATCH 28/40] Pontoon: Update Portuguese (Brazil) (pt-BR) localization of Firefox Profiler Co-authored-by: Marcelo Ghelman --- locales/pt-BR/app.ftl | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/locales/pt-BR/app.ftl b/locales/pt-BR/app.ftl index cf99b82de2..80f014cbc3 100644 --- a/locales/pt-BR/app.ftl +++ b/locales/pt-BR/app.ftl @@ -679,6 +679,13 @@ TabBar--marker-table-tab = Tabela de marcadores TabBar--network-tab = Rede TabBar--js-tracer-tab = Traçador JS +## TabSelectorMenu +## This component is a context menu that's opened when you click on the root +## range at the top left corner for profiler analysis view. It's used to switch +## between tabs that were captured in the profile. + +TabSelectorMenu--all-tabs-and-windows = Todas as abas e janelas + ## TrackContextMenu ## This is used as a context menu for timeline to organize the tracks in the ## analysis UI. From 5b59eeee04ecf31db124aa7eded5c8fb232350d2 Mon Sep 17 00:00:00 2001 From: Olvcpr423 Date: Thu, 19 Sep 2024 13:41:55 +0000 Subject: [PATCH 29/40] Pontoon: Update Chinese (China) (zh-CN) localization of Firefox Profiler Co-authored-by: Olvcpr423 --- locales/zh-CN/app.ftl | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/locales/zh-CN/app.ftl b/locales/zh-CN/app.ftl index 2fef196cd6..ce606408fc 100644 --- a/locales/zh-CN/app.ftl +++ b/locales/zh-CN/app.ftl @@ -663,6 +663,13 @@ TabBar--marker-table-tab = 标记表 TabBar--network-tab = 网络 TabBar--js-tracer-tab = JS 追踪器 +## TabSelectorMenu +## This component is a context menu that's opened when you click on the root +## range at the top left corner for profiler analysis view. It's used to switch +## between tabs that were captured in the profile. + +TabSelectorMenu--all-tabs-and-windows = 所有标签页和窗口 + ## TrackContextMenu ## This is used as a context menu for timeline to organize the tracks in the ## analysis UI. From ce1190ec32667e731159f59ee6e0619a573fdd79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20K=C3=B6hler?= Date: Thu, 19 Sep 2024 15:02:04 +0000 Subject: [PATCH 30/40] Pontoon: Update German (de) localization of Firefox Profiler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Michael Köhler --- locales/de/app.ftl | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/locales/de/app.ftl b/locales/de/app.ftl index 1f4d26b011..1947701e95 100644 --- a/locales/de/app.ftl +++ b/locales/de/app.ftl @@ -726,6 +726,13 @@ TabBar--marker-table-tab = Markierungstabelle TabBar--network-tab = Netzwerk TabBar--js-tracer-tab = JS-Aufzeichnung +## TabSelectorMenu +## This component is a context menu that's opened when you click on the root +## range at the top left corner for profiler analysis view. It's used to switch +## between tabs that were captured in the profile. + +TabSelectorMenu--all-tabs-and-windows = Alle Tabs und Fenster + ## TrackContextMenu ## This is used as a context menu for timeline to organize the tracks in the ## analysis UI. From 6e32e3c117044dea2155bbb39285c15e604a781a Mon Sep 17 00:00:00 2001 From: Jim Spentzos Date: Thu, 19 Sep 2024 15:31:45 +0000 Subject: [PATCH 31/40] Pontoon: Update Greek (el) localization of Firefox Profiler Co-authored-by: Jim Spentzos --- locales/el/app.ftl | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/locales/el/app.ftl b/locales/el/app.ftl index 83004fb8e7..bd3b98bc6f 100644 --- a/locales/el/app.ftl +++ b/locales/el/app.ftl @@ -745,6 +745,13 @@ TabBar--marker-table-tab = Πίνακας δεικτών TabBar--network-tab = Δίκτυο TabBar--js-tracer-tab = JS Tracer +## TabSelectorMenu +## This component is a context menu that's opened when you click on the root +## range at the top left corner for profiler analysis view. It's used to switch +## between tabs that were captured in the profile. + +TabSelectorMenu--all-tabs-and-windows = Όλες οι καρτέλες και τα παράθυρα + ## TrackContextMenu ## This is used as a context menu for timeline to organize the tracks in the ## analysis UI. From 29fc94471927b69432797b797ca5af484ace7167 Mon Sep 17 00:00:00 2001 From: Ian Neal Date: Thu, 19 Sep 2024 16:31:28 +0000 Subject: [PATCH 32/40] Pontoon: Update English (Great Britain) (en-GB) localization of Firefox Profiler Co-authored-by: Ian Neal --- locales/en-GB/app.ftl | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/locales/en-GB/app.ftl b/locales/en-GB/app.ftl index 9274612cd9..b7b952a1e2 100644 --- a/locales/en-GB/app.ftl +++ b/locales/en-GB/app.ftl @@ -750,6 +750,13 @@ TabBar--marker-table-tab = Marker Table TabBar--network-tab = Network TabBar--js-tracer-tab = JS Tracer +## TabSelectorMenu +## This component is a context menu that's opened when you click on the root +## range at the top left corner for profiler analysis view. It's used to switch +## between tabs that were captured in the profile. + +TabSelectorMenu--all-tabs-and-windows = All tabs and windows + ## TrackContextMenu ## This is used as a context menu for timeline to organize the tracks in the ## analysis UI. From 885af76b83d84f11e1a9048d94c0135050e69ea4 Mon Sep 17 00:00:00 2001 From: Valery Ledovskoy Date: Thu, 19 Sep 2024 17:31:28 +0000 Subject: [PATCH 33/40] Pontoon: Update Russian (ru) localization of Firefox Profiler Co-authored-by: Valery Ledovskoy --- locales/ru/app.ftl | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/locales/ru/app.ftl b/locales/ru/app.ftl index dbc3feebdb..b907ab154f 100644 --- a/locales/ru/app.ftl +++ b/locales/ru/app.ftl @@ -764,6 +764,13 @@ TabBar--marker-table-tab = Таблица маркеров TabBar--network-tab = Сеть TabBar--js-tracer-tab = JS-трассировщик +## TabSelectorMenu +## This component is a context menu that's opened when you click on the root +## range at the top left corner for profiler analysis view. It's used to switch +## between tabs that were captured in the profile. + +TabSelectorMenu--all-tabs-and-windows = Все вкладки и окна + ## TrackContextMenu ## This is used as a context menu for timeline to organize the tracks in the ## analysis UI. From b3fb2f76cdad1083e57ab6df11874defdf8391a7 Mon Sep 17 00:00:00 2001 From: Andreas Pettersson Date: Thu, 19 Sep 2024 21:11:34 +0000 Subject: [PATCH 34/40] Pontoon: Update Swedish (sv-SE) localization of Firefox Profiler Co-authored-by: Andreas Pettersson --- locales/sv-SE/app.ftl | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/locales/sv-SE/app.ftl b/locales/sv-SE/app.ftl index 4dd5d8be67..95b50dcd6a 100644 --- a/locales/sv-SE/app.ftl +++ b/locales/sv-SE/app.ftl @@ -745,6 +745,13 @@ TabBar--marker-table-tab = Markörtabell TabBar--network-tab = Nätverk TabBar--js-tracer-tab = JS Tracer +## TabSelectorMenu +## This component is a context menu that's opened when you click on the root +## range at the top left corner for profiler analysis view. It's used to switch +## between tabs that were captured in the profile. + +TabSelectorMenu--all-tabs-and-windows = Alla flikar och fönster + ## TrackContextMenu ## This is used as a context menu for timeline to organize the tracks in the ## analysis UI. From 20b8616d0703cf80146ed336b4d5f12afdecb422 Mon Sep 17 00:00:00 2001 From: Lobodzets Date: Fri, 20 Sep 2024 19:41:45 +0000 Subject: [PATCH 35/40] Pontoon: Update Ukrainian (uk) localization of Firefox Profiler Co-authored-by: Lobodzets --- locales/uk/app.ftl | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/locales/uk/app.ftl b/locales/uk/app.ftl index 9b36391d0f..ef8e8fa282 100644 --- a/locales/uk/app.ftl +++ b/locales/uk/app.ftl @@ -751,6 +751,13 @@ TabBar--marker-table-tab = Маркерна таблиця TabBar--network-tab = Мережа TabBar--js-tracer-tab = JS Tracer +## TabSelectorMenu +## This component is a context menu that's opened when you click on the root +## range at the top left corner for profiler analysis view. It's used to switch +## between tabs that were captured in the profile. + +TabSelectorMenu--all-tabs-and-windows = Усі вкладки та вікна + ## TrackContextMenu ## This is used as a context menu for timeline to organize the tracks in the ## analysis UI. From efc85013e9e4597f27370fbe2c6acc126fe0a980 Mon Sep 17 00:00:00 2001 From: chutten Date: Fri, 20 Sep 2024 20:11:50 +0000 Subject: [PATCH 36/40] Pontoon: Update English (Canada) (en-CA) localization of Firefox Profiler Co-authored-by: chutten --- locales/en-CA/app.ftl | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/locales/en-CA/app.ftl b/locales/en-CA/app.ftl index 77ad56edb6..8f8d8c7dc1 100644 --- a/locales/en-CA/app.ftl +++ b/locales/en-CA/app.ftl @@ -755,6 +755,13 @@ TabBar--marker-table-tab = Marker Table TabBar--network-tab = Network TabBar--js-tracer-tab = JS Tracer +## TabSelectorMenu +## This component is a context menu that's opened when you click on the root +## range at the top left corner for profiler analysis view. It's used to switch +## between tabs that were captured in the profile. + +TabSelectorMenu--all-tabs-and-windows = All tabs and windows + ## TrackContextMenu ## This is used as a context menu for timeline to organize the tracks in the ## analysis UI. From de0cd4ddcccdb8441d9c05592915c746566605dd Mon Sep 17 00:00:00 2001 From: ravmn Date: Mon, 23 Sep 2024 10:41:26 +0000 Subject: [PATCH 37/40] Pontoon: Update Spanish (Chile) (es-CL) localization of Firefox Profiler Co-authored-by: ravmn --- locales/es-CL/app.ftl | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/locales/es-CL/app.ftl b/locales/es-CL/app.ftl index 202f2c39e5..c37882a8a9 100644 --- a/locales/es-CL/app.ftl +++ b/locales/es-CL/app.ftl @@ -680,6 +680,13 @@ TabBar--marker-table-tab = Tabla de marcas TabBar--network-tab = Red TabBar--js-tracer-tab = Trazador JS +## TabSelectorMenu +## This component is a context menu that's opened when you click on the root +## range at the top left corner for profiler analysis view. It's used to switch +## between tabs that were captured in the profile. + +TabSelectorMenu--all-tabs-and-windows = Todas las pestañas y ventanas + ## TrackContextMenu ## This is used as a context menu for timeline to organize the tracks in the ## analysis UI. From 5d53dcb062628cca7345e13764942fcd89312a18 Mon Sep 17 00:00:00 2001 From: Julien Wajsberg Date: Mon, 23 Sep 2024 14:18:49 +0200 Subject: [PATCH 38/40] Support profiling from the toolbox in Thunderbird Release (#5135) Thunderbird Releases doesn't include the Firefox/ string in its userAgent, therefore we neeed to match Thunderbird/ as well. See also the discussion in https://bugzilla.mozilla.org/show_bug.cgi?id=1920387. --- src/app-logic/browser-connection.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/app-logic/browser-connection.js b/src/app-logic/browser-connection.js index 340e25cdb5..04ede410ad 100644 --- a/src/app-logic/browser-connection.js +++ b/src/app-logic/browser-connection.js @@ -183,8 +183,19 @@ class BrowserConnectionImpl implements BrowserConnection { } } +// Should work with: +// Firefox Desktop: "Mozilla/5.0 (X11; Linux x86_64; rv:132.0) Gecko/20100101 Firefox/132.0" +// Thunderbird: "Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Thunderbird/128.2.3" +// Firefox Android: "Mozilla/5.0 (Android 12; Mobile; rv:132.0) Gecko/132.0 Firefox/132.0" +// Should not work with: +// Chrome: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36' +// Safari: 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.1 Mobile/15E148 Safari/604.1' +// +// We could match for Gecko/ but do all Gecko-based browsers support the +// WebChannel? Probably not. Therefore specifically Firefox and Thunderbird are +// looked for, until we find that we need a broader net. function _isFirefox(userAgent: string): boolean { - return Boolean(userAgent.match(/Firefox\/\d+\.\d+/)); + return userAgent.includes('Firefox/') || userAgent.includes('Thunderbird/'); } class TimeoutError extends Error { From bf1ac66d8523ec6e9ba43b7994fc62bb35c90b04 Mon Sep 17 00:00:00 2001 From: Richard Fine Date: Mon, 23 Sep 2024 16:56:38 +0200 Subject: [PATCH 39/40] Add a dedicated symbolication tool (#5123) In order to avoid the need to launch an entire browser to get a profile symbolicated, this PR adds a small standalone NodeJS-based tool which can be run as a CLI process to symbolicate a profile. The components used by the frontend are reused so that the logic is shared, they're just packaged together into a minimal app with no React/Redux, etc. Once built, the tool is at `dist/symbolicator.js`. ## Usage ``` node dist/symbolicator.js \ --input path/to/unsymbolicated/profile.json \ --output path/to/write/symbolicated/profile.json \ --server ``` --- .circleci/config.yml | 1 + package.json | 3 + src/profile-logic/symbol-store.js | 15 +- src/symbolicator-cli/index.js | 205 +++++ src/symbolicator-cli/webpack.config.js | 28 + src/test/README.md | 13 +- .../symbol-server-response.json | 178 ++++ .../symbolicator-cli/symbolicated.json | 858 ++++++++++++++++++ .../symbolicator-cli/symbolicator-cli.test.js | 58 ++ .../symbolicator-cli/unsymbolicated.json | 813 +++++++++++++++++ src/test/unit/symbolicator-cli.test.js | 129 +++ src/types/index.js | 1 + src/types/libdef/npm/minimist_v1.x.x.js | 22 + src/types/symbolication.js | 36 + yarn.lock | 5 + 15 files changed, 2355 insertions(+), 10 deletions(-) create mode 100644 src/symbolicator-cli/index.js create mode 100644 src/symbolicator-cli/webpack.config.js create mode 100644 src/test/integration/symbolicator-cli/symbol-server-response.json create mode 100644 src/test/integration/symbolicator-cli/symbolicated.json create mode 100644 src/test/integration/symbolicator-cli/symbolicator-cli.test.js create mode 100644 src/test/integration/symbolicator-cli/unsymbolicated.json create mode 100644 src/test/unit/symbolicator-cli.test.js create mode 100644 src/types/libdef/npm/minimist_v1.x.x.js create mode 100644 src/types/symbolication.js diff --git a/.circleci/config.yml b/.circleci/config.yml index 1498c35e50..b8ed74d510 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -54,6 +54,7 @@ jobs: steps: - checkout-and-dependencies - run: yarn build-prod:quiet + - run: yarn build-symbolicator-cli:quiet licence-check: executor: node diff --git a/package.json b/package.json index 57349638a9..46647a606a 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,8 @@ "build-l10n-prod:quiet": "yarn build:clean && yarn build-photon && cross-env NODE_ENV=production L10N=1 webpack", "build-l10n-prod": "yarn build-l10n-prod:quiet --progress", "build-photon": "webpack --config res/photon/webpack.config.js", + "build-symbolicator-cli": "yarn build-symbolicator-cli:quiet --progress", + "build-symbolicator-cli:quiet": "yarn build:clean && cross-env NODE_ENV=production webpack --config src/symbolicator-cli/webpack.config.js", "lint": "node bin/output-fixing-commands.js run-p lint-js lint-css prettier-run", "lint-fix": "run-p lint-fix-js lint-fix-css prettier-fix", "lint-js": "node bin/output-fixing-commands.js eslint *.js bin src --report-unused-disable-directives --cache --cache-strategy content", @@ -78,6 +80,7 @@ "jszip": "^3.10.1", "memoize-immutable": "^3.0.0", "memoize-one": "^6.0.0", + "minimist": "^1.2.8", "mixedtuplemap": "^1.0.0", "namedtuplemap": "^1.0.0", "photon-colors": "^3.3.2", diff --git a/src/profile-logic/symbol-store.js b/src/profile-logic/symbol-store.js index 28d203a1cc..b35c6cfd87 100644 --- a/src/profile-logic/symbol-store.js +++ b/src/profile-logic/symbol-store.js @@ -6,7 +6,7 @@ import SymbolStoreDB from './symbol-store-db'; import { SymbolsNotFoundError } from './errors'; -import type { RequestedLib } from 'firefox-profiler/types'; +import type { RequestedLib, ISymbolStoreDB } from 'firefox-profiler/types'; import type { SymbolTableAsTuple } from './symbol-store-db'; import { ensureExists } from '../utils/flow'; @@ -226,11 +226,18 @@ async function _getDemangleCallback(): Promise { */ export class SymbolStore { _symbolProvider: SymbolProvider; - _db: SymbolStoreDB; + _db: ISymbolStoreDB; - constructor(dbNamePrefix: string, symbolProvider: SymbolProvider) { + constructor( + dbNamePrefixOrDB: string | ISymbolStoreDB, + symbolProvider: SymbolProvider + ) { this._symbolProvider = symbolProvider; - this._db = new SymbolStoreDB(`${dbNamePrefix}-symbol-tables`); + if (typeof dbNamePrefixOrDB === 'string') { + this._db = new SymbolStoreDB(`${dbNamePrefixOrDB}-symbol-tables`); + } else { + this._db = dbNamePrefixOrDB; + } } async closeDb() { diff --git a/src/symbolicator-cli/index.js b/src/symbolicator-cli/index.js new file mode 100644 index 0000000000..4cf902c689 --- /dev/null +++ b/src/symbolicator-cli/index.js @@ -0,0 +1,205 @@ +// @flow + +/* + * This implements a simple CLI to symbolicate profiles captured by the profiler + * or by samply. + * + * To use it it first needs to be built: + * yarn build-symbolicator-cli + * + * Then it can be run from the `dist` directory: + * node dist/symbolicator-cli.js --input --output --server + * + * For example: + * node dist/symbolicator-cli.js --input samply-profile.json --output profile-symbolicated.json --server http://localhost:3000 + * + */ + +const fs = require('fs'); + +import { unserializeProfileOfArbitraryFormat } from '../profile-logic/process-profile'; +import { SymbolStore } from '../profile-logic/symbol-store'; +import { + symbolicateProfile, + applySymbolicationSteps, +} from '../profile-logic/symbolication'; +import type { SymbolicationStepInfo } from '../profile-logic/symbolication'; +import type { SymbolTableAsTuple } from '../profile-logic/symbol-store-db'; +import * as MozillaSymbolicationAPI from '../profile-logic/mozilla-symbolication-api'; +import { SymbolsNotFoundError } from '../profile-logic/errors'; +import type { ThreadIndex } from '../types'; + +/** + * Simple 'in-memory' symbol DB that conforms to the same interface as SymbolStoreDB but + * just stores everything in a simple dictionary instead of IndexedDB. The composite key + * [debugName, breakpadId] is flattened to a string "debugName:breakpadId" to use as the + * map key. + */ +export class InMemorySymbolDB { + _store: Map; + + constructor() { + this._store = new Map(); + } + + _makeKey(debugName: string, breakpadId: string): string { + return `${debugName}:${breakpadId}`; + } + + async storeSymbolTable( + debugName: string, + breakpadId: string, + symbolTable: SymbolTableAsTuple + ): Promise { + this._store.set(this._makeKey(debugName, breakpadId), symbolTable); + } + + async getSymbolTable( + debugName: string, + breakpadId: string + ): Promise { + const key = this._makeKey(debugName, breakpadId); + const value = this._store.get(key); + if (typeof value !== 'undefined') { + return value; + } + throw new SymbolsNotFoundError( + 'The requested library does not exist in the database.', + { debugName, breakpadId } + ); + } + + async close(): Promise {} +} + +interface CliOptions { + input: string; + output: string; + server: string; +} + +export async function run(options: CliOptions) { + console.log(`Loading profile from ${options.input}`); + const serializedProfile = JSON.parse(fs.readFileSync(options.input, 'utf8')); + const profile = await unserializeProfileOfArbitraryFormat(serializedProfile); + if (profile === undefined) { + throw new Error('Unable to parse the profile.'); + } + + const symbolStoreDB = new InMemorySymbolDB(); + + /** + * SymbolStore implementation which just forwards everything to the symbol server in + * MozillaSymbolicationAPI format. No support for getting symbols from 'the browser' as + * there is no browser in this context. + */ + const symbolStore = new SymbolStore(symbolStoreDB, { + requestSymbolsFromServer: async (requests) => { + for (const { lib } of requests) { + console.log(` Loading symbols for ${lib.debugName}`); + } + try { + return await MozillaSymbolicationAPI.requestSymbols( + 'symbol server', + requests, + async (path, json) => { + const response = await fetch(options.server + path, { + body: json, + method: 'POST', + }); + return response.json(); + } + ); + } catch (e) { + throw new Error( + `There was a problem with the symbolication API request to the symbol server: ${e.message}` + ); + } + }, + + requestSymbolsFromBrowser: async () => { + return []; + }, + + requestSymbolTableFromBrowser: async () => { + throw new Error('Not supported in this context'); + }, + }); + + console.log('Symbolicating...'); + + const symbolicationStepsPerThread: Map = + new Map(); + await symbolicateProfile( + profile, + symbolStore, + ( + threadIndex: ThreadIndex, + symbolicationStepInfo: SymbolicationStepInfo + ) => { + let threadSteps = symbolicationStepsPerThread.get(threadIndex); + if (threadSteps === undefined) { + threadSteps = []; + symbolicationStepsPerThread.set(threadIndex, threadSteps); + } + threadSteps.push(symbolicationStepInfo); + } + ); + + console.log('Applying collected symbolication steps...'); + + profile.threads = profile.threads.map((oldThread, threadIndex) => { + const symbolicationSteps = symbolicationStepsPerThread.get(threadIndex); + if (symbolicationSteps === undefined) { + return oldThread; + } + const { thread } = applySymbolicationSteps(oldThread, symbolicationSteps); + return thread; + }); + + profile.meta.symbolicated = true; + + console.log(`Saving profile to ${options.output}`); + fs.writeFileSync(options.output, JSON.stringify(profile)); + console.log('Finished.'); +} + +export function makeOptionsFromArgv(processArgv: string[]): CliOptions { + const argv = require('minimist')(processArgv.slice(2)); + + if (!('input' in argv && typeof argv.input === 'string')) { + throw new Error( + 'Argument --input must be supplied with the path to the input profile' + ); + } + + if (!('output' in argv && typeof argv.output === 'string')) { + throw new Error( + 'Argument --output must be supplied with the path to the output profile' + ); + } + + if (!('server' in argv && typeof argv.server === 'string')) { + throw new Error( + 'Argument --server must be supplied with the URI of the symbol server endpoint' + ); + } + + return { + input: argv.input, + output: argv.output, + server: argv.server, + }; +} + +if (!module.parent) { + try { + const options = makeOptionsFromArgv(process.argv); + run(options).catch((err) => { + throw err; + }); + } catch (e) { + console.error(e); + process.exit(1); + } +} diff --git a/src/symbolicator-cli/webpack.config.js b/src/symbolicator-cli/webpack.config.js new file mode 100644 index 0000000000..bb6b052c46 --- /dev/null +++ b/src/symbolicator-cli/webpack.config.js @@ -0,0 +1,28 @@ +// @noflow +const path = require('path'); +const projectRoot = path.join(__dirname, '../..'); +const includes = [path.join(projectRoot, 'src')]; + +module.exports = { + name: 'symbolicator-cli', + target: 'node', + mode: process.env.NODE_ENV, + output: { + path: path.resolve(projectRoot, 'dist'), + filename: 'symbolicator-cli.js', + }, + entry: './src/symbolicator-cli/index.js', + module: { + rules: [ + { + test: /\.js$/, + use: ['babel-loader'], + include: includes, + }, + ], + }, + experiments: { + // Make WebAssembly work just like in webpack v4 + syncWebAssembly: true, + }, +}; diff --git a/src/test/README.md b/src/test/README.md index 933227ad51..5f0fa922d6 100644 --- a/src/test/README.md +++ b/src/test/README.md @@ -25,9 +25,10 @@ Flow type tests are a little different, because they do not use Jest. Instead, t ## The tests -| Test type | Description | -| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | -| [components](./components) | Tests for React components, utilizing Enzyme for full behavioral testing, and snapshot tests to ensure that components output correct markup. | -| [store](./store) | Testing the [Redux](http://redux.js.org/) store using actions and selectors. | -| [types](./types) | Flow type tests. | -| [unit](./unit) | Unit testing | +| Test type | Description | +| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | +| [components](./components) | Tests for React components, utilizing Enzyme for full behavioral testing, and snapshot tests to ensure that components output correct markup. | +| [store](./store) | Testing the [Redux](http://redux.js.org/) store using actions and selectors. | +| [types](./types) | Flow type tests. | +| [unit](./unit) | Unit testing | +| [integration](./integration) | Integration testing | diff --git a/src/test/integration/symbolicator-cli/symbol-server-response.json b/src/test/integration/symbolicator-cli/symbol-server-response.json new file mode 100644 index 0000000000..bfeeec7b0c --- /dev/null +++ b/src/test/integration/symbolicator-cli/symbol-server-response.json @@ -0,0 +1,178 @@ +{ + "results": [ + { + "stacks": [ + [ + { + "frame": 0, + "module_offset": "0x6153", + "module": "dyld", + "function": "start", + "function_offset": "0x9ab", + "function_size": "0xae4" + } + ] + ], + "found_modules": { + "dyld/F635824E318B3F0C842CC369737F2B680": true + } + }, + { + "stacks": [ + [ + { + "frame": 0, + "module_offset": "0x3ec3", + "module": "a.out", + "function": "main", + "function_offset": "0xef", + "function_size": "0x17c" + }, + { + "frame": 1, + "module_offset": "0x3db3", + "module": "a.out", + "function": "threadfunc(void*)", + "function_offset": "0x2b", + "function_size": "0x4c" + }, + { + "frame": 2, + "module_offset": "0x3d33", + "module": "a.out", + "function": "fac(unsigned long)", + "function_offset": "0x17", + "function_size": "0x6c" + }, + { + "frame": 3, + "module_offset": "0x3d67", + "module": "a.out", + "function": "fac(unsigned long)", + "function_offset": "0x4b", + "function_size": "0x6c" + } + ] + ], + "found_modules": { + "a.out/F61DA4D57CBB38CA8BDF059C645834520": true + } + }, + { + "stacks": [ + [ + { + "frame": 0, + "module_offset": "0x948b", + "module": "libsystem_pthread.dylib", + "function": "_pthread_join", + "function_offset": "0x25f", + "function_size": "0x404" + }, + { + "frame": 1, + "module_offset": "0x6f93", + "module": "libsystem_pthread.dylib", + "function": "_pthread_start", + "function_offset": "0x87", + "function_size": "0x140" + }, + { + "frame": 2, + "module_offset": "0x6f9f", + "module": "libsystem_pthread.dylib", + "function": "_pthread_start", + "function_offset": "0x93", + "function_size": "0x140" + }, + { + "frame": 3, + "module_offset": "0x769f", + "module": "libsystem_pthread.dylib", + "function": "_pthread_exit", + "function_offset": "0x6f", + "function_size": "0x78" + }, + { + "frame": 4, + "module_offset": "0x4983", + "module": "libsystem_pthread.dylib", + "function": "_pthread_terminate_invoke", + "function_offset": "0x4f", + "function_size": "0x5c" + } + ] + ], + "found_modules": { + "libsystem_pthread.dylib/E03E84786F5C3D21A79A58408F5140000": true + } + }, + { + "stacks": [ + [ + { + "frame": 0, + "module_offset": "0x2bac", + "module": "libsystem_kernel.dylib", + "function": "__ulock_wait", + "function_offset": "0x8", + "function_size": "0x2c" + }, + { + "frame": 1, + "module_offset": "0x43e8", + "module": "libsystem_kernel.dylib", + "function": "__semwait_signal", + "function_offset": "0x8", + "function_size": "0x2c" + } + ] + ], + "found_modules": { + "libsystem_kernel.dylib/71FF45B8F14E36669E966CF58315B91D0": true + } + }, + { + "stacks": [ + [ + { + "frame": 0, + "module_offset": "0xd47f", + "module": "libsystem_c.dylib", + "function": "usleep", + "function_offset": "0x43", + "function_size": "0x50" + }, + { + "frame": 1, + "module_offset": "0xd567", + "module": "libsystem_c.dylib", + "function": "nanosleep", + "function_offset": "0xdb", + "function_size": "0x1d0" + } + ] + ], + "found_modules": { + "libsystem_c.dylib/D30F183093D03D0B8CBA9544E84BFD5B0": true + } + }, + { + "stacks": [ + [ + { + "frame": 0, + "module_offset": "0x3f3c", + "module": "libsystem_platform.dylib", + "function": "_platform_memset", + "function_offset": "0x6c", + "function_size": "0xd4" + } + ] + ], + "found_modules": { + "libsystem_platform.dylib/B4BF9F8931D737428CE7AB3554F9F5250": true + } + } + ] +} diff --git a/src/test/integration/symbolicator-cli/symbolicated.json b/src/test/integration/symbolicator-cli/symbolicated.json new file mode 100644 index 0000000000..e117c2f0ab --- /dev/null +++ b/src/test/integration/symbolicator-cli/symbolicated.json @@ -0,0 +1,858 @@ +{ + "meta": { + "categories": [ + { "name": "Other", "color": "grey", "subcategories": ["Other"] }, + { "name": "User", "color": "yellow", "subcategories": ["Other"] } + ], + "debug": false, + "extensions": { "baseURL": [], "id": [], "length": 0, "name": [] }, + "interval": 1, + "preprocessedProfileVersion": 50, + "processType": 0, + "product": "a.out", + "oscpu": "macOS 14.6.1", + "sampleUnits": { "eventDelay": "ms", "threadCPUDelta": "µs", "time": "ms" }, + "startTime": 1726433495880.2869, + "symbolicated": true, + "pausedRanges": [], + "version": 24, + "usesOnlyOneStackType": true, + "doesNotUseFrameImplementation": true, + "sourceCodeIsNotOnSearchfox": true, + "markerSchema": [] + }, + "libs": [ + { + "name": "dyld", + "path": "/usr/lib/dyld", + "debugName": "dyld", + "debugPath": "/usr/lib/dyld", + "breakpadId": "F635824E318B3F0C842CC369737F2B680", + "codeId": "F635824E318B3F0C842CC369737F2B68", + "arch": "arm64e" + }, + { + "name": "a.out", + "path": "/usr/helloworld/a.out", + "debugName": "a.out", + "debugPath": "/usr/helloworld/a.out", + "breakpadId": "F61DA4D57CBB38CA8BDF059C645834520", + "codeId": "F61DA4D57CBB38CA8BDF059C64583452", + "arch": "arm64" + }, + { + "name": "libsystem_pthread.dylib", + "path": "/usr/lib/system/libsystem_pthread.dylib", + "debugName": "libsystem_pthread.dylib", + "debugPath": "/usr/lib/system/libsystem_pthread.dylib", + "breakpadId": "E03E84786F5C3D21A79A58408F5140000", + "codeId": "E03E84786F5C3D21A79A58408F514000", + "arch": "arm64e" + }, + { + "name": "libsystem_kernel.dylib", + "path": "/usr/lib/system/libsystem_kernel.dylib", + "debugName": "libsystem_kernel.dylib", + "debugPath": "/usr/lib/system/libsystem_kernel.dylib", + "breakpadId": "71FF45B8F14E36669E966CF58315B91D0", + "codeId": "71FF45B8F14E36669E966CF58315B91D", + "arch": "arm64e" + }, + { + "name": "libsystem_c.dylib", + "path": "/usr/lib/system/libsystem_c.dylib", + "debugName": "libsystem_c.dylib", + "debugPath": "/usr/lib/system/libsystem_c.dylib", + "breakpadId": "D30F183093D03D0B8CBA9544E84BFD5B0", + "codeId": "D30F183093D03D0B8CBA9544E84BFD5B", + "arch": "arm64e" + }, + { + "name": "libsystem_platform.dylib", + "path": "/usr/lib/system/libsystem_platform.dylib", + "debugName": "libsystem_platform.dylib", + "debugPath": "/usr/lib/system/libsystem_platform.dylib", + "breakpadId": "B4BF9F8931D737428CE7AB3554F9F5250", + "codeId": "B4BF9F8931D737428CE7AB3554F9F525", + "arch": "arm64e" + } + ], + "pages": [], + "profilerOverhead": [], + "counters": [], + "threads": [ + { + "frameTable": { + "address": [24915, 16067, 38027, 11180], + "inlineDepth": [0, 0, 0, 0], + "category": [1, 1, 1, 1], + "subcategory": [0, 0, 0, 0], + "func": [0, 1, 2, 3], + "nativeSymbol": [0, 1, 2, 3], + "innerWindowID": [null, null, null, null], + "implementation": [null, null, null, null], + "line": [null, null, null, null], + "column": [null, null, null, null], + "length": 4 + }, + "funcTable": { + "isJS": [false, false, false, false], + "relevantForJS": [false, false, false, false], + "name": [8, 9, 10, 11], + "resource": [0, 1, 2, 3], + "fileName": [null, null, null, null], + "lineNumber": [null, null, null, null], + "columnNumber": [null, null, null, null], + "length": 4 + }, + "markers": { + "length": 0, + "category": [], + "data": [], + "endTime": [], + "name": [], + "phase": [], + "startTime": [] + }, + "name": "a.out", + "isMainThread": true, + "nativeSymbols": { + "libIndex": [0, 1, 2, 3], + "address": [22440, 15828, 37420, 11172], + "name": [8, 9, 10, 11], + "functionSize": [2788, 380, 1028, 44], + "length": 4 + }, + "pausedRanges": [], + "pid": "56127", + "processName": "a.out", + "processShutdownTime": 300.814417, + "processStartupTime": 237.805292, + "processType": "default", + "registerTime": 237.805292, + "resourceTable": { + "length": 4, + "lib": [0, 1, 2, 3], + "name": [0, 2, 4, 6], + "host": [null, null, null, null], + "type": [1, 1, 1, 1] + }, + "stackTable": { + "length": 4, + "prefix": [null, 0, 1, 2], + "frame": [0, 1, 2, 3], + "category": [1, 1, 1, 1], + "subcategory": [0, 0, 0, 0] + }, + "tid": "6274156", + "unregisterTime": 300.814417, + "samples": { + "time": [ + 240.520375, 251.722709, 252.728334, 261.724667, 262.732625, + 263.723459, 274.768584, 275.7785, 286.765084, 287.756375, 299.7795 + ], + "length": 11, + "weightType": "samples", + "stack": [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3], + "weight": [1, 11, 1, 9, 1, 1, 11, 1, 11, 1, 12], + "threadCPUDelta": [11273, 0, 8, 0, 3, 4, 0, 30, 0, 21, 0] + }, + "stringTable": { + "_array": [ + "dyld", + "0x6153", + "a.out", + "0x3ec3", + "libsystem_pthread.dylib", + "0x948b", + "libsystem_kernel.dylib", + "0x2bac", + "start", + "main", + "_pthread_join", + "__ulock_wait" + ], + "_stringToIndex": {} + } + }, + { + "frameTable": { + "address": [28563, 15795, 15667, 54399, 54631, 17384], + "inlineDepth": [0, 0, 0, 0, 0, 0], + "category": [1, 1, 1, 1, 1, 1], + "subcategory": [0, 0, 0, 0, 0, 0], + "func": [0, 1, 2, 3, 4, 5], + "nativeSymbol": [2, 0, 1, 4, 5, 3], + "innerWindowID": [null, null, null, null, null, null], + "implementation": [null, null, null, null, null, null], + "line": [null, null, null, null, null, null], + "column": [null, null, null, null, null, null], + "length": 6 + }, + "funcTable": { + "isJS": [false, false, false, false, false, false], + "relevantForJS": [false, false, false, false, false, false], + "name": [12, 10, 11, 14, 15, 13], + "resource": [0, 1, 1, 2, 2, 3], + "fileName": [null, null, null, null, null, null], + "lineNumber": [null, null, null, null, null, null], + "columnNumber": [null, null, null, null, null, null], + "length": 6 + }, + "markers": { + "length": 0, + "category": [], + "data": [], + "endTime": [], + "name": [], + "phase": [], + "startTime": [] + }, + "name": "Thread <6274161>", + "isMainThread": false, + "nativeSymbols": { + "libIndex": [1, 1, 2, 3, 4, 4], + "address": [15752, 15644, 28428, 17376, 54332, 54412], + "name": [10, 11, 12, 13, 14, 15], + "functionSize": [76, 108, 320, 44, 80, 464], + "length": 6 + }, + "pausedRanges": [], + "pid": "56127", + "processName": "a.out", + "processShutdownTime": 300.814417, + "processStartupTime": 237.805292, + "processType": "default", + "registerTime": 240.520375, + "resourceTable": { + "length": 4, + "lib": [2, 1, 4, 3], + "name": [0, 2, 5, 8], + "host": [null, null, null, null], + "type": [1, 1, 1, 1] + }, + "stackTable": { + "length": 6, + "prefix": [null, 0, 1, 2, 3, 4], + "frame": [0, 1, 2, 3, 4, 5], + "category": [1, 1, 1, 1, 1, 1], + "subcategory": [0, 0, 0, 0, 0, 0] + }, + "tid": "6274161", + "unregisterTime": 252.728334, + "samples": { + "time": [240.520375, 251.722709], + "length": 2, + "weightType": "samples", + "stack": [5, 5], + "weight": [1, 11], + "threadCPUDelta": [7, 0] + }, + "stringTable": { + "_array": [ + "libsystem_pthread.dylib", + "0x6f93", + "a.out", + "0x3db3", + "0x3d33", + "libsystem_c.dylib", + "0xd47f", + "0xd567", + "libsystem_kernel.dylib", + "0x43e8", + "threadfunc(void*)", + "fac(unsigned long)", + "_pthread_start", + "__semwait_signal", + "usleep", + "nanosleep" + ], + "_stringToIndex": {} + } + }, + { + "frameTable": { + "address": [28563, 15795, 15667, 54399, 54631, 17384, 15719], + "inlineDepth": [0, 0, 0, 0, 0, 0, 0], + "category": [1, 1, 1, 1, 1, 1, 1], + "subcategory": [0, 0, 0, 0, 0, 0, 0], + "func": [0, 1, 2, 3, 4, 5, 2], + "nativeSymbol": [2, 0, 1, 4, 5, 3, 1], + "innerWindowID": [null, null, null, null, null, null, null], + "implementation": [null, null, null, null, null, null, null], + "line": [null, null, null, null, null, null, null], + "column": [null, null, null, null, null, null, null], + "length": 7 + }, + "funcTable": { + "isJS": [false, false, false, false, false, false, false], + "relevantForJS": [false, false, false, false, false, false, false], + "name": [13, 11, 12, 15, 16, 14, 10], + "resource": [0, 1, 1, 2, 2, 3, 1], + "fileName": [null, null, null, null, null, null, null], + "lineNumber": [null, null, null, null, null, null, null], + "columnNumber": [null, null, null, null, null, null, null], + "length": 7 + }, + "markers": { + "length": 0, + "category": [], + "data": [], + "endTime": [], + "name": [], + "phase": [], + "startTime": [] + }, + "name": "Thread <6274162>", + "isMainThread": false, + "nativeSymbols": { + "libIndex": [1, 1, 2, 3, 4, 4], + "address": [15752, 15644, 28428, 17376, 54332, 54412], + "name": [11, 12, 13, 14, 15, 16], + "functionSize": [76, 108, 320, 44, 80, 464], + "length": 6 + }, + "pausedRanges": [], + "pid": "56127", + "processName": "a.out", + "processShutdownTime": 300.814417, + "processStartupTime": 237.805292, + "processType": "default", + "registerTime": 240.520375, + "resourceTable": { + "length": 4, + "lib": [2, 1, 4, 3], + "name": [0, 2, 5, 8], + "host": [null, null, null, null], + "type": [1, 1, 1, 1] + }, + "stackTable": { + "length": 11, + "prefix": [null, 0, 1, 2, 3, 4, 1, 6, 7, 8, 9], + "frame": [0, 1, 2, 3, 4, 5, 6, 2, 3, 4, 5], + "category": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + "subcategory": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + }, + "tid": "6274162", + "unregisterTime": 262.732625, + "samples": { + "time": [240.520375, 251.722709, 252.728334, 261.724667], + "length": 4, + "weightType": "samples", + "stack": [5, 5, 10, 10], + "weight": [1, 11, 1, 9], + "threadCPUDelta": [3, 0, 5, 0] + }, + "stringTable": { + "_array": [ + "libsystem_pthread.dylib", + "0x6f93", + "a.out", + "0x3db3", + "0x3d33", + "libsystem_c.dylib", + "0xd47f", + "0xd567", + "libsystem_kernel.dylib", + "0x43e8", + "0x3d67", + "threadfunc(void*)", + "fac(unsigned long)", + "_pthread_start", + "__semwait_signal", + "usleep", + "nanosleep" + ], + "_stringToIndex": {} + } + }, + { + "frameTable": { + "address": [28563, 15795, 15667, 54399, 54631, 17384, 15719], + "inlineDepth": [0, 0, 0, 0, 0, 0, 0], + "category": [1, 1, 1, 1, 1, 1, 1], + "subcategory": [0, 0, 0, 0, 0, 0, 0], + "func": [0, 1, 2, 3, 4, 5, 2], + "nativeSymbol": [2, 0, 1, 4, 5, 3, 1], + "innerWindowID": [null, null, null, null, null, null, null], + "implementation": [null, null, null, null, null, null, null], + "line": [null, null, null, null, null, null, null], + "column": [null, null, null, null, null, null, null], + "length": 7 + }, + "funcTable": { + "isJS": [false, false, false, false, false, false, false], + "relevantForJS": [false, false, false, false, false, false, false], + "name": [13, 11, 12, 15, 16, 14, 10], + "resource": [0, 1, 1, 2, 2, 3, 1], + "fileName": [null, null, null, null, null, null, null], + "lineNumber": [null, null, null, null, null, null, null], + "columnNumber": [null, null, null, null, null, null, null], + "length": 7 + }, + "markers": { + "length": 0, + "category": [], + "data": [], + "endTime": [], + "name": [], + "phase": [], + "startTime": [] + }, + "name": "Thread <6274163>", + "isMainThread": false, + "nativeSymbols": { + "libIndex": [1, 1, 2, 3, 4, 4], + "address": [15752, 15644, 28428, 17376, 54332, 54412], + "name": [11, 12, 13, 14, 15, 16], + "functionSize": [76, 108, 320, 44, 80, 464], + "length": 6 + }, + "pausedRanges": [], + "pid": "56127", + "processName": "a.out", + "processShutdownTime": 300.814417, + "processStartupTime": 237.805292, + "processType": "default", + "registerTime": 240.520375, + "resourceTable": { + "length": 4, + "lib": [2, 1, 4, 3], + "name": [0, 2, 5, 8], + "host": [null, null, null, null], + "type": [1, 1, 1, 1] + }, + "stackTable": { + "length": 16, + "prefix": [null, 0, 1, 2, 3, 4, 1, 6, 7, 8, 9, 6, 11, 12, 13, 14], + "frame": [0, 1, 2, 3, 4, 5, 6, 2, 3, 4, 5, 6, 2, 3, 4, 5], + "category": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + "subcategory": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + }, + "tid": "6274163", + "unregisterTime": 275.7785, + "samples": { + "time": [ + 240.520375, 251.722709, 252.728334, 263.723459, 264.733042, 274.768584 + ], + "length": 6, + "weightType": "samples", + "stack": [5, 5, 10, 10, 15, 15], + "weight": [1, 11, 1, 11, 1, 10], + "threadCPUDelta": [1, 0, 5, 0, 3, 0] + }, + "stringTable": { + "_array": [ + "libsystem_pthread.dylib", + "0x6f93", + "a.out", + "0x3db3", + "0x3d33", + "libsystem_c.dylib", + "0xd47f", + "0xd567", + "libsystem_kernel.dylib", + "0x43e8", + "0x3d67", + "threadfunc(void*)", + "fac(unsigned long)", + "_pthread_start", + "__semwait_signal", + "usleep", + "nanosleep" + ], + "_stringToIndex": {} + } + }, + { + "frameTable": { + "address": [ + 28563, 15795, 15667, 54399, 54631, 17384, 15719, 28575, 30367, 18819, + 16188 + ], + "inlineDepth": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "category": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + "subcategory": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "func": [0, 1, 2, 3, 4, 5, 2, 0, 7, 8, 10], + "nativeSymbol": [2, 0, 1, 6, 7, 5, 1, 2, 3, 4, 8], + "innerWindowID": [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + "implementation": [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + "line": [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + "column": [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + "length": 11 + }, + "funcTable": { + "isJS": [ + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false + ], + "relevantForJS": [ + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false + ], + "name": [18, 16, 17, 22, 23, 21, 10, 19, 20, 13, 24], + "resource": [0, 1, 1, 2, 2, 3, 1, 0, 0, 0, 4], + "fileName": [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + "lineNumber": [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + "columnNumber": [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + "length": 11 + }, + "markers": { + "length": 0, + "category": [], + "data": [], + "endTime": [], + "name": [], + "phase": [], + "startTime": [] + }, + "name": "Thread <6274164>", + "isMainThread": false, + "nativeSymbols": { + "libIndex": [1, 1, 2, 2, 2, 3, 4, 4, 5], + "address": [ + 15752, 15644, 28428, 30256, 18740, 17376, 54332, 54412, 16080 + ], + "name": [16, 17, 18, 19, 20, 21, 22, 23, 24], + "functionSize": [76, 108, 320, 120, 92, 44, 80, 464, 212], + "length": 9 + }, + "pausedRanges": [], + "pid": "56127", + "processName": "a.out", + "processShutdownTime": 300.814417, + "processStartupTime": 237.805292, + "processType": "default", + "registerTime": 240.520375, + "resourceTable": { + "length": 5, + "lib": [2, 1, 4, 3, 5], + "name": [0, 2, 5, 8, 14], + "host": [null, null, null, null, null], + "type": [1, 1, 1, 1, 1] + }, + "stackTable": { + "length": 25, + "prefix": [ + null, + 0, + 1, + 2, + 3, + 4, + 1, + 6, + 7, + 8, + 9, + 6, + 11, + 12, + 13, + 14, + 11, + 16, + 17, + 18, + 19, + null, + 21, + 22, + 23 + ], + "frame": [ + 0, 1, 2, 3, 4, 5, 6, 2, 3, 4, 5, 6, 2, 3, 4, 5, 6, 2, 3, 4, 5, 7, 8, + 9, 10 + ], + "category": [ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1 + ], + "subcategory": [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0 + ] + }, + "tid": "6274164", + "unregisterTime": 287.756375, + "samples": { + "time": [ + 240.520375, 251.722709, 252.728334, 261.724667, 262.732625, 273.755, + 274.768584, 285.764042, 286.765084 + ], + "length": 9, + "weightType": "samples", + "stack": [5, 5, 10, 10, 15, 15, 20, 20, 24], + "weight": [1, 11, 1, 9, 1, 11, 1, 11, 1], + "threadCPUDelta": [6, 0, 9, 0, 20, 0, 6, 0, 2] + }, + "stringTable": { + "_array": [ + "libsystem_pthread.dylib", + "0x6f93", + "a.out", + "0x3db3", + "0x3d33", + "libsystem_c.dylib", + "0xd47f", + "0xd567", + "libsystem_kernel.dylib", + "0x43e8", + "0x3d67", + "0x6f9f", + "0x769f", + "0x4983", + "libsystem_platform.dylib", + "0x3f3c", + "threadfunc(void*)", + "fac(unsigned long)", + "_pthread_start", + "_pthread_exit", + "_pthread_terminate_invoke", + "__semwait_signal", + "usleep", + "nanosleep", + "_platform_memset" + ], + "_stringToIndex": {} + } + }, + { + "frameTable": { + "address": [28563, 15795, 15667, 54399, 54631, 17384, 15719], + "inlineDepth": [0, 0, 0, 0, 0, 0, 0], + "category": [1, 1, 1, 1, 1, 1, 1], + "subcategory": [0, 0, 0, 0, 0, 0, 0], + "func": [0, 1, 2, 3, 4, 5, 2], + "nativeSymbol": [2, 0, 1, 4, 5, 3, 1], + "innerWindowID": [null, null, null, null, null, null, null], + "implementation": [null, null, null, null, null, null, null], + "line": [null, null, null, null, null, null, null], + "column": [null, null, null, null, null, null, null], + "length": 7 + }, + "funcTable": { + "isJS": [false, false, false, false, false, false, false], + "relevantForJS": [false, false, false, false, false, false, false], + "name": [13, 11, 12, 15, 16, 14, 10], + "resource": [0, 1, 1, 2, 2, 3, 1], + "fileName": [null, null, null, null, null, null, null], + "lineNumber": [null, null, null, null, null, null, null], + "columnNumber": [null, null, null, null, null, null, null], + "length": 7 + }, + "markers": { + "length": 0, + "category": [], + "data": [], + "endTime": [], + "name": [], + "phase": [], + "startTime": [] + }, + "name": "Thread <6274165>", + "isMainThread": false, + "nativeSymbols": { + "libIndex": [1, 1, 2, 3, 4, 4], + "address": [15752, 15644, 28428, 17376, 54332, 54412], + "name": [11, 12, 13, 14, 15, 16], + "functionSize": [76, 108, 320, 44, 80, 464], + "length": 6 + }, + "pausedRanges": [], + "pid": "56127", + "processName": "a.out", + "processShutdownTime": 300.814417, + "processStartupTime": 237.805292, + "processType": "default", + "registerTime": 240.520375, + "resourceTable": { + "length": 4, + "lib": [2, 1, 4, 3], + "name": [0, 2, 5, 8], + "host": [null, null, null, null], + "type": [1, 1, 1, 1] + }, + "stackTable": { + "length": 26, + "prefix": [ + null, + 0, + 1, + 2, + 3, + 4, + 1, + 6, + 7, + 8, + 9, + 6, + 11, + 12, + 13, + 14, + 11, + 16, + 17, + 18, + 19, + 16, + 21, + 22, + 23, + 24 + ], + "frame": [ + 0, 1, 2, 3, 4, 5, 6, 2, 3, 4, 5, 6, 2, 3, 4, 5, 6, 2, 3, 4, 5, 6, 2, + 3, 4, 5 + ], + "category": [ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1 + ], + "subcategory": [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0 + ] + }, + "tid": "6274165", + "unregisterTime": 300.814417, + "samples": { + "time": [ + 240.520375, 251.722709, 252.728334, 263.723459, 264.733042, + 274.768584, 275.7785, 287.756375, 288.76475, 299.7795 + ], + "length": 10, + "weightType": "samples", + "stack": [5, 5, 10, 10, 15, 15, 20, 20, 25, 25], + "weight": [1, 11, 1, 11, 1, 10, 1, 12, 1, 11], + "threadCPUDelta": [2, 0, 6, 0, 4, 0, 5, 0, 4, 0] + }, + "stringTable": { + "_array": [ + "libsystem_pthread.dylib", + "0x6f93", + "a.out", + "0x3db3", + "0x3d33", + "libsystem_c.dylib", + "0xd47f", + "0xd567", + "libsystem_kernel.dylib", + "0x43e8", + "0x3d67", + "threadfunc(void*)", + "fac(unsigned long)", + "_pthread_start", + "__semwait_signal", + "usleep", + "nanosleep" + ], + "_stringToIndex": {} + } + } + ] +} diff --git a/src/test/integration/symbolicator-cli/symbolicator-cli.test.js b/src/test/integration/symbolicator-cli/symbolicator-cli.test.js new file mode 100644 index 0000000000..b289dc15d4 --- /dev/null +++ b/src/test/integration/symbolicator-cli/symbolicator-cli.test.js @@ -0,0 +1,58 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +// @flow + +import fs from 'fs'; +import os from 'os'; +import path from 'path'; +import { run } from '../../../symbolicator-cli'; + +describe('symbolicator-cli tool', function () { + async function runToTempFileAndReturnOutput(options) { + const tempDir = fs.mkdtempSync( + path.join(os.tmpdir(), 'symbolicator-cli-test') + ); + const tempFile = path.join(tempDir, 'temp.json'); + options.output = tempFile; + + try { + await run(options); + return JSON.parse(fs.readFileSync(tempFile, 'utf-8')); + } finally { + // $FlowExpectError Flow doesn't know about the rmSync API despite it's been implemented in node v16. Sigh + fs.rmSync(tempDir, { recursive: true, force: true }); + } + } + + it('is symbolicating a trace correctly', async function () { + jest.spyOn(console, 'log').mockImplementation(() => {}); + jest.spyOn(console, 'warn').mockImplementation(() => {}); + + const symbolsJson = fs.readFileSync( + 'src/test/integration/symbolicator-cli/symbol-server-response.json' + ); + const expected = JSON.parse( + fs.readFileSync( + 'src/test/integration/symbolicator-cli/symbolicated.json', + 'utf-8' + ) + ); + + window.fetch.post( + 'http://symbol.server/symbolicate/v5', + new Response(symbolsJson) + ); + + const options = { + input: 'src/test/integration/symbolicator-cli/unsymbolicated.json', + output: '', + server: 'http://symbol.server', + }; + + const result = await runToTempFileAndReturnOutput(options); + + expect(console.warn).not.toHaveBeenCalled(); + expect(result).toEqual(expected); + }); +}); diff --git a/src/test/integration/symbolicator-cli/unsymbolicated.json b/src/test/integration/symbolicator-cli/unsymbolicated.json new file mode 100644 index 0000000000..143f065b20 --- /dev/null +++ b/src/test/integration/symbolicator-cli/unsymbolicated.json @@ -0,0 +1,813 @@ +{ + "meta": { + "categories": [ + { "name": "Other", "color": "grey", "subcategories": ["Other"] }, + { "name": "User", "color": "yellow", "subcategories": ["Other"] } + ], + "debug": false, + "extensions": { "baseURL": [], "id": [], "length": 0, "name": [] }, + "interval": 1.0, + "preprocessedProfileVersion": 49, + "processType": 0, + "product": "a.out", + "oscpu": "macOS 14.6.1", + "sampleUnits": { "eventDelay": "ms", "threadCPUDelta": "µs", "time": "ms" }, + "startTime": 1726433495880.2869, + "symbolicated": false, + "pausedRanges": [], + "version": 24, + "usesOnlyOneStackType": true, + "doesNotUseFrameImplementation": true, + "sourceCodeIsNotOnSearchfox": true, + "markerSchema": [] + }, + "libs": [ + { + "name": "dyld", + "path": "/usr/lib/dyld", + "debugName": "dyld", + "debugPath": "/usr/lib/dyld", + "breakpadId": "F635824E318B3F0C842CC369737F2B680", + "codeId": "F635824E318B3F0C842CC369737F2B68", + "arch": "arm64e" + }, + { + "name": "a.out", + "path": "/usr/helloworld/a.out", + "debugName": "a.out", + "debugPath": "/usr/helloworld/a.out", + "breakpadId": "F61DA4D57CBB38CA8BDF059C645834520", + "codeId": "F61DA4D57CBB38CA8BDF059C64583452", + "arch": "arm64" + }, + { + "name": "libsystem_pthread.dylib", + "path": "/usr/lib/system/libsystem_pthread.dylib", + "debugName": "libsystem_pthread.dylib", + "debugPath": "/usr/lib/system/libsystem_pthread.dylib", + "breakpadId": "E03E84786F5C3D21A79A58408F5140000", + "codeId": "E03E84786F5C3D21A79A58408F514000", + "arch": "arm64e" + }, + { + "name": "libsystem_kernel.dylib", + "path": "/usr/lib/system/libsystem_kernel.dylib", + "debugName": "libsystem_kernel.dylib", + "debugPath": "/usr/lib/system/libsystem_kernel.dylib", + "breakpadId": "71FF45B8F14E36669E966CF58315B91D0", + "codeId": "71FF45B8F14E36669E966CF58315B91D", + "arch": "arm64e" + }, + { + "name": "libsystem_c.dylib", + "path": "/usr/lib/system/libsystem_c.dylib", + "debugName": "libsystem_c.dylib", + "debugPath": "/usr/lib/system/libsystem_c.dylib", + "breakpadId": "D30F183093D03D0B8CBA9544E84BFD5B0", + "codeId": "D30F183093D03D0B8CBA9544E84BFD5B", + "arch": "arm64e" + }, + { + "name": "libsystem_platform.dylib", + "path": "/usr/lib/system/libsystem_platform.dylib", + "debugName": "libsystem_platform.dylib", + "debugPath": "/usr/lib/system/libsystem_platform.dylib", + "breakpadId": "B4BF9F8931D737428CE7AB3554F9F5250", + "codeId": "B4BF9F8931D737428CE7AB3554F9F525", + "arch": "arm64e" + } + ], + "threads": [ + { + "frameTable": { + "length": 4, + "address": [24915, 16067, 38027, 11180], + "inlineDepth": [0, 0, 0, 0], + "category": [1, 1, 1, 1], + "subcategory": [0, 0, 0, 0], + "func": [0, 1, 2, 3], + "nativeSymbol": [null, null, null, null], + "innerWindowID": [null, null, null, null], + "implementation": [null, null, null, null], + "line": [null, null, null, null], + "column": [null, null, null, null] + }, + "funcTable": { + "length": 4, + "name": [1, 3, 5, 7], + "isJS": [false, false, false, false], + "relevantForJS": [false, false, false, false], + "resource": [0, 1, 2, 3], + "fileName": [null, null, null, null], + "lineNumber": [null, null, null, null], + "columnNumber": [null, null, null, null] + }, + "markers": { + "length": 0, + "category": [], + "data": [], + "endTime": [], + "name": [], + "phase": [], + "startTime": [] + }, + "name": "a.out", + "isMainThread": true, + "nativeSymbols": { + "length": 0, + "address": [], + "functionSize": [], + "libIndex": [], + "name": [] + }, + "pausedRanges": [], + "pid": "56127", + "processName": "a.out", + "processShutdownTime": 300.814417, + "processStartupTime": 237.805292, + "processType": "default", + "registerTime": 237.805292, + "resourceTable": { + "length": 4, + "lib": [0, 1, 2, 3], + "name": [0, 2, 4, 6], + "host": [null, null, null, null], + "type": [1, 1, 1, 1] + }, + "samples": { + "length": 11, + "weightType": "samples", + "stack": [3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3], + "time": [ + 240.520375, 251.722709, 252.728334, 261.724667, 262.732625, + 263.723459, 274.768584, 275.7785, 286.765084, 287.756375, 299.7795 + ], + "weight": [1, 11, 1, 9, 1, 1, 11, 1, 11, 1, 12], + "threadCPUDelta": [11273, 0, 8, 0, 3, 4, 0, 30, 0, 21, 0] + }, + "stackTable": { + "length": 4, + "prefix": [null, 0, 1, 2], + "frame": [0, 1, 2, 3], + "category": [1, 1, 1, 1], + "subcategory": [0, 0, 0, 0] + }, + "stringArray": [ + "dyld", + "0x6153", + "a.out", + "0x3ec3", + "libsystem_pthread.dylib", + "0x948b", + "libsystem_kernel.dylib", + "0x2bac" + ], + "tid": "6274156", + "unregisterTime": 300.814417 + }, + { + "frameTable": { + "length": 6, + "address": [28563, 15795, 15667, 54399, 54631, 17384], + "inlineDepth": [0, 0, 0, 0, 0, 0], + "category": [1, 1, 1, 1, 1, 1], + "subcategory": [0, 0, 0, 0, 0, 0], + "func": [0, 1, 2, 3, 4, 5], + "nativeSymbol": [null, null, null, null, null, null], + "innerWindowID": [null, null, null, null, null, null], + "implementation": [null, null, null, null, null, null], + "line": [null, null, null, null, null, null], + "column": [null, null, null, null, null, null] + }, + "funcTable": { + "length": 6, + "name": [1, 3, 4, 6, 7, 9], + "isJS": [false, false, false, false, false, false], + "relevantForJS": [false, false, false, false, false, false], + "resource": [0, 1, 1, 2, 2, 3], + "fileName": [null, null, null, null, null, null], + "lineNumber": [null, null, null, null, null, null], + "columnNumber": [null, null, null, null, null, null] + }, + "markers": { + "length": 0, + "category": [], + "data": [], + "endTime": [], + "name": [], + "phase": [], + "startTime": [] + }, + "name": "Thread <6274161>", + "isMainThread": false, + "nativeSymbols": { + "length": 0, + "address": [], + "functionSize": [], + "libIndex": [], + "name": [] + }, + "pausedRanges": [], + "pid": "56127", + "processName": "a.out", + "processShutdownTime": 300.814417, + "processStartupTime": 237.805292, + "processType": "default", + "registerTime": 240.520375, + "resourceTable": { + "length": 4, + "lib": [2, 1, 4, 3], + "name": [0, 2, 5, 8], + "host": [null, null, null, null], + "type": [1, 1, 1, 1] + }, + "samples": { + "length": 2, + "weightType": "samples", + "stack": [5, 5], + "time": [240.520375, 251.722709], + "weight": [1, 11], + "threadCPUDelta": [7, 0] + }, + "stackTable": { + "length": 6, + "prefix": [null, 0, 1, 2, 3, 4], + "frame": [0, 1, 2, 3, 4, 5], + "category": [1, 1, 1, 1, 1, 1], + "subcategory": [0, 0, 0, 0, 0, 0] + }, + "stringArray": [ + "libsystem_pthread.dylib", + "0x6f93", + "a.out", + "0x3db3", + "0x3d33", + "libsystem_c.dylib", + "0xd47f", + "0xd567", + "libsystem_kernel.dylib", + "0x43e8" + ], + "tid": "6274161", + "unregisterTime": 252.728334 + }, + { + "frameTable": { + "length": 7, + "address": [28563, 15795, 15667, 54399, 54631, 17384, 15719], + "inlineDepth": [0, 0, 0, 0, 0, 0, 0], + "category": [1, 1, 1, 1, 1, 1, 1], + "subcategory": [0, 0, 0, 0, 0, 0, 0], + "func": [0, 1, 2, 3, 4, 5, 6], + "nativeSymbol": [null, null, null, null, null, null, null], + "innerWindowID": [null, null, null, null, null, null, null], + "implementation": [null, null, null, null, null, null, null], + "line": [null, null, null, null, null, null, null], + "column": [null, null, null, null, null, null, null] + }, + "funcTable": { + "length": 7, + "name": [1, 3, 4, 6, 7, 9, 10], + "isJS": [false, false, false, false, false, false, false], + "relevantForJS": [false, false, false, false, false, false, false], + "resource": [0, 1, 1, 2, 2, 3, 1], + "fileName": [null, null, null, null, null, null, null], + "lineNumber": [null, null, null, null, null, null, null], + "columnNumber": [null, null, null, null, null, null, null] + }, + "markers": { + "length": 0, + "category": [], + "data": [], + "endTime": [], + "name": [], + "phase": [], + "startTime": [] + }, + "name": "Thread <6274162>", + "isMainThread": false, + "nativeSymbols": { + "length": 0, + "address": [], + "functionSize": [], + "libIndex": [], + "name": [] + }, + "pausedRanges": [], + "pid": "56127", + "processName": "a.out", + "processShutdownTime": 300.814417, + "processStartupTime": 237.805292, + "processType": "default", + "registerTime": 240.520375, + "resourceTable": { + "length": 4, + "lib": [2, 1, 4, 3], + "name": [0, 2, 5, 8], + "host": [null, null, null, null], + "type": [1, 1, 1, 1] + }, + "samples": { + "length": 4, + "weightType": "samples", + "stack": [5, 5, 10, 10], + "time": [240.520375, 251.722709, 252.728334, 261.724667], + "weight": [1, 11, 1, 9], + "threadCPUDelta": [3, 0, 5, 0] + }, + "stackTable": { + "length": 11, + "prefix": [null, 0, 1, 2, 3, 4, 1, 6, 7, 8, 9], + "frame": [0, 1, 2, 3, 4, 5, 6, 2, 3, 4, 5], + "category": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + "subcategory": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + }, + "stringArray": [ + "libsystem_pthread.dylib", + "0x6f93", + "a.out", + "0x3db3", + "0x3d33", + "libsystem_c.dylib", + "0xd47f", + "0xd567", + "libsystem_kernel.dylib", + "0x43e8", + "0x3d67" + ], + "tid": "6274162", + "unregisterTime": 262.732625 + }, + { + "frameTable": { + "length": 7, + "address": [28563, 15795, 15667, 54399, 54631, 17384, 15719], + "inlineDepth": [0, 0, 0, 0, 0, 0, 0], + "category": [1, 1, 1, 1, 1, 1, 1], + "subcategory": [0, 0, 0, 0, 0, 0, 0], + "func": [0, 1, 2, 3, 4, 5, 6], + "nativeSymbol": [null, null, null, null, null, null, null], + "innerWindowID": [null, null, null, null, null, null, null], + "implementation": [null, null, null, null, null, null, null], + "line": [null, null, null, null, null, null, null], + "column": [null, null, null, null, null, null, null] + }, + "funcTable": { + "length": 7, + "name": [1, 3, 4, 6, 7, 9, 10], + "isJS": [false, false, false, false, false, false, false], + "relevantForJS": [false, false, false, false, false, false, false], + "resource": [0, 1, 1, 2, 2, 3, 1], + "fileName": [null, null, null, null, null, null, null], + "lineNumber": [null, null, null, null, null, null, null], + "columnNumber": [null, null, null, null, null, null, null] + }, + "markers": { + "length": 0, + "category": [], + "data": [], + "endTime": [], + "name": [], + "phase": [], + "startTime": [] + }, + "name": "Thread <6274163>", + "isMainThread": false, + "nativeSymbols": { + "length": 0, + "address": [], + "functionSize": [], + "libIndex": [], + "name": [] + }, + "pausedRanges": [], + "pid": "56127", + "processName": "a.out", + "processShutdownTime": 300.814417, + "processStartupTime": 237.805292, + "processType": "default", + "registerTime": 240.520375, + "resourceTable": { + "length": 4, + "lib": [2, 1, 4, 3], + "name": [0, 2, 5, 8], + "host": [null, null, null, null], + "type": [1, 1, 1, 1] + }, + "samples": { + "length": 6, + "weightType": "samples", + "stack": [5, 5, 10, 10, 15, 15], + "time": [ + 240.520375, 251.722709, 252.728334, 263.723459, 264.733042, 274.768584 + ], + "weight": [1, 11, 1, 11, 1, 10], + "threadCPUDelta": [1, 0, 5, 0, 3, 0] + }, + "stackTable": { + "length": 16, + "prefix": [null, 0, 1, 2, 3, 4, 1, 6, 7, 8, 9, 6, 11, 12, 13, 14], + "frame": [0, 1, 2, 3, 4, 5, 6, 2, 3, 4, 5, 6, 2, 3, 4, 5], + "category": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + "subcategory": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + }, + "stringArray": [ + "libsystem_pthread.dylib", + "0x6f93", + "a.out", + "0x3db3", + "0x3d33", + "libsystem_c.dylib", + "0xd47f", + "0xd567", + "libsystem_kernel.dylib", + "0x43e8", + "0x3d67" + ], + "tid": "6274163", + "unregisterTime": 275.7785 + }, + { + "frameTable": { + "length": 11, + "address": [ + 28563, 15795, 15667, 54399, 54631, 17384, 15719, 28575, 30367, 18819, + 16188 + ], + "inlineDepth": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "category": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + "subcategory": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "func": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + "nativeSymbol": [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + "innerWindowID": [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + "implementation": [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + "line": [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + "column": [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ] + }, + "funcTable": { + "length": 11, + "name": [1, 3, 4, 6, 7, 9, 10, 11, 12, 13, 15], + "isJS": [ + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false + ], + "relevantForJS": [ + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false + ], + "resource": [0, 1, 1, 2, 2, 3, 1, 0, 0, 0, 4], + "fileName": [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + "lineNumber": [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ], + "columnNumber": [ + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null + ] + }, + "markers": { + "length": 0, + "category": [], + "data": [], + "endTime": [], + "name": [], + "phase": [], + "startTime": [] + }, + "name": "Thread <6274164>", + "isMainThread": false, + "nativeSymbols": { + "length": 0, + "address": [], + "functionSize": [], + "libIndex": [], + "name": [] + }, + "pausedRanges": [], + "pid": "56127", + "processName": "a.out", + "processShutdownTime": 300.814417, + "processStartupTime": 237.805292, + "processType": "default", + "registerTime": 240.520375, + "resourceTable": { + "length": 5, + "lib": [2, 1, 4, 3, 5], + "name": [0, 2, 5, 8, 14], + "host": [null, null, null, null, null], + "type": [1, 1, 1, 1, 1] + }, + "samples": { + "length": 9, + "weightType": "samples", + "stack": [5, 5, 10, 10, 15, 15, 20, 20, 24], + "time": [ + 240.520375, 251.722709, 252.728334, 261.724667, 262.732625, 273.755, + 274.768584, 285.764042, 286.765084 + ], + "weight": [1, 11, 1, 9, 1, 11, 1, 11, 1], + "threadCPUDelta": [6, 0, 9, 0, 20, 0, 6, 0, 2] + }, + "stackTable": { + "length": 25, + "prefix": [ + null, + 0, + 1, + 2, + 3, + 4, + 1, + 6, + 7, + 8, + 9, + 6, + 11, + 12, + 13, + 14, + 11, + 16, + 17, + 18, + 19, + null, + 21, + 22, + 23 + ], + "frame": [ + 0, 1, 2, 3, 4, 5, 6, 2, 3, 4, 5, 6, 2, 3, 4, 5, 6, 2, 3, 4, 5, 7, 8, + 9, 10 + ], + "category": [ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1 + ], + "subcategory": [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0 + ] + }, + "stringArray": [ + "libsystem_pthread.dylib", + "0x6f93", + "a.out", + "0x3db3", + "0x3d33", + "libsystem_c.dylib", + "0xd47f", + "0xd567", + "libsystem_kernel.dylib", + "0x43e8", + "0x3d67", + "0x6f9f", + "0x769f", + "0x4983", + "libsystem_platform.dylib", + "0x3f3c" + ], + "tid": "6274164", + "unregisterTime": 287.756375 + }, + { + "frameTable": { + "length": 7, + "address": [28563, 15795, 15667, 54399, 54631, 17384, 15719], + "inlineDepth": [0, 0, 0, 0, 0, 0, 0], + "category": [1, 1, 1, 1, 1, 1, 1], + "subcategory": [0, 0, 0, 0, 0, 0, 0], + "func": [0, 1, 2, 3, 4, 5, 6], + "nativeSymbol": [null, null, null, null, null, null, null], + "innerWindowID": [null, null, null, null, null, null, null], + "implementation": [null, null, null, null, null, null, null], + "line": [null, null, null, null, null, null, null], + "column": [null, null, null, null, null, null, null] + }, + "funcTable": { + "length": 7, + "name": [1, 3, 4, 6, 7, 9, 10], + "isJS": [false, false, false, false, false, false, false], + "relevantForJS": [false, false, false, false, false, false, false], + "resource": [0, 1, 1, 2, 2, 3, 1], + "fileName": [null, null, null, null, null, null, null], + "lineNumber": [null, null, null, null, null, null, null], + "columnNumber": [null, null, null, null, null, null, null] + }, + "markers": { + "length": 0, + "category": [], + "data": [], + "endTime": [], + "name": [], + "phase": [], + "startTime": [] + }, + "name": "Thread <6274165>", + "isMainThread": false, + "nativeSymbols": { + "length": 0, + "address": [], + "functionSize": [], + "libIndex": [], + "name": [] + }, + "pausedRanges": [], + "pid": "56127", + "processName": "a.out", + "processShutdownTime": 300.814417, + "processStartupTime": 237.805292, + "processType": "default", + "registerTime": 240.520375, + "resourceTable": { + "length": 4, + "lib": [2, 1, 4, 3], + "name": [0, 2, 5, 8], + "host": [null, null, null, null], + "type": [1, 1, 1, 1] + }, + "samples": { + "length": 10, + "weightType": "samples", + "stack": [5, 5, 10, 10, 15, 15, 20, 20, 25, 25], + "time": [ + 240.520375, 251.722709, 252.728334, 263.723459, 264.733042, + 274.768584, 275.7785, 287.756375, 288.76475, 299.7795 + ], + "weight": [1, 11, 1, 11, 1, 10, 1, 12, 1, 11], + "threadCPUDelta": [2, 0, 6, 0, 4, 0, 5, 0, 4, 0] + }, + "stackTable": { + "length": 26, + "prefix": [ + null, + 0, + 1, + 2, + 3, + 4, + 1, + 6, + 7, + 8, + 9, + 6, + 11, + 12, + 13, + 14, + 11, + 16, + 17, + 18, + 19, + 16, + 21, + 22, + 23, + 24 + ], + "frame": [ + 0, 1, 2, 3, 4, 5, 6, 2, 3, 4, 5, 6, 2, 3, 4, 5, 6, 2, 3, 4, 5, 6, 2, + 3, 4, 5 + ], + "category": [ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1 + ], + "subcategory": [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0 + ] + }, + "stringArray": [ + "libsystem_pthread.dylib", + "0x6f93", + "a.out", + "0x3db3", + "0x3d33", + "libsystem_c.dylib", + "0xd47f", + "0xd567", + "libsystem_kernel.dylib", + "0x43e8", + "0x3d67" + ], + "tid": "6274165", + "unregisterTime": 300.814417 + } + ], + "pages": [], + "profilerOverhead": [], + "counters": [] +} diff --git a/src/test/unit/symbolicator-cli.test.js b/src/test/unit/symbolicator-cli.test.js new file mode 100644 index 0000000000..1d47b69fdf --- /dev/null +++ b/src/test/unit/symbolicator-cli.test.js @@ -0,0 +1,129 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +// @flow + +import { InMemorySymbolDB, makeOptionsFromArgv } from '../../symbolicator-cli'; +import { completeSymbolTableAsTuple } from '../fixtures/example-symbol-table'; +import { SymbolsNotFoundError } from '../../profile-logic/errors'; + +describe('makeOptionsFromArgv', function () { + const commonArgs = ['/path/to/node', '/path/to/symbolicator-cli.js']; + + it('should pass arguments into options object', function () { + const options = makeOptionsFromArgv([ + ...commonArgs, + '--input', + '/path/to/input', + '--output', + '/path/to/output', + '--server', + 'http://symbol.server/', + ]); + + expect(options.input).toEqual('/path/to/input'); + expect(options.output).toEqual('/path/to/output'); + expect(options.server).toEqual('http://symbol.server/'); + }); + + it('should throw if an argument is missing', function () { + expect(() => makeOptionsFromArgv(commonArgs)).toThrow(); + + expect(() => + makeOptionsFromArgv([...commonArgs, '--input', 'value']) + ).toThrow(); + expect(() => + makeOptionsFromArgv([...commonArgs, '--output', 'value']) + ).toThrow(); + expect(() => + makeOptionsFromArgv([...commonArgs, '--server', 'value']) + ).toThrow(); + + expect(() => + makeOptionsFromArgv([ + ...commonArgs, + '--output', + 'value', + '--server', + 'value', + ]) + ).toThrow(); + expect(() => + makeOptionsFromArgv([ + ...commonArgs, + '--input', + 'value', + '--server', + 'value', + ]) + ).toThrow(); + expect(() => + makeOptionsFromArgv([ + ...commonArgs, + '--input', + 'value', + '--output', + 'value', + ]) + ).toThrow(); + }); + + it('should throw if argument has no specified value', function () { + expect(() => + makeOptionsFromArgv([ + ...commonArgs, + '--input', + '--output', + 'value', + '--server', + 'value', + ]) + ).toThrow(); + expect(() => + makeOptionsFromArgv([ + ...commonArgs, + '--input', + 'value', + '--output', + '--server', + 'value', + ]) + ).toThrow(); + expect(() => + makeOptionsFromArgv([ + ...commonArgs, + '--input', + 'value', + '--output', + 'value', + '--server', + ]) + ).toThrow(); + }); +}); + +describe('InMemorySymbolDB', function () { + const debugName = 'debugName'; + const breakpadId = 'breakpadId'; + + it('should get a SymbolTable that was set', async function () { + const db = new InMemorySymbolDB(); + + await db.storeSymbolTable( + debugName, + breakpadId, + completeSymbolTableAsTuple + ); + + const table = await db.getSymbolTable(debugName, breakpadId); + expect(table).toEqual(completeSymbolTableAsTuple); + }); + + it('should throw when getting a SymbolTable that was not set', async function () { + const db = new InMemorySymbolDB(); + + await expect(async () => { + await db.getSymbolTable(debugName, breakpadId); + }).rejects.toThrow(SymbolsNotFoundError); + }); +}); diff --git a/src/types/index.js b/src/types/index.js index dbc6bf4518..5b11d0b7c8 100644 --- a/src/types/index.js +++ b/src/types/index.js @@ -11,6 +11,7 @@ export * from './profile-derived'; export * from './profile'; export * from './state'; export * from './store'; +export * from './symbolication'; export * from './transforms'; export * from './units'; export * from './utils'; diff --git a/src/types/libdef/npm/minimist_v1.x.x.js b/src/types/libdef/npm/minimist_v1.x.x.js new file mode 100644 index 0000000000..57aa4397fe --- /dev/null +++ b/src/types/libdef/npm/minimist_v1.x.x.js @@ -0,0 +1,22 @@ +// flow-typed signature: 4f1f9ccb55e99cfffea0ffa6566add59 +// flow-typed version: c6154227d1/minimist_v1.x.x/flow_>=v0.28.x <=v0.103.x + +declare module 'minimist' { + declare type minimistOptions = { + string?: string | Array, + boolean?: boolean | string | Array, + alias?: { [arg: string]: string | Array }, + default?: { [arg: string]: any }, + stopEarly?: boolean, + // TODO: Strings as keys don't work... + // '--'? boolean, + unknown?: (param: string) => boolean + }; + + declare type minimistOutput = { + _: Array, + [flag: string]: string | boolean + }; + + declare module.exports: (argv: Array, opts?: minimistOptions) => minimistOutput; +} diff --git a/src/types/symbolication.js b/src/types/symbolication.js new file mode 100644 index 0000000000..15166cbf67 --- /dev/null +++ b/src/types/symbolication.js @@ -0,0 +1,36 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// @flow + +export interface ISymbolStoreDB { + /** + * Store the symbol table for a given library. + * @param {string} The debugName of the library. + * @param {string} The breakpadId of the library. + * @param {symbolTable} The symbol table, in SymbolTableAsTuple format. + * @return A promise that resolves (with nothing) once storage + * has succeeded. + */ + storeSymbolTable( + debugName: string, + breakpadId: string, + symbolTable: SymbolTableAsTuple + ): Promise; + + /** + * Retrieve the symbol table for the given library. + * @param {string} The debugName of the library. + * @param {string} The breakpadId of the library. + * @return A promise that resolves with the symbol table (in + * SymbolTableAsTuple format), or fails if we couldn't + * find a symbol table for the requested library. + */ + getSymbolTable( + debugName: string, + breakpadId: string + ): Promise; + + close(): Promise; +} diff --git a/yarn.lock b/yarn.lock index e2c67c7c15..74a0f78009 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9281,6 +9281,11 @@ minimist@^1.2.0, minimist@^1.2.6: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== +minimist@^1.2.8: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + minipass@^4.2.4: version "4.2.8" resolved "https://registry.yarnpkg.com/minipass/-/minipass-4.2.8.tgz#f0010f64393ecfc1d1ccb5f582bcaf45f48e1a3a" From 91fe71ee5fe363b4280f57bdfdbe2127b78226fc Mon Sep 17 00:00:00 2001 From: Julien Wajsberg Date: Mon, 23 Sep 2024 17:02:30 +0200 Subject: [PATCH 40/40] Export a tool to extract gecko logs from a profile (#4973) --- .../__snapshots__/window-console.test.js.snap | 9 ++- src/test/unit/window-console.test.js | 38 ++++++++++- src/utils/window-console.js | 65 ++++++++++++++++++- 3 files changed, 105 insertions(+), 7 deletions(-) diff --git a/src/test/unit/__snapshots__/window-console.test.js.snap b/src/test/unit/__snapshots__/window-console.test.js.snap index f52d898f52..311415926e 100644 --- a/src/test/unit/__snapshots__/window-console.test.js.snap +++ b/src/test/unit/__snapshots__/window-console.test.js.snap @@ -21,7 +21,7 @@ Array [ "font-family: Menlo, monospace;", ], Array [ - "%cThe following profiler information is available via the console:%c + "%cThe following profiler information and tools are available via the console:%c %cwindow.profile%c - The currently loaded profile %cwindow.filteredThread%c - The current filtered thread @@ -35,8 +35,9 @@ Array [ %cwindow.experimental%c - The object that holds flags of all the experimental features. %cwindow.togglePseudoLocalization%c - Enable pseudo localizations by passing \\"accented\\" or \\"bidi\\" to this function, or disable using no parameters. %cwindow.toggleTimelineType%c - Toggle timeline graph type by passing \\"cpu-category\\", \\"category\\", or \\"stack\\". -%cwindow.retrieveRawProfileDataFromBrowser%c - Retrieve the profile attached to the current tab and returns it. Use \\"await\\" to call it. -%cwindow.saveToDisk%c - Saves to a file the parameter passed to it, with an optional filename parameter. You can use that to save the profile returned by \\"retrieveRawProfileDataFromBrowser\\". +%cwindow.retrieveRawProfileDataFromBrowser%c - Retrieve the profile attached to the current tab and returns it. Use \\"await\\" to call it, and use saveToDisk to save it. +%cwindow.extractGeckoLogs%c - Retrieve recorded logs in the current range, using the MOZ_LOG format. Use with \\"copy\\" or \\"saveToDisk\\". +%cwindow.saveToDisk%c - Saves to a file the parameter passed to it, with an optional filename parameter. You can use that to save the profile returned by \\"retrieveRawProfileDataFromBrowser\\" or the data returned by \\"extractGeckoLogs\\". The profile format is documented here: %chttps://github.com/firefox-devtools/profiler/blob/main/docs-developer/processed-profile-format.md%c @@ -73,6 +74,8 @@ The CallTree class's source code is available here: "", "font-weight: bold;", "", + "font-weight: bold;", + "", "font-style: italic; text-decoration: underline;", "", "font-style: italic; text-decoration: underline;", diff --git a/src/test/unit/window-console.test.js b/src/test/unit/window-console.test.js index 8872763601..a39dbe35a3 100644 --- a/src/test/unit/window-console.test.js +++ b/src/test/unit/window-console.test.js @@ -3,11 +3,14 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ // @flow +import { stripIndent } from 'common-tags'; + import { addDataToWindowObject, logFriendlyPreamble, } from '../../utils/window-console'; -import { storeWithSimpleProfile } from '../fixtures/stores'; +import { storeWithSimpleProfile, storeWithProfile } from '../fixtures/stores'; +import { getProfileWithMarkers } from '../fixtures/profiles/processed-profile'; describe('console-accessible values on the window object', function () { // Coerce the window into a generic object, as these values aren't defined @@ -37,4 +40,37 @@ describe('console-accessible values on the window object', function () { expect(console.log.mock.calls).toMatchSnapshot(); (console: any).log = log; }); + + it('can extract gecko logs', function () { + const profile = getProfileWithMarkers([ + [ + 'LogMessages', + 170, + null, + { + type: 'Log', + module: 'nsHttp', + name: 'ParentChannelListener::ParentChannelListener [this=7fb5e19b98d0, next=7fb5f48f2320]', + }, + ], + [ + 'LogMessages', + 190, + null, + { + type: 'Log', + name: 'nsJARChannel::nsJARChannel [this=0x87f1ec80]\n', + module: 'nsJarProtocol', + }, + ], + ]); + const store = storeWithProfile(profile); + const target = {}; + addDataToWindowObject(store.getState, store.dispatch, target); + const result = target.extractGeckoLogs(); + expect(result).toBe(stripIndent` + 1970-01-01 00:00:00.170000000 UTC - [Unknown Process 0: Empty]: D/nsHttp ParentChannelListener::ParentChannelListener [this=7fb5e19b98d0, next=7fb5f48f2320] + 1970-01-01 00:00:00.190000000 UTC - [Unknown Process 0: Empty]: D/nsJarProtocol nsJARChannel::nsJARChannel [this=0x87f1ec80] + `); + }); }); diff --git a/src/utils/window-console.js b/src/utils/window-console.js index 558fcdb7a1..fe2036ad60 100644 --- a/src/utils/window-console.js +++ b/src/utils/window-console.js @@ -210,6 +210,61 @@ export function addDataToWindowObject( URL.revokeObjectURL(blobUrl); }; + // This function extracts MOZ_LOGs saved as markers in a Firefox profile, + // using the MOZ_LOG canonical format. All logs are saved as a debug log + // because the log level information isn't saved in these markers. + target.extractGeckoLogs = function () { + function pad(p, c) { + return String(p).padStart(c, '0'); + } + + // This transforms a timestamp to a string as output by mozlog usually. + function d2s(ts) { + const d = new Date(ts); + // new Date rounds down the timestamp (in milliseconds) to the lower integer, + // let's get the microseconds and nanoseconds differently. + // This will be imperfect because of float rounding errors but still better + // than not having them. + const ns = Math.trunc((ts - Math.trunc(ts)) * 10 ** 6); + return `${d.getFullYear()}-${pad(d.getUTCMonth() + 1, 2)}-${pad(d.getUTCDate(), 2)} ${pad(d.getUTCHours(), 2)}:${pad(d.getUTCMinutes(), 2)}:${pad(d.getUTCSeconds(), 2)}.${pad(d.getUTCMilliseconds(), 3)}${pad(ns, 6)} UTC`; + } + + const logs = []; + + // This algorithm loops over the raw marker table instead of using the + // selectors so that the full marker list isn't generated for all the + // threads in the profile. + const profile = selectorsForConsole.profile.getProfile(getState()); + const range = + selectorsForConsole.profile.getPreviewSelectionRange(getState()); + + for (const thread of profile.threads) { + const { markers } = thread; + for (let i = 0; i < markers.length; i++) { + const startTime = markers.startTime[i]; + // Note that Log markers are instant markers, so they only have a start time. + if ( + startTime !== null && + markers.data[i] && + markers.data[i].type === 'Log' && + startTime >= range.start && + startTime <= range.end + ) { + const data = markers.data[i]; + const strTimestamp = d2s( + profile.meta.startTime + markers.startTime[i] + ); + const processName = thread.processName ?? 'Unknown Process'; + // TODO: lying about the log level as it's not available yet in the markers + const statement = `${strTimestamp} - [${processName} ${thread.pid}: ${thread.name}]: D/${data.module} ${data.name.trim()}`; + logs.push(statement); + } + } + } + + return logs.sort().join('\n'); + }; + target.shortenUrl = shortenUrl; target.getState = getState; target.selectors = selectorsForConsole; @@ -255,7 +310,7 @@ export function logFriendlyPreamble() { console.log( stripIndent` - %cThe following profiler information is available via the console:%c + %cThe following profiler information and tools are available via the console:%c %cwindow.profile%c - The currently loaded profile %cwindow.filteredThread%c - The current filtered thread @@ -269,8 +324,9 @@ export function logFriendlyPreamble() { %cwindow.experimental%c - The object that holds flags of all the experimental features. %cwindow.togglePseudoLocalization%c - Enable pseudo localizations by passing "accented" or "bidi" to this function, or disable using no parameters. %cwindow.toggleTimelineType%c - Toggle timeline graph type by passing "cpu-category", "category", or "stack". - %cwindow.retrieveRawProfileDataFromBrowser%c - Retrieve the profile attached to the current tab and returns it. Use "await" to call it. - %cwindow.saveToDisk%c - Saves to a file the parameter passed to it, with an optional filename parameter. You can use that to save the profile returned by "retrieveRawProfileDataFromBrowser". + %cwindow.retrieveRawProfileDataFromBrowser%c - Retrieve the profile attached to the current tab and returns it. Use "await" to call it, and use saveToDisk to save it. + %cwindow.extractGeckoLogs%c - Retrieve recorded logs in the current range, using the MOZ_LOG format. Use with "copy" or "saveToDisk". + %cwindow.saveToDisk%c - Saves to a file the parameter passed to it, with an optional filename parameter. You can use that to save the profile returned by "retrieveRawProfileDataFromBrowser" or the data returned by "extractGeckoLogs". The profile format is documented here: %chttps://github.com/firefox-devtools/profiler/blob/main/docs-developer/processed-profile-format.md%c @@ -320,6 +376,9 @@ export function logFriendlyPreamble() { // "window.retrieveRawProfileDataFromBrowser" bold, reset, + // "window.extractGeckoLogs" + bold, + reset, // "window.saveToDisk" bold, reset,