diff --git a/docs-developer/CHANGELOG-formats.md b/docs-developer/CHANGELOG-formats.md
index 21c7185a5a..551f33a9a9 100644
--- a/docs-developer/CHANGELOG-formats.md
+++ b/docs-developer/CHANGELOG-formats.md
@@ -6,6 +6,12 @@ Note that this is not an exhaustive list. Processed profile format upgraders can
## Processed profile format
+### Version 59
+
+A new optional `usedInnerWindowIDs` field was added to the `Thread` type. This field contains an array of inner window IDs. It is used for the tab selector dropdown in the profiler UI, together with the information from `profile.pages`. When a tab is selected in this dropdown, threads that don't have an inner window ID for the selected tab in their `usedInnerWindowIDs` field are hidden. The array is treated as a set - the order of items in it has no meaning.
+
+Profiles which don't use `profile.pages` also don't need to use the `thread.usedInnerWindowIDs` field.
+
### Version 58
A new `SourceTable` has been added to `profile.shared.sources` to centralize all source file information. The `FuncTable.fileName` field has been replaced with `FuncTable.source`, which references indices in the shared sources table. This change allows storing a UUID per JS source, which will be used for fetching sources.
diff --git a/jest.config.js b/jest.config.js
index dbb1aa6adc..67cbc9e4ca 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -18,10 +18,14 @@ module.exports = {
'!src/types/libdef/**',
],
+ transform: {
+ '\\.([jt]sx?|mjs)$': 'babel-jest',
+ },
+
// Transform ESM modules to CommonJS for Jest
// These packages ship as pure ESM and need to be transformed by Babel
transformIgnorePatterns: [
- '/node_modules/(?!(query-string|decode-uri-component|iongraph-web|split-on-first|filter-obj|fetch-mock)/)',
+ '/node_modules/(?!(query-string|decode-uri-component|iongraph-web|split-on-first|filter-obj|fetch-mock|devtools-reps)/)',
],
// Mock static assets (images, CSS, etc.)
diff --git a/locales/be/app.ftl b/locales/be/app.ftl
index 2d70f8efab..95bda10941 100644
--- a/locales/be/app.ftl
+++ b/locales/be/app.ftl
@@ -79,6 +79,9 @@ CallNodeContextMenu--transform-focus-function = Фокус на функцыі
.title = { CallNodeContextMenu--transform-focus-function-title }
CallNodeContextMenu--transform-focus-function-inverted = Фокус на функцыі (інвертавана)
.title = { CallNodeContextMenu--transform-focus-function-title }
+
+##
+
CallNodeContextMenu--transform-focus-subtree = Фокус толькі на паддрэве
.title = Фокус на паддрэве прывядзе да выдалення любога ўзору, які не ўключае гэтую канкрэтную частку дрэва выклікаў. Гэта выдаляе галіну дрэва выклікаў, але робіць гэта толькі для аднаго вузла выкліку. Усе іншыя выклікі функцый ігнаруюцца.
# This is used as the context menu item to apply the "Focus on category" transform.
@@ -569,7 +572,6 @@ MenuButtons--metaInfo--buffer-duration-seconds =
MenuButtons--metaInfo--buffer-duration-unlimited = Неабмежавана
MenuButtons--metaInfo--application = Праграма
MenuButtons--metaInfo--name-and-version = Назва і версія:
-MenuButtons--metaInfo--application-uptime = Час працы:
MenuButtons--metaInfo--update-channel = Канал абнаўлення:
MenuButtons--metaInfo--build-id = ID зборкі:
MenuButtons--metaInfo--build-type = Тып зборкі:
diff --git a/locales/de/app.ftl b/locales/de/app.ftl
index 9ebd91f04e..62e72d124f 100644
--- a/locales/de/app.ftl
+++ b/locales/de/app.ftl
@@ -76,6 +76,21 @@ CallNodeContextMenu--transform-focus-function = Auf Funktion fokussieren
.title = { CallNodeContextMenu--transform-focus-function-title }
CallNodeContextMenu--transform-focus-function-inverted = Auf Funktion fokussieren (invertiert)
.title = { CallNodeContextMenu--transform-focus-function-title }
+
+## The translation for "self" in these strings should match the translation used
+## in CallTree--samples-self and CallTree--bytes-self. Alternatively it can be
+## translated as "self values" or "self time" (though "self time" is less desirable
+## because this menu item is also shown in "bytes" mode).
+
+CallNodeContextMenu--transform-focus-self-title =
+ Die Fokussierung auf sich selbst ähnelt der Fokussierung auf eine Funktion, behält jedoch nur Samples bei,
+ die zur Zeit der Funktion selbst beitragen. Samples in Angerufenen
+ werden verworfen und der Aufrufbaum wird auf die Funktion im Fokus zurückgesetzt.
+CallNodeContextMenu--transform-focus-self = Nur auf sich selbst konzentrieren
+ .title = { CallNodeContextMenu--transform-focus-self-title }
+
+##
+
CallNodeContextMenu--transform-focus-subtree = Nur auf Unterbaum konzentrieren
.title =
Der Fokus auf einen Unterbaum entfernt jede Teilmenge, die diesen
@@ -546,7 +561,8 @@ MenuButtons--metaInfo--buffer-duration-seconds =
MenuButtons--metaInfo--buffer-duration-unlimited = Unbegrenzt
MenuButtons--metaInfo--application = Anwendung
MenuButtons--metaInfo--name-and-version = Name und Version:
-MenuButtons--metaInfo--application-uptime = Uptime:
+# The time between application startup and when the profiler was started
+MenuButtons--metaInfo--application-uptime2 = Uptime:
MenuButtons--metaInfo--update-channel = Update-Kanal:
MenuButtons--metaInfo--build-id = Build-ID:
MenuButtons--metaInfo--build-type = Build-Typ:
@@ -861,6 +877,11 @@ TrackPower--tooltip-power-watt = { $value } W
# $value (String) - the power value at this location
TrackPower--tooltip-power-milliwatt = { $value } mW
.label = Leistung
+# This is used in the tooltip when the instant power value uses the microwatt unit.
+# Variables:
+# $value (String) - the power value at this location
+TrackPower--tooltip-power-microwatt = { $value } μW
+ .label = Leistung
# This is used in the tooltip when the power value uses the kilowatt unit.
# Variables:
# $value (String) - the power value at this location
@@ -1010,6 +1031,12 @@ TransformNavigator--focus-subtree = Knoten fokussieren: { $item }
# Variables:
# $item (String) - Name of the function that transform applied to.
TransformNavigator--focus-function = Fokussieren: { $item }
+# "Focus self" transform.
+# See: https://profiler.firefox.com/docs/#/./guide-filtering-call-trees?id=focus-on-function-self
+# Also see the translation note above CallNodeContextMenu--transform-focus-self.
+# Variables:
+# $item (String) - Name of the function that transform applied to.
+TransformNavigator--focus-self = Fokus auf sich selbst: { $item }
# "Focus category" transform. The word "Focus" has the meaning of an adjective here.
# See: https://profiler.firefox.com/docs/#/./guide-filtering-call-trees?id=focus-category
# Variables:
diff --git a/locales/el/app.ftl b/locales/el/app.ftl
index 0b2651b9eb..b68c19404c 100644
--- a/locales/el/app.ftl
+++ b/locales/el/app.ftl
@@ -79,6 +79,9 @@ CallNodeContextMenu--transform-focus-function = Εστίαση στη συνάρ
.title = { CallNodeContextMenu--transform-focus-function-title }
CallNodeContextMenu--transform-focus-function-inverted = Εστίαση στη συνάρτηση (ανεστραμμένη)
.title = { CallNodeContextMenu--transform-focus-function-title }
+
+##
+
CallNodeContextMenu--transform-focus-subtree = Εστίαση στο υπόδεντρο μόνο
.title =
Η εστίαση σε υπόδεντρο θα αφαιρέσει κάθε δείγμα που δεν περιλαμβάνει αυτό το
@@ -333,6 +336,7 @@ Home--additional-content-title = Φόρτωση υπαρχόντων προφί
Home--additional-content-content = Μπορείτε να σύρετε και να εναποθέσετε ένα αρχείο προφίλ εδώ για φόρτωση, ή:
Home--compare-recordings-info = Μπορείτε επίσης να συγκρίνετε καταγραφές. Άνοιγμα περιβάλλοντος σύγκρισης.
Home--your-recent-uploaded-recordings-title = Πρόσφατα μεταφορτωμένες καταγραφές
+Home--dark-mode-title = Σκουρόχρωμη λειτουργία
# We replace the elements such as and with links to the
# documentation to use these tools.
Home--load-files-from-other-tools2 =
@@ -424,6 +428,13 @@ MarkerContextMenu--select-the-sender-thread = Επιλέξτε το νήμα α
# $filter (String) - Search string that will be used to filter the markers.
MarkerFiltersContextMenu--drop-samples-outside-of-markers-matching = Απόρριψη δειγμάτων εκτός των δεικτών που αντιστοιχούν στο «{ $filter }»
+## MarkerCopyTableContextMenu
+## This is the menu when the copy icon is clicked in Marker Chart and Marker
+## Table panels.
+
+MarkerCopyTableContextMenu--copy-table-as-plain = Αντιγραφή πίνακα δεικτών ως απλού κειμένου
+MarkerCopyTableContextMenu--copy-table-as-markdown = Αντιγραφή πίνακα δεικτών ως Markdown
+
## MarkerSettings
## This is used in all panels related to markers.
@@ -434,6 +445,12 @@ MarkerSettings--marker-filters =
.title = Φίλτρα δείκτη
MarkerSettings--copy-table =
.title = Αντιγραφή πίνακα ως κειμένου
+# This string is used when the user tries to copy a marker table with
+# more than 10000 rows.
+# Variable:
+# $rows (Number) - Number of rows the marker table has
+# $maxRows (Number) - Number of maximum rows that can be copied
+MarkerSettings--copy-table-exceeed-max-rows = Ο αριθμός σειρών υπερβαίνει το όριο: { $rows } > { $maxRows }. Θα αντιγραφούν μόνο οι πρώτες { $maxRows } σειρές.
## MarkerSidebar
## This is the sidebar component that is used in Marker Table panel.
@@ -551,7 +568,6 @@ MenuButtons--metaInfo--buffer-duration-seconds =
MenuButtons--metaInfo--buffer-duration-unlimited = Απεριόριστη
MenuButtons--metaInfo--application = Εφαρμογή
MenuButtons--metaInfo--name-and-version = Όνομα και έκδοση:
-MenuButtons--metaInfo--application-uptime = Χρόνος λειτουργίας:
MenuButtons--metaInfo--update-channel = Κανάλι ενημερώσεων:
MenuButtons--metaInfo--build-id = ID δομής:
MenuButtons--metaInfo--build-type = Τύπος δομής:
@@ -866,6 +882,11 @@ TrackPower--tooltip-power-watt = { $value } W
# $value (String) - the power value at this location
TrackPower--tooltip-power-milliwatt = { $value } mW
.label = Ισχύς
+# This is used in the tooltip when the instant power value uses the microwatt unit.
+# Variables:
+# $value (String) - the power value at this location
+TrackPower--tooltip-power-microwatt = { $value } μW
+ .label = Ισχύς
# This is used in the tooltip when the power value uses the kilowatt unit.
# Variables:
# $value (String) - the power value at this location
@@ -1160,6 +1181,17 @@ AssemblyView--show-button =
# Assembly refers to the low-level programming language.
AssemblyView--hide-button =
.title = Απόκρυψη προβολής assembly
+# The "◀" button above the assembly view.
+AssemblyView--prev-button =
+ .title = Προηγούμενο
+# The "▶" button above the assembly view.
+AssemblyView--next-button =
+ .title = Επόμενο
+# The label showing the current position and total count above the assembly view.
+# Variables:
+# $current (Number) - The current position (1-indexed).
+# $total (Number) - The total count.
+AssemblyView--position-label = { $current } από { $total }
## UploadedRecordingsHome
## This is the page that displays all the profiles that user has uploaded.
diff --git a/locales/en-CA/app.ftl b/locales/en-CA/app.ftl
index 1452fdf931..c6480b0216 100644
--- a/locales/en-CA/app.ftl
+++ b/locales/en-CA/app.ftl
@@ -79,6 +79,9 @@ CallNodeContextMenu--transform-focus-function = Focus on function
.title = { CallNodeContextMenu--transform-focus-function-title }
CallNodeContextMenu--transform-focus-function-inverted = Focus on function (inverted)
.title = { CallNodeContextMenu--transform-focus-function-title }
+
+##
+
CallNodeContextMenu--transform-focus-subtree = Focus on subtree only
.title =
Focusing on a subtree will remove any sample that does not include that
@@ -570,7 +573,6 @@ MenuButtons--metaInfo--buffer-duration-seconds =
MenuButtons--metaInfo--buffer-duration-unlimited = Unlimited
MenuButtons--metaInfo--application = Application
MenuButtons--metaInfo--name-and-version = Name and version:
-MenuButtons--metaInfo--application-uptime = Uptime:
MenuButtons--metaInfo--update-channel = Update channel:
MenuButtons--metaInfo--build-id = Build ID:
MenuButtons--metaInfo--build-type = Build type:
diff --git a/locales/en-GB/app.ftl b/locales/en-GB/app.ftl
index e4c74fd58c..f8e82fb3dd 100644
--- a/locales/en-GB/app.ftl
+++ b/locales/en-GB/app.ftl
@@ -79,6 +79,21 @@ CallNodeContextMenu--transform-focus-function = Focus on function
.title = { CallNodeContextMenu--transform-focus-function-title }
CallNodeContextMenu--transform-focus-function-inverted = Focus on function (inverted)
.title = { CallNodeContextMenu--transform-focus-function-title }
+
+## The translation for "self" in these strings should match the translation used
+## in CallTree--samples-self and CallTree--bytes-self. Alternatively it can be
+## translated as "self values" or "self time" (though "self time" is less desirable
+## because this menu item is also shown in "bytes" mode).
+
+CallNodeContextMenu--transform-focus-self-title =
+ Focusing on self is similar to focusing on a function, but only keeps samples
+ that contribute to the function’s self time. Samples in callees
+ are dropped and the call tree is re-rooted to the focused function.
+CallNodeContextMenu--transform-focus-self = Focus on self only
+ .title = { CallNodeContextMenu--transform-focus-self-title }
+
+##
+
CallNodeContextMenu--transform-focus-subtree = Focus on subtree only
.title =
Focusing on a subtree will remove any sample that does not include that
@@ -570,7 +585,6 @@ MenuButtons--metaInfo--buffer-duration-seconds =
MenuButtons--metaInfo--buffer-duration-unlimited = Unlimited
MenuButtons--metaInfo--application = Application
MenuButtons--metaInfo--name-and-version = Name and version:
-MenuButtons--metaInfo--application-uptime = Uptime:
MenuButtons--metaInfo--update-channel = Update Channel:
MenuButtons--metaInfo--build-id = Build ID:
MenuButtons--metaInfo--build-type = Build Type:
@@ -885,6 +899,11 @@ TrackPower--tooltip-power-watt = { $value } W
# $value (String) - the power value at this location
TrackPower--tooltip-power-milliwatt = { $value } mW
.label = Power
+# This is used in the tooltip when the instant power value uses the microwatt unit.
+# Variables:
+# $value (String) - the power value at this location
+TrackPower--tooltip-power-microwatt = { $value } μW
+ .label = Power
# This is used in the tooltip when the power value uses the kilowatt unit.
# Variables:
# $value (String) - the power value at this location
@@ -1034,6 +1053,12 @@ TransformNavigator--focus-subtree = Focus Node: { $item }
# Variables:
# $item (String) - Name of the function that transform applied to.
TransformNavigator--focus-function = Focus: { $item }
+# "Focus self" transform.
+# See: https://profiler.firefox.com/docs/#/./guide-filtering-call-trees?id=focus-on-function-self
+# Also see the translation note above CallNodeContextMenu--transform-focus-self.
+# Variables:
+# $item (String) - Name of the function that transform applied to.
+TransformNavigator--focus-self = Focus Self: { $item }
# "Focus category" transform. The word "Focus" has the meaning of an adjective here.
# See: https://profiler.firefox.com/docs/#/./guide-filtering-call-trees?id=focus-category
# Variables:
diff --git a/locales/en-US/app.ftl b/locales/en-US/app.ftl
index 72757131d3..9892116a6c 100644
--- a/locales/en-US/app.ftl
+++ b/locales/en-US/app.ftl
@@ -24,6 +24,16 @@ AppHeader--app-header = { -profiler-brand-name } —
+
diff --git a/res/img/svg/theme-light.svg b/res/img/svg/theme-light.svg
new file mode 100644
index 0000000000..94f363d2ff
--- /dev/null
+++ b/res/img/svg/theme-light.svg
@@ -0,0 +1,12 @@
+
+
diff --git a/res/img/svg/theme-system.svg b/res/img/svg/theme-system.svg
new file mode 100644
index 0000000000..634fe53df1
--- /dev/null
+++ b/res/img/svg/theme-system.svg
@@ -0,0 +1,6 @@
+
+
diff --git a/src/app-logic/constants.ts b/src/app-logic/constants.ts
index be047a25b2..4679913070 100644
--- a/src/app-logic/constants.ts
+++ b/src/app-logic/constants.ts
@@ -12,7 +12,7 @@ export const GECKO_PROFILE_VERSION = 32;
// The current version of the "processed" profile format.
// Please don't forget to update the processed profile format changelog in
// `docs-developer/CHANGELOG-formats.md`.
-export const PROCESSED_PROFILE_VERSION = 58;
+export const PROCESSED_PROFILE_VERSION = 59;
// The following are the margin sizes for the left and right of the timeline. Independent
// components need to share these values.
diff --git a/src/components/app/AppHeader.css b/src/components/app/AppHeader.css
index 71867d69b5..85457a9f8d 100644
--- a/src/components/app/AppHeader.css
+++ b/src/components/app/AppHeader.css
@@ -6,6 +6,7 @@
--internal-border-color: #ddd;
display: flex;
+ align-items: center;
justify-content: space-between;
padding-bottom: 0.5em;
border-bottom: 1px solid var(--internal-border-color);
@@ -41,15 +42,19 @@
font-weight: normal;
}
+.appHeaderRightControls {
+ display: flex;
+ align-items: center;
+ gap: 0.5em;
+}
+
.appHeaderGithubIcon {
+ display: inline-flex;
+ align-items: center;
margin: 0 0.2em;
transition: opacity 250ms var(--animation-curve);
}
-.appHeaderGithubIcon svg {
- vertical-align: middle;
-}
-
.appHeaderGithubIcon:hover {
opacity: 0.5;
}
diff --git a/src/components/app/AppHeader.tsx b/src/components/app/AppHeader.tsx
index 797c65471e..6dc0eb9649 100644
--- a/src/components/app/AppHeader.tsx
+++ b/src/components/app/AppHeader.tsx
@@ -9,6 +9,7 @@
import * as React from 'react';
import { InnerNavigationLink } from 'firefox-profiler/components/shared/InnerNavigationLink';
+import { ThemeToggle } from './ThemeToggle';
import './AppHeader.css';
import { Localized } from '@fluent/react';
@@ -42,29 +43,32 @@ export class AppHeader extends React.PureComponent<{}> {
-
-
-
-
-
+
+
+
+
);
diff --git a/src/components/app/BottomBox.css b/src/components/app/BottomBox.css
index d337882d52..dc80d0345b 100644
--- a/src/components/app/BottomBox.css
+++ b/src/components/app/BottomBox.css
@@ -33,7 +33,7 @@
/* Provide 3px extra grabbable surface on each side of the splitter */
.bottom-box .layout-splitter::before {
position: absolute;
- z-index: 1;
+ z-index: var(--z-bottom-box);
display: block;
content: '';
inset: 0 -3px;
diff --git a/src/components/app/Home.css b/src/components/app/Home.css
index 70b55808b2..fe2fb6c878 100644
--- a/src/components/app/Home.css
+++ b/src/components/app/Home.css
@@ -28,7 +28,7 @@
--internal-special-border-color: var(--grey-40);
--internal-screenshot-border-color: var(--grey-40);
--internal-screenshot-shadow-color: var(--grey-90);
- --internal-button-disabled-foreground-color: var(--grey-10-a40);
+ --internal-button-disabled-foreground-color: var(--grey-10-a60);
--internal-button-enabled-foreground-color: var(--blue-40);
--internal-docs-icon: url(../../../res/img/svg/help-blue-light.svg);
}
diff --git a/src/components/app/KeyboardShortcut.css b/src/components/app/KeyboardShortcut.css
index 3898f85c5a..8b9ba8fab2 100644
--- a/src/components/app/KeyboardShortcut.css
+++ b/src/components/app/KeyboardShortcut.css
@@ -8,6 +8,7 @@
--internal-shadow-color: rgb(0 0 0 / 0.4);
position: absolute;
+ z-index: var(--z-keyboard-shortcuts);
top: 0;
left: 0;
display: none;
diff --git a/src/components/app/MenuButtons/MetaInfo.tsx b/src/components/app/MenuButtons/MetaInfo.tsx
index 8defaf2ef9..799980acf4 100644
--- a/src/components/app/MenuButtons/MetaInfo.tsx
+++ b/src/components/app/MenuButtons/MetaInfo.tsx
@@ -353,7 +353,7 @@ class MetaInfoPanelImpl extends React.PureComponent {
{meta.profilingStartTime ? (
-
+
Uptime:
diff --git a/src/components/app/ProfileViewer.css b/src/components/app/ProfileViewer.css
index 36b349703b..1fdf52b78f 100644
--- a/src/components/app/ProfileViewer.css
+++ b/src/components/app/ProfileViewer.css
@@ -110,11 +110,11 @@
}
.profileViewerSplitter {
- /* Create a stacking context, so that the KeyboardShortcut can overlay the profile
- viewer. In addition, the built-in class uses position: absolute, which is not
- appropriate here. */
+ /* Position relative for layout. Don't set z-index here to avoid creating a
+ stacking context that would trap the context menu below the screenshot hover.
+ This allows both the screenshot hover and context menu to be compared
+ at the .profileViewer level, ensuring the context menu appears on top. */
position: relative;
- z-index: 0;
}
.profileViewerSplitter > .layout-pane:not(.layout-pane-primary) {
diff --git a/src/components/app/ProfileViewer.tsx b/src/components/app/ProfileViewer.tsx
index dcfeafcb9c..e00a87117d 100644
--- a/src/components/app/ProfileViewer.tsx
+++ b/src/components/app/ProfileViewer.tsx
@@ -103,7 +103,6 @@ class ProfileViewerImpl extends PureComponent {
} as React.CSSProperties)
}
>
-
{hasZipFile ? (
{this._renderCategoryTimings(timings)}
diff --git a/src/components/tooltip/Tooltip.css b/src/components/tooltip/Tooltip.css
index a2a3c150b2..230085ffc2 100644
--- a/src/components/tooltip/Tooltip.css
+++ b/src/components/tooltip/Tooltip.css
@@ -177,3 +177,16 @@
.sidebar .tooltipDetailSeparator {
display: none;
}
+
+.tooltipArgumentsLabel {
+ /* match tooltips label, without being aligned to the right */
+ color: var(--grey-50);
+}
+
+.tooltipArguments > span {
+ display: block;
+}
+
+.tooltipArguments > span:nth-child(odd) {
+ background-color: rgb(0 0 0 / 0.05);
+}
diff --git a/src/components/tooltip/TrackPower.tsx b/src/components/tooltip/TrackPower.tsx
index 062abd0aab..984d98f564 100644
--- a/src/components/tooltip/TrackPower.tsx
+++ b/src/components/tooltip/TrackPower.tsx
@@ -83,7 +83,7 @@ class TooltipTrackPowerImpl extends React.PureComponent {
l10nIdKiloUnit: string,
l10nIdUnit: string,
l10nIdMilliUnit: string,
- l10nIdMicroUnit?: string
+ l10nIdMicroUnit: string
): React.ReactElement {
let value, l10nId, carbonValue;
const carbon = this._computeCO2eFromPower(power);
@@ -99,7 +99,7 @@ class TooltipTrackPowerImpl extends React.PureComponent {
value = 0;
carbonValue = 0;
l10nId = l10nIdUnit;
- } else if (power < 0.001 && l10nIdMicroUnit) {
+ } else if (power < 0.001) {
value = formatNumber(power * 1000000);
// Note: even though the power value is expressed in µWh, the carbon value
// is still expressed in mg.
@@ -190,7 +190,8 @@ class TooltipTrackPowerImpl extends React.PureComponent {
power,
'TrackPower--tooltip-power-kilowatt',
'TrackPower--tooltip-power-watt',
- 'TrackPower--tooltip-power-milliwatt'
+ 'TrackPower--tooltip-power-milliwatt',
+ 'TrackPower--tooltip-power-microwatt'
)}
{this.maybeRenderForPreviewSelection(previewSelection)}
{this._formatPowerValue(
diff --git a/src/index.tsx b/src/index.tsx
index 51421b2551..30658b2910 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -13,6 +13,7 @@ import '../res/css/global.css';
import '../res/css/categories.css';
import '../res/css/network.css';
import 'react-splitter-layout/lib/index.css';
+import 'devtools-reps/reps.css';
// React imported for JSX in Root component
import { createRoot } from 'react-dom/client';
@@ -93,7 +94,11 @@ if (svgFiltersElement) {
const forcedColorsMql = window.matchMedia('(forced-colors: active)');
const darkSchemeMql = window.matchMedia('(prefers-color-scheme: dark)');
forcedColorsMql.addEventListener('change', defineSvgFiltersForColors);
- darkSchemeMql.addEventListener('change', defineSvgFiltersForColors);
+ darkSchemeMql.addEventListener('change', () => {
+ defineSvgFiltersForColors();
+ // Re-apply theme when system preference changes (for 'system' mode users)
+ initTheme();
+ });
}
const store = createStore();
diff --git a/src/profile-logic/merge-compare.ts b/src/profile-logic/merge-compare.ts
index bfd339b3e3..b5f650baa9 100644
--- a/src/profile-logic/merge-compare.ts
+++ b/src/profile-logic/merge-compare.ts
@@ -67,6 +67,7 @@ import type {
MarkerIndex,
Milliseconds,
Tid,
+ RawProfileSharedData,
} from 'firefox-profiler/types';
/**
@@ -134,40 +135,23 @@ export function mergeProfilesForDiffing(
);
}
- // First let's merge categories. We'll use the resulting maps when
- // handling the thread data later.
+ // First, let's merge profile.meta.categories, profile.libs, and profile.shared.
const {
categories: newCategories,
- translationMaps: translationMapsForCategories,
- } = mergeCategories(profiles.map((profile) => profile.meta.categories));
+ libs: newLibs,
+ shared: newShared,
+ translationMapsForCategories,
+ translationMapsForLibs,
+ translationMapsForStrings,
+ translationMapsForSources,
+ } = mergeSharedData(profiles);
resultProfile.meta.categories = newCategories;
-
- const {
- stringArray: newStringArray,
- translationMaps: translationMapForStrings,
- } = mergeStringArrays(profiles.map((profile) => profile.shared.stringArray));
-
- // Then merge sources.
- const { sources: newSources, translationMaps: translationMapForSources } =
- mergeSources(
- profiles.map((profile) => profile.shared.sources ?? null),
- translationMapForStrings
- );
-
- // Then merge libs.
- const { libs: newLibs, translationMaps: translationMapsForLibs } = mergeLibs(
- profiles.map((profile) => profile.libs)
- );
resultProfile.libs = newLibs;
-
- resultProfile.shared = {
- stringArray: newStringArray,
- sources: newSources,
- };
+ resultProfile.shared = newShared;
// Then we loop over all profiles and do the necessary changes according
// to the states we computed earlier.
- const transformStacks: any = {};
+ const transformStacks: TransformStacksPerThread = {};
const implementationFilters: ImplementationFilter[] = [];
// These may be needed for filtering markers.
let ipcCorrelations;
@@ -203,22 +187,22 @@ export function mergeProfilesForDiffing(
...thread.funcTable,
name: adjustStringIndexes(
thread.funcTable.name,
- translationMapForStrings[i]
+ translationMapsForStrings[i]
),
source: adjustNullableSourceIndexes(
thread.funcTable.source,
- translationMapForSources[i]
+ translationMapsForSources[i]
),
};
thread.resourceTable = {
...thread.resourceTable,
name: adjustStringIndexes(
thread.resourceTable.name,
- translationMapForStrings[i]
+ translationMapsForStrings[i]
),
host: adjustNullableStringIndexes(
thread.resourceTable.host,
- translationMapForStrings[i]
+ translationMapsForStrings[i]
),
lib: adjustResourceTableLibs(
thread.resourceTable.lib,
@@ -229,7 +213,7 @@ export function mergeProfilesForDiffing(
...thread.nativeSymbols,
name: adjustStringIndexes(
thread.nativeSymbols.name,
- translationMapForStrings[i]
+ translationMapsForStrings[i]
),
libIndex: adjustNativeSymbolLibs(
thread.nativeSymbols.libIndex,
@@ -240,11 +224,11 @@ export function mergeProfilesForDiffing(
...thread.markers,
name: adjustStringIndexes(
thread.markers.name,
- translationMapForStrings[i]
+ translationMapsForStrings[i]
),
data: adjustMarkerDataStringIndexes(
thread.markers.data,
- translationMapForStrings[i],
+ translationMapsForStrings[i],
stringIndexMarkerFieldsByDataType
),
};
@@ -475,6 +459,50 @@ function mergeCategories(categoriesPerProfile: Array): {
return { categories: newCategories, translationMaps };
}
+export function mergeSharedData(profiles: Profile[]): {
+ categories: CategoryList;
+ libs: Lib[];
+ shared: RawProfileSharedData;
+ translationMapsForCategories: TranslationMapForCategories[];
+ translationMapsForLibs: TranslationMapForLibs[];
+ translationMapsForStrings: TranslationMapForStrings[];
+ translationMapsForSources: TranslationMapForSources[];
+} {
+ const {
+ categories: newCategories,
+ translationMaps: translationMapsForCategories,
+ } = mergeCategories(profiles.map((profile) => profile.meta.categories));
+
+ const {
+ stringArray: newStringArray,
+ translationMaps: translationMapsForStrings,
+ } = mergeStringArrays(profiles.map((profile) => profile.shared.stringArray));
+
+ const { libs: newLibs, translationMaps: translationMapsForLibs } = mergeLibs(
+ profiles.map((profile) => profile.libs)
+ );
+
+ const { sources: newSources, translationMaps: translationMapsForSources } =
+ mergeSources(
+ profiles.map((profile) => profile.shared.sources ?? null),
+ translationMapsForStrings
+ );
+ const newShared: RawProfileSharedData = {
+ stringArray: newStringArray,
+ sources: newSources,
+ };
+
+ return {
+ categories: newCategories,
+ libs: newLibs,
+ shared: newShared,
+ translationMapsForCategories,
+ translationMapsForLibs,
+ translationMapsForStrings,
+ translationMapsForSources,
+ };
+}
+
function mergeStringArrays(stringArraysPerProfile: Array): {
stringArray: string[];
translationMaps: TranslationMapForStrings[];
diff --git a/src/profile-logic/process-profile.ts b/src/profile-logic/process-profile.ts
index a1049c0cab..755c639813 100644
--- a/src/profile-logic/process-profile.ts
+++ b/src/profile-logic/process-profile.ts
@@ -1015,6 +1015,10 @@ function _processSamples(geckoSamples: GeckoSampleStruct): RawSamplesTable {
}
}
+ if ('argumentValues' in geckoSamples) {
+ samples.argumentValues = geckoSamples.argumentValues;
+ }
+
if ('eventDelay' in geckoSamples) {
samples.eventDelay = geckoSamples.eventDelay;
} else if ('responsiveness' in geckoSamples) {
@@ -1210,6 +1214,29 @@ function _processThread(
);
const samples = _processSamples(geckoSamples);
+ // Compute usedInnerWindowIDs from the frameTable and the markers.
+ let usedInnerWindowIDs: number[] | undefined;
+ const usedInnerWindowIDsSet = new Set();
+ for (let i = 0; i < frameTable.length; i++) {
+ const innerWindowID = frameTable.innerWindowID[i];
+ if (innerWindowID !== null && innerWindowID !== 0) {
+ usedInnerWindowIDsSet.add(innerWindowID);
+ }
+ }
+ for (let i = 0; i < markers.length; i++) {
+ const data = markers.data[i];
+ if (!data || !('innerWindowID' in data)) {
+ continue;
+ }
+ const innerWindowID = data.innerWindowID;
+ if (typeof innerWindowID === 'number' && innerWindowID !== 0) {
+ usedInnerWindowIDsSet.add(innerWindowID);
+ }
+ }
+ if (usedInnerWindowIDsSet.size !== 0) {
+ usedInnerWindowIDs = Array.from(usedInnerWindowIDsSet);
+ }
+
const newThread: RawThread = {
name: thread.name,
isMainThread: thread.name === 'GeckoMain',
@@ -1245,6 +1272,10 @@ function _processThread(
newThread.userContextId = thread.userContextId;
}
+ if (usedInnerWindowIDs !== undefined) {
+ newThread.usedInnerWindowIDs = usedInnerWindowIDs;
+ }
+
if (jsAllocations) {
// Only add the JS allocations if they exist.
newThread.jsAllocations = jsAllocations;
@@ -1255,6 +1286,14 @@ function _processThread(
newThread.nativeAllocations = nativeAllocations;
}
+ if (thread.tracedValues) {
+ newThread.tracedValuesBuffer = thread.tracedValues;
+ }
+
+ if (thread.tracedObjectShapes) {
+ newThread.tracedObjectShapes = thread.tracedObjectShapes;
+ }
+
function processJsTracer() {
// Optionally extract the JS Tracer information, if they exist.
const { jsTracerEvents } = thread;
diff --git a/src/profile-logic/processed-profile-versioning.ts b/src/profile-logic/processed-profile-versioning.ts
index 2448b486e7..8e341a4fab 100644
--- a/src/profile-logic/processed-profile-versioning.ts
+++ b/src/profile-logic/processed-profile-versioning.ts
@@ -2693,6 +2693,39 @@ const _upgraders: {
}
profile.shared.sources = sourceTable;
},
+ [59]: (profile) => {
+ // Add usedInnerWindowIDs to each thread by computing it from the frameTable and the markers.
+ // This changes the profile format from having innerWindowIDs implicitly
+ // defined by the frameTable / markers to having them explicitly listed in
+ // thread.usedInnerWindowIDs.
+ for (const thread of profile.threads) {
+ const { frameTable, markers } = thread;
+ const usedInnerWindowIDsSet = new Set();
+
+ // Collect all unique innerWindowIDs from the frameTable
+ for (let i = 0; i < frameTable.length; i++) {
+ const innerWindowID = frameTable.innerWindowID[i];
+ if (innerWindowID !== null && innerWindowID !== 0) {
+ usedInnerWindowIDsSet.add(innerWindowID);
+ }
+ }
+ for (let i = 0; i < markers.length; i++) {
+ const data = markers.data[i];
+ if (!data || !('innerWindowID' in data)) {
+ continue;
+ }
+ const innerWindowID = data.innerWindowID;
+ if (typeof innerWindowID === 'number' && innerWindowID !== 0) {
+ usedInnerWindowIDsSet.add(innerWindowID);
+ }
+ }
+
+ // Convert the set to an array and store it on the thread, if non-empty
+ if (usedInnerWindowIDsSet.size !== 0) {
+ thread.usedInnerWindowIDs = Array.from(usedInnerWindowIDsSet);
+ }
+ }
+ },
// If you add a new upgrader here, please document the change in
// `docs-developer/CHANGELOG-formats.md`.
};
diff --git a/src/profile-logic/profile-data.ts b/src/profile-logic/profile-data.ts
index 3be6925778..6e247fad55 100644
--- a/src/profile-logic/profile-data.ts
+++ b/src/profile-logic/profile-data.ts
@@ -1882,6 +1882,13 @@ export function filterThreadSamplesToRange(
);
}
+ if (samples.argumentValues) {
+ newSamples.argumentValues = samples.argumentValues.slice(
+ beginSampleIndex,
+ endSampleIndex
+ );
+ }
+
if (samples.threadId) {
newSamples.threadId = samples.threadId.slice(
beginSampleIndex,
@@ -2008,6 +2015,13 @@ export function filterRawThreadSamplesToRange(
);
}
+ if (samples.argumentValues) {
+ newSamples.argumentValues = samples.argumentValues.slice(
+ beginSampleIndex,
+ endSampleIndex
+ );
+ }
+
if (samples.threadId) {
newSamples.threadId = samples.threadId.slice(
beginSampleIndex,
@@ -2114,6 +2128,9 @@ export function filterCounterSamplesToRange(
number: samples.number
? samples.number.slice(beginSampleIndex, endSampleIndex)
: undefined,
+ argumentValues: samples.argumentValues
+ ? samples.argumentValues.slice(beginSampleIndex, endSampleIndex)
+ : undefined,
};
return newCounter;
@@ -2438,6 +2455,7 @@ export function computeSamplesTableFromRawSamplesTable(
const {
responsiveness,
eventDelay,
+ argumentValues,
stack,
weight,
weightType,
@@ -2459,6 +2477,7 @@ export function computeSamplesTableFromRawSamplesTable(
// These fields are copied from the raw samples table:
responsiveness,
eventDelay,
+ argumentValues,
stack,
weight,
weightType,
@@ -2479,7 +2498,8 @@ export function createThreadFromDerivedTables(
samples: SamplesTable,
stackTable: StackTable,
stringTable: StringTable,
- sources: SourceTable
+ sources: SourceTable,
+ tracedValuesBuffer: ArrayBuffer | undefined
): Thread {
const {
processType,
@@ -2506,6 +2526,7 @@ export function createThreadFromDerivedTables(
jsTracer,
isPrivateBrowsing,
userContextId,
+ tracedObjectShapes,
} = rawThread;
const thread: Thread = {
@@ -2534,12 +2555,14 @@ export function createThreadFromDerivedTables(
jsTracer,
isPrivateBrowsing,
userContextId,
+ tracedObjectShapes,
// These fields are derived:
samples,
stackTable,
stringTable,
sources,
+ tracedValuesBuffer,
};
return thread;
}
@@ -4098,17 +4121,19 @@ export function computeTabToThreadIndexesMap(
return tabToThreadIndexesMap;
}
- // We need to iterate over all the samples and markers once to figure out
- // which innerWindowIDs are present in each thread. This is probably not
- // very cheap, but it'll allow us to not compute this information every
- // time when we need it.
+ // Iterate over the usedInnerWindowIDs for each thread to figure out
+ // which threads are involved for each tab.
for (let threadIdx = 0; threadIdx < threads.length; threadIdx++) {
const thread = threads[threadIdx];
+ const { usedInnerWindowIDs } = thread;
+
+ if (!usedInnerWindowIDs) {
+ // No innerWindowIDs for this thread
+ continue;
+ }
- // 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 || innerWindowID === 0) {
+ for (const innerWindowID of usedInnerWindowIDs) {
+ if (innerWindowID === 0) {
// Zero value also means null for innerWindowID.
continue;
}
@@ -4127,38 +4152,6 @@ export function computeTabToThreadIndexesMap(
}
threadIndexes.add(threadIdx);
}
-
- // Then go over the markers to find their innerWindowIDs.
- for (let i = 0; i < thread.markers.length; i++) {
- const markerData = thread.markers.data[i];
-
- if (!markerData) {
- continue;
- }
-
- if (
- 'innerWindowID' in markerData &&
- markerData.innerWindowID !== null &&
- markerData.innerWindowID !== undefined &&
- // Zero value also means null for innerWindowID.
- markerData.innerWindowID !== 0
- ) {
- const innerWindowID = markerData.innerWindowID;
- const tabID = innerWindowIDToTabMap.get(innerWindowID);
- if (tabID === undefined) {
- // We couldn't find the tab of this innerWindowID, this should
- // never happen, it might indicate a bug in Firefox.
- continue;
- }
-
- let threadIndexes = tabToThreadIndexesMap.get(tabID);
- if (!threadIndexes) {
- threadIndexes = new Set();
- tabToThreadIndexesMap.set(tabID, threadIndexes);
- }
- threadIndexes.add(threadIdx);
- }
- }
}
return tabToThreadIndexesMap;
diff --git a/src/profile-logic/sanitize.ts b/src/profile-logic/sanitize.ts
index e9eefdd8f7..51f2d29728 100644
--- a/src/profile-logic/sanitize.ts
+++ b/src/profile-logic/sanitize.ts
@@ -411,6 +411,9 @@ function sanitizeThreadPII(
delete newThread['eTLD+1'];
}
+ delete newThread.tracedValuesBuffer;
+ delete newThread.tracedObjectShapes;
+
if (windowIdFromPrivateBrowsing.size > 0) {
// In this block, we'll remove everything related to frame table entries
// that have a innerWindowID with a isPrivateBrowsing flag.
diff --git a/src/profile-logic/stack-timing.ts b/src/profile-logic/stack-timing.ts
index 7ea7f20991..3f6f97c8f0 100644
--- a/src/profile-logic/stack-timing.ts
+++ b/src/profile-logic/stack-timing.ts
@@ -87,6 +87,9 @@ export type StackTiming = {
sameWidthsStart: number[];
sameWidthsEnd: number[];
callNode: IndexIntoCallNodeTable[];
+ // argumentValues is used by the JS Execution Tracing setting and allows
+ // displaying function calls' argument values.
+ argumentValues?: number[];
length: number;
};
@@ -115,14 +118,20 @@ export function getStackTimingByDepth(
} = callNodeTable;
const stackTimingByDepth: StackTimingByDepth = Array.from(
{ length: maxDepthPlusOne },
- (): StackTiming => ({
- start: [],
- end: [],
- sameWidthsStart: [],
- sameWidthsEnd: [],
- callNode: [],
- length: 0,
- })
+ (): StackTiming => {
+ const shape: StackTiming = {
+ start: [],
+ end: [],
+ sameWidthsStart: [],
+ sameWidthsEnd: [],
+ callNode: [],
+ length: 0,
+ };
+ if ('argumentValues' in samples) {
+ shape.argumentValues = [];
+ }
+ return shape;
+ }
);
const sameWidthsIndexToTimestampMap: SameWidthsIndexToTimestampMap = [];
@@ -154,6 +163,7 @@ export function getStackTimingByDepth(
let deepestOpenBoxDepth = -1;
const openBoxStartTimeByDepth = new Float64Array(maxDepthPlusOne);
const openBoxStartTickByDepth = new Float64Array(maxDepthPlusOne);
+ const openBoxArgsByDepth = new Int32Array(maxDepthPlusOne);
let currentStackTick = 0;
for (let sampleIndex = 0; sampleIndex < samples.length; sampleIndex++) {
@@ -162,6 +172,14 @@ export function getStackTimingByDepth(
continue;
}
+ let sampleArgs: number = -1;
+ if ('argumentValues' in samples && samples.argumentValues !== undefined) {
+ const val = samples.argumentValues[sampleIndex];
+ if (val !== null) {
+ sampleArgs = val;
+ }
+ }
+
const sampleTime = samples.time[sampleIndex];
// Phase 1: Commit open boxes which are not shared by the current call node,
@@ -193,6 +211,10 @@ export function getStackTimingByDepth(
stackTimingForThisDepth.sameWidthsStart[index] = startStackTick;
stackTimingForThisDepth.sameWidthsEnd[index] = currentStackTick;
stackTimingForThisDepth.callNode[index] = deepestOpenBoxCallNodeIndex;
+ if (stackTimingForThisDepth.argumentValues) {
+ stackTimingForThisDepth.argumentValues[index] =
+ openBoxArgsByDepth[deepestOpenBoxDepth];
+ }
deepestOpenBoxCallNodeIndex =
callNodeTablePrefixColumn[deepestOpenBoxCallNodeIndex];
deepestOpenBoxDepth--;
@@ -208,6 +230,12 @@ export function getStackTimingByDepth(
deepestOpenBoxDepth++;
openBoxStartTimeByDepth[deepestOpenBoxDepth] = sampleTime;
openBoxStartTickByDepth[deepestOpenBoxDepth] = currentStackTick;
+ if (
+ 'argumentValues' in samples &&
+ samples.argumentValues !== undefined
+ ) {
+ openBoxArgsByDepth[deepestOpenBoxDepth] = sampleArgs;
+ }
}
}
@@ -229,6 +257,10 @@ export function getStackTimingByDepth(
stackTimingForThisDepth.sameWidthsStart[index] = startStackTick;
stackTimingForThisDepth.sameWidthsEnd[index] = currentStackTick;
stackTimingForThisDepth.callNode[index] = deepestOpenBoxCallNodeIndex;
+ if (stackTimingForThisDepth.argumentValues) {
+ stackTimingForThisDepth.argumentValues[index] =
+ openBoxArgsByDepth[deepestOpenBoxDepth];
+ }
deepestOpenBoxCallNodeIndex =
callNodeTablePrefixColumn[deepestOpenBoxCallNodeIndex];
deepestOpenBoxDepth--;
diff --git a/src/selectors/per-thread/thread.tsx b/src/selectors/per-thread/thread.tsx
index fc8b619b89..e6fec6c67d 100644
--- a/src/selectors/per-thread/thread.tsx
+++ b/src/selectors/per-thread/thread.tsx
@@ -18,6 +18,7 @@ import {
ensureExists,
getFirstItemFromSet,
} from '../../utils/types';
+import { base64StringToBytes } from '../../utils/base64';
import type {
Thread,
@@ -92,6 +93,12 @@ export function getBasicThreadSelectorsPerThread(
? ProfileSelectors.getProfile(state).threads[singleThreadIndex]
: getMergedRawThread(state);
+ const getTracedValuesBuffer: Selector =
+ createSelector(
+ (state: State) => getRawThread(state).tracedValuesBuffer,
+ (tracedValuesBuffer: string | undefined) =>
+ tracedValuesBuffer ? base64StringToBytes(tracedValuesBuffer) : undefined
+ );
const getRawSamplesTable: Selector = (state) =>
getRawThread(state).samples;
const getSamplesTable: Selector = createSelector(
@@ -147,6 +154,7 @@ export function getBasicThreadSelectorsPerThread(
getStackTable,
ProfileSelectors.getStringTable,
ProfileSelectors.getSourceTable,
+ getTracedValuesBuffer,
ProfileData.createThreadFromDerivedTables
);
@@ -386,6 +394,7 @@ export function getBasicThreadSelectorsPerThread(
getRawThread,
getThread,
getSamplesTable,
+ getTracedValuesBuffer,
getSamplesWeightType,
getNativeAllocations,
getJsAllocations,
diff --git a/src/test/components/StackChart.test.tsx b/src/test/components/StackChart.test.tsx
index 19b0a69a56..e8a2769361 100644
--- a/src/test/components/StackChart.test.tsx
+++ b/src/test/components/StackChart.test.tsx
@@ -63,6 +63,7 @@ import {
import { autoMockElementSize } from '../fixtures/mocks/element-size';
import type { CssPixels, Store } from 'firefox-profiler/types';
+import { unserializeProfileOfArbitraryFormat } from 'firefox-profiler/profile-logic/process-profile';
jest.useFakeTimers();
@@ -314,6 +315,21 @@ describe('StackChart', function () {
});
});
+describe('ArgumentValues', () => {
+ it('shows argument values when a profile has them', async () => {
+ const profiler = await unserializeProfileOfArbitraryFormat(
+ require('../fixtures/upgrades/argument-values.json')
+ );
+ const store = storeWithProfile(profiler);
+ const { getTooltip, moveMouse, findFillTextPosition } = setup(store);
+
+ moveMouse(findFillTextPosition('bar'));
+ const tooltip = getTooltip();
+ expect(within(ensureExists(tooltip)).getByText('bar')).toBeInTheDocument();
+ expect(tooltip).toMatchSnapshot();
+ });
+});
+
describe('MarkerChart', function () {
it('can turn on the show user timings', () => {
const { getByLabelText, getState } = setupUserTimings({
diff --git a/src/test/components/TabSelectorMenu.test.tsx b/src/test/components/TabSelectorMenu.test.tsx
index b8689d3b41..c1f2f618e5 100644
--- a/src/test/components/TabSelectorMenu.test.tsx
+++ b/src/test/components/TabSelectorMenu.test.tsx
@@ -32,21 +32,20 @@ describe('app/TabSelectorMenu', () => {
threadCPUDelta: 'ns',
};
- // 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.
+ // Associate the threads with innerWindowIDs now.
//
// 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[0].usedInnerWindowIDs = [
+ extraPageData.parentInnerWindowIDsWithChildren,
+ ];
// Add a threadCPUDelta value for thread activity score.
profile.threads[0].samples.threadCPUDelta = [1];
- profile.threads[1].frameTable.innerWindowID[0] =
- extraPageData.secondTabInnerWindowIDs[0];
- profile.threads[1].frameTable.length++;
+ profile.threads[1].usedInnerWindowIDs = [
+ extraPageData.secondTabInnerWindowIDs[0],
+ ];
// Add a threadCPUDelta value for thread activity score. This thread
// should stay above the first thread.
profile.threads[0].samples.threadCPUDelta = [2];
@@ -196,13 +195,11 @@ describe('app/TabSelectorMenu', () => {
url: removeURLs(page.url, ``),
}));
- // Attach innerWindowIDs to the samples.
- profile.threads[0].frameTable.innerWindowID[0] =
- extraPageData.parentInnerWindowIDsWithChildren;
- profile.threads[0].frameTable.length++;
- profile.threads[0].frameTable.innerWindowID[1] =
- extraPageData.secondTabInnerWindowIDs[0];
- profile.threads[0].frameTable.length++;
+ // Attach innerWindowIDs to the threads.
+ profile.threads[0].usedInnerWindowIDs = [
+ extraPageData.parentInnerWindowIDsWithChildren,
+ extraPageData.secondTabInnerWindowIDs[0],
+ ];
const store = storeWithProfile(profile);
render(
diff --git a/src/test/components/TooltipCallnode.test.tsx b/src/test/components/TooltipCallnode.test.tsx
index ee2853bbbc..ddf6f409b8 100644
--- a/src/test/components/TooltipCallnode.test.tsx
+++ b/src/test/components/TooltipCallnode.test.tsx
@@ -4,7 +4,7 @@
import { Provider } from 'react-redux';
-import { render } from 'firefox-profiler/test/fixtures/testing-library';
+import { render, screen } from 'firefox-profiler/test/fixtures/testing-library';
import { TooltipCallNode } from '../../components/tooltip/CallNode';
import { storeWithProfile } from '../fixtures/stores';
import {
@@ -117,10 +117,12 @@ describe('TooltipCallNode', function () {
const { frameTable } = profile.threads[threadIndex];
+ const innerWindowID =
+ profile.pages[profile.pages.length - 1].innerWindowID;
for (let i = 1; i < frameTable.length; i++) {
- frameTable.innerWindowID[i] =
- profile.pages[profile.pages.length - 1].innerWindowID;
+ frameTable.innerWindowID[i] = innerWindowID;
}
+ profile.threads[threadIndex].usedInnerWindowIDs = [innerWindowID];
const callNodePath = [A, Bjs, Cjs];
const { dispatch, renderTooltip } = setup(profile);
@@ -165,4 +167,72 @@ describe('TooltipCallNode', function () {
expect(displayedUrl).toHaveTextContent(`${pageUrl} (private)`);
});
});
+
+ describe('with argument values', function () {
+ function setupWithArguments(argumentValues?: Array) {
+ const {
+ profile,
+ funcNamesDictPerThread: [{ A, Bjs, Cjs }],
+ } = getProfileFromTextSamples(`
+ A
+ Bjs
+ Cjs
+ `);
+ const threadIndex = 0;
+ const callNodePath = [A, Bjs, Cjs];
+
+ const store = storeWithProfile(profile);
+ const { getState, dispatch } = store;
+ dispatch(changeSelectedCallNode(threadIndex, callNodePath));
+
+ const callTree = selectedThreadSelectors.getCallTree(getState());
+ const callNodeIndex = ensureExists(
+ selectedThreadSelectors.getSelectedCallNodeIndex(getState()),
+ 'Unable to find a selected call node index.'
+ );
+ const displayData = callTree.getDisplayData(callNodeIndex);
+
+ return render(
+
+
+
+ );
+ }
+
+ it('does not display arguments when argumentValues is undefined', () => {
+ setupWithArguments(undefined);
+ expect(screen.queryByText('Arguments:')).not.toBeInTheDocument();
+ });
+
+ it('displays em dash when argumentValues is empty', () => {
+ const { getByText, container } = setupWithArguments([]);
+ expect(getByText('Arguments:')).toBeInTheDocument();
+ expect(getByText('—')).toBeInTheDocument();
+ expect(container.firstChild).toMatchSnapshot();
+ });
+
+ it('displays argument values when provided', () => {
+ const argumentValues = [42, 'hello'];
+ const { getByText, container } = setupWithArguments(argumentValues);
+ expect(getByText('Arguments:')).toBeInTheDocument();
+ expect(container.firstChild).toMatchSnapshot();
+ });
+ });
});
diff --git a/src/test/components/__snapshots__/CompareHome.test.tsx.snap b/src/test/components/__snapshots__/CompareHome.test.tsx.snap
index b2f257169e..fb4484b036 100644
--- a/src/test/components/__snapshots__/CompareHome.test.tsx.snap
+++ b/src/test/components/__snapshots__/CompareHome.test.tsx.snap
@@ -24,27 +24,65 @@ exports[`app/CompareHome matches the snapshot 1`] = `
Web app for Firefox performance analysis
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+