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/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. 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. 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. 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. diff --git a/locales/en-US/app.ftl b/locales/en-US/app.ftl index 8c493ab4a2..5946fc9a2d 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--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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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/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..fe6ddec989 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,107 @@ 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 + ); + 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/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 { diff --git a/src/components/app/ProfileFilterNavigator.css b/src/components/app/ProfileFilterNavigator.css new file mode 100644 index 0000000000..9fb73fee2f --- /dev/null +++ b/src/components/app/ProfileFilterNavigator.css @@ -0,0 +1,31 @@ +/* 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; + column-gap: 5px; + + /* 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. */ +.profileFilterNavigator--tab-selector::before { + position: absolute; + border-top: 6px solid; + border-right: 4px solid transparent; + border-bottom: 0 solid transparent; + border-left: 4px solid transparent; + margin: 5px 5px 0; + color: var(--internal-selected-color); + content: ''; + inset-block-start: 4px; + inset-inline-start: 4px; +} + +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 1c4fd9bb7f..baf2b6b0d9 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 || 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; + } + + 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,100 @@ 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 - - ); + // 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. + // eslint-disable-next-line no-constant-condition + if (false && pageDataByTabID && pageDataByTabID.size > 0) { + const pageData = + tabFilter !== null ? pageDataByTabID.get(tabFilter) : null; + + const itemContents = pageData ? ( + <> + {/* Show the page data if the profile is filtered by tab */} + {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 + + ); + } } const itemsWithFirstElement = this._getItemsWithFirstElement( @@ -88,13 +182,18 @@ class ProfileFilterNavigatorBarImpl extends React.PureComponent { items ); return ( - + <> + + {pageDataByTabID && pageDataByTabID.size > 0 ? ( + + ) : null} + ); } } @@ -112,7 +211,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 +224,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/components/marker-chart/Canvas.js b/src/components/marker-chart/Canvas.js index a50539c0af..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, - +text: string, + +markerIndex: MarkerIndex, |}; // We can hover over multiple items with Marker chart when we are in the active @@ -80,6 +80,8 @@ type OwnProps = {| +markerTimingAndBuckets: MarkerTimingAndBuckets, +rowHeight: CssPixels, +getMarker: (MarkerIndex) => Marker, + +getMarkerLabel: (MarkerIndex) => string, + +markerListLength: number, +threadsKey: ThreadsKey, +updatePreviewSelection: WrapFunctionInDispatch, +changeMouseTimePosition: ChangeMouseTimePosition, @@ -164,11 +166,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 +192,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 +256,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 +274,7 @@ class MarkerChartCanvasImpl extends React.PureComponent { timingIndex < markerTiming.length; timingIndex++ ) { - markerIndexToTimingRow.set(markerTiming.index[timingIndex], rowIndex); + markerIndexToTimingRow[markerTiming.index[timingIndex]] = rowIndex; } } return markerIndexToTimingRow; @@ -287,13 +291,13 @@ class MarkerChartCanvasImpl extends React.PureComponent { w: CssPixels, h: CssPixels, isInstantMarker: boolean, - text: string, + markerIndex: MarkerIndex, 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, markerIndex, isHighlighted); } } @@ -303,10 +307,10 @@ class MarkerChartCanvasImpl extends React.PureComponent { y: CssPixels, w: CssPixels, h: CssPixels, - text: 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 @@ -351,7 +355,10 @@ 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( + getMarkerLabel(markerIndex), + w2 + ); if (fittedText) { ctx.fillStyle = isHighlighted ? 'white' : 'black'; ctx.fillText(fittedText, x2, y + TEXT_OFFSET_TOP); @@ -474,7 +481,6 @@ class MarkerChartCanvasImpl extends React.PureComponent { x = Math.round(x * devicePixelRatio) / devicePixelRatio; w = Math.round(w * devicePixelRatio) / devicePixelRatio; - const text = markerTiming.label[i]; const markerIndex = markerTiming.index[i]; const isHighlighted = @@ -483,7 +489,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, + markerIndex, + }); } else if ( // Always render non-dot markers and markers that are larger than // one pixel. @@ -493,7 +506,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, markerIndex); } } } @@ -509,7 +522,7 @@ class MarkerChartCanvasImpl extends React.PureComponent { highlightedMarker.w, highlightedMarker.h, highlightedMarker.isInstantMarker, - highlightedMarker.text, + highlightedMarker.markerIndex, true /* isHighlighted */ ); }); diff --git a/src/components/marker-chart/index.js b/src/components/marker-chart/index.js index 86a8d25bc1..17abbb99cf 100644 --- a/src/components/marker-chart/index.js +++ b/src/components/marker-chart/index.js @@ -55,8 +55,10 @@ type DispatchProps = {| type StateProps = {| +getMarker: (MarkerIndex) => Marker, + +getMarkerLabel: (MarkerIndex) => string, +markerTimingAndBuckets: MarkerTimingAndBuckets, +maxMarkerRows: number, + +markerListLength: number, +timeRange: StartEndRange, +threadsKey: ThreadsKey, +previewSelection: PreviewSelection, @@ -105,10 +107,12 @@ class MarkerChartImpl extends React.PureComponent { render() { const { maxMarkerRows, + markerListLength, timeRange, threadsKey, markerTimingAndBuckets, getMarker, + getMarkerLabel, previewSelection, updatePreviewSelection, changeMouseTimePosition, @@ -156,6 +160,8 @@ class MarkerChartImpl extends React.PureComponent { chartProps={{ markerTimingAndBuckets, getMarker, + getMarkerLabel, + markerListLength, // $FlowFixMe Error introduced by upgrading to v0.96.0. See issue #1936. updatePreviewSelection, changeMouseTimePosition, @@ -194,8 +200,10 @@ 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), timeRange: getCommittedRange(state), threadsKey: getSelectedThreadsKey(state), previewSelection: getPreviewSelection(state), diff --git a/src/components/shared/FilterNavigatorBar.css b/src/components/shared/FilterNavigatorBar.css index e8e6b1b2b0..5aba1b1e9c 100644 --- a/src/components/shared/FilterNavigatorBar.css +++ b/src/components/shared/FilterNavigatorBar.css @@ -108,7 +108,8 @@ color: var(--internal-selected-color); } -.filterNavigatorBarItem:not(.filterNavigatorBarLeafItem):hover { +.filterNavigatorBarItem:not(.filterNavigatorBarLeafItem):hover, +.filterNavigatorBarItem:has(button.profileFilterNavigator--tab-selector):hover { background-color: rgb(0 0 0 / 0.1); } diff --git a/src/components/shared/TabSelectorMenu.css b/src/components/shared/TabSelectorMenu.css new file mode 100644 index 0000000000..5da4b4a96a --- /dev/null +++ b/src/components/shared/TabSelectorMenu.css @@ -0,0 +1,19 @@ +/* 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/. */ + +.TabSelectorMenu { + /* Make it scrollable if the tab list is too long. */ + overflow-y: auto; +} + +.tabSelectorMenuItem.checkable { + padding-inline: 20px 10px; +} + +.react-contextmenu-item.tabSelectorMenuItem.checked:not( + .react-contextmenu-item--disabled + )::before { + /* Move the checkmark to inline-start instead of right, as it's logically better. */ + inset-inline: 8px 0; +} diff --git a/src/components/shared/TabSelectorMenu.js b/src/components/shared/TabSelectorMenu.js new file mode 100644 index 0000000000..532a1aa252 --- /dev/null +++ b/src/components/shared/TabSelectorMenu.js @@ -0,0 +1,102 @@ +/* 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 { changeTabFilter } from 'firefox-profiler/actions/receive-profile'; +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 = {| + +changeTabFilter: typeof changeTabFilter, +|}; + +type Props = ConnectedProps<{||}, StateProps, DispatchProps>; + +import './TabSelectorMenu.css'; + +class TabSelectorMenuImpl extends React.PureComponent { + _handleClick = (_event: SyntheticEvent<>, data: {| id: TabID |}): void => { + this.props.changeTabFilter(data.id); + }; + + renderTabSelectorMenuContents() { + const { pageDataByTabID, tabFilter } = this.props; + if (!pageDataByTabID || pageDataByTabID.size === 0) { + // There is no page data, return early. + return null; + } + + return ( + <> + + + All tabs and windows + + + {[...pageDataByTabID].map(([tabID, pageData]) => ( + + {pageData.hostname} + + ))} + + ); + } + + render() { + return ( + + {this.renderTabSelectorMenuContents()} + + ); + } +} + +export const TabSelectorMenu = explicitConnect<{||}, StateProps, DispatchProps>( + { + mapStateToProps: (state) => ({ + tabFilter: getTabFilter(state), + pageDataByTabID: getProfileFilterPageDataByTabID(state), + }), + mapDispatchToProps: { + changeTabFilter, + }, + component: TabSelectorMenuImpl, + } +); diff --git a/src/profile-logic/marker-timing.js b/src/profile-logic/marker-timing.js index 8635198b75..9bf6b13f0d 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, @@ -90,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 @@ -110,7 +105,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 +123,6 @@ export function getMarkerTiming( start: [], end: [], index: [], - label: [], name: markerLineName, bucket: bucketName, instantOnly, @@ -254,7 +247,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 +256,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 +265,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 +275,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, @@ -297,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/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); 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/profile-logic/tracks.js b/src/profile-logic/tracks.js index 2fca93c0e7..207788bdc4 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. The ones that have been filtered out by the tab selector + * should be ignored. */ export function computeLocalTracksByPid( profile: Profile, + availableGlobalTracks: GlobalTrack[], markerSchemaByName: MarkerSchemaByName ): Map { const localTracksByPid = new Map(); + // 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) { + 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. @@ -449,7 +476,7 @@ export function computeGlobalTracks(profile: Profile): GlobalTrack[] { mainThreadIndex: number | null, }; const globalTracksByPid: Map = new Map(); - const globalTracks: GlobalTrack[] = []; + let globalTracks: GlobalTrack[] = []; // Create the global tracks. for ( @@ -526,6 +553,14 @@ export function computeGlobalTracks(profile: Profile): GlobalTrack[] { } } + // 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( @@ -537,6 +572,67 @@ export function computeGlobalTracks(profile: Profile): GlobalTrack[] { return globalTracks; } +/** + * 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) { + // 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; + } + + // 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 include 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; +} + /** * Determine the display order for the global tracks, which will be different the * initial ordering of the tracks, as the initial ordering must remain stable as @@ -726,7 +822,7 @@ export function computeDefaultHiddenTracks( ): HiddenTracks { return _computeHiddenTracksForVisibleThreads( profile, - computeDefaultVisibleThreads(profile), + computeDefaultVisibleThreads(profile, tracksWithOrder), tracksWithOrder ); } @@ -912,12 +1008,48 @@ 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; // 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 +1064,11 @@ export function computeDefaultVisibleThreads( return new Set(profile.meta.initialVisibleThreads); } + const allTrackThreads = computeAllTrackThreads(tracksWithOrder); + // 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 +1077,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..9e0be11d12 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; 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/selectors/per-thread/markers.js b/src/selectors/per-thread/markers.js index 66826cf505..393ee3f168 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: @@ -446,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, @@ -481,7 +487,6 @@ export function getMarkerSelectorsPerThread( createSelector( getMarkerGetter, getMarkerChartMarkerIndexes, - _getMarkerChartLabelGetter, ProfileSelectors.getCategories, MarkerTimingLogic.getMarkerTimingAndBuckets ); @@ -535,7 +540,6 @@ export function getMarkerSelectorsPerThread( const getNetworkTrackTiming: Selector = createSelector( getMarkerGetter, getNetworkMarkerIndexes, - _getMarkerChartLabelGetter, MarkerTimingLogic.getMarkerTiming ); @@ -546,7 +550,6 @@ export function getMarkerSelectorsPerThread( const getUserTimingMarkerTiming: Selector = createSelector( getMarkerGetter, getUserTimingMarkerIndexes, - _getMarkerChartLabelGetter, MarkerTimingLogic.getMarkerTiming ); @@ -739,11 +742,13 @@ export function getMarkerSelectorsPerThread( getMarkerIndexToRawMarkerIndexes, getFullMarkerList, getFullMarkerListIndexes, + getMarkerListLength, getNetworkMarkerIndexes, getSearchFilteredNetworkMarkerIndexes, getAreMarkerPanelsEmptyInFullRange, getMarkerTableMarkerIndexes, getMarkerChartMarkerIndexes, + getMarkerChartLabelGetter, getMarkerTooltipLabelGetter, getMarkerTableLabelGetter, getMarkerLabelToCopyGetter, 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/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/TabSelectorMenu.test.js b/src/test/components/TabSelectorMenu.test.js new file mode 100644 index 0000000000..02c9fab4fa --- /dev/null +++ b/src/test/components/TabSelectorMenu.test.js @@ -0,0 +1,152 @@ +/* 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 { screen } from '@testing-library/react'; + +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, + 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() { + const { profile, ...extraPageData } = addActiveTabInformationToProfile( + 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); + render( + + + + ); + + return { + profile, + ...extraPageData, + ...store, + }; + } + + it('should render properly', () => { + setup(); + expect(document.body).toMatchSnapshot(); + }); + + it('should not render when the profile does not contain any page data', () => { + const store = storeWithProfile(getProfileWithNiceTracks()); + render( + + + + ); + expect(document.body).toMatchSnapshot(); + }); + + it('should switch tabs properly', () => { + 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 = 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 = 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 = screen.getByText('All tabs and windows'); + fireFullClick(allTabs); + + // Check the tab filter again, it should be null, meaning all tabs and windows. + expect(getTabFilter(getState())).toBe(null); + }); + + it('should display the relevant threads after tab switch', () => { + const { getState, 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 = 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); + // 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 = 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); + // Also make sure that the first process is visible. + expect(getHumanReadableTracks(getState())).toEqual([ + 'show [thread GeckoMain default] SELECTED', + ]); + + // Change the tab filter to 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. + 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]', + ]); + }); +}); 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/components/__snapshots__/TabSelectorMenu.test.js.snap b/src/test/components/__snapshots__/TabSelectorMenu.test.js.snap new file mode 100644 index 0000000000..6dd1aa39ad --- /dev/null +++ b/src/test/components/__snapshots__/TabSelectorMenu.test.js.snap @@ -0,0 +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`] = ` + +
+ +
+ +`; 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/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/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/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/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, 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/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/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/test/url-handling.test.js b/src/test/url-handling.test.js index 3fffcbea50..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, @@ -596,7 +593,7 @@ describe('ctxId', function () { const resourceTracks = getActiveTabResourceTracks(getState()); expect(resourceTracks).toEqual([ { - name: 'Page #2', + name: 'https://www.youtube.com/', type: 'sub-frame', threadIndex: 1, }, 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 = 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/profile-derived.js b/src/types/profile-derived.js index 03cc077102..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: string[], name: string, bucket: string, // True if this marker timing contains only instant markers. 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/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, 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"