diff --git a/docs-developer/CHANGELOG-formats.md b/docs-developer/CHANGELOG-formats.md
index e07d501aa6..b59d5fe0e3 100644
--- a/docs-developer/CHANGELOG-formats.md
+++ b/docs-developer/CHANGELOG-formats.md
@@ -6,6 +6,10 @@ Note that this is not an exhaustive list. Processed profile format upgraders can
## Processed profile format
+### Version 65
+
+The stack table's `frame` column (stored at `profile.shared.stackTable.frame`) can now optionally be stored as an `Int32Array`, for profiles loaded from [JsonSlabs](https://github.com/mstange/json-slabs/) files (.jslb, .jslb.gz). Regular JS / JSON arrays are still accepted.
+
### Version 64
A new `SourceLocationTable` has been added to `profile.shared.sourceLocationTable`. It holds the original (pre-compilation) source positions produced by source map symbolication, paired with the generated `line`/`column` already on `FrameTable`.
diff --git a/locales/de/app.ftl b/locales/de/app.ftl
index 85f964073f..178ac76272 100644
--- a/locales/de/app.ftl
+++ b/locales/de/app.ftl
@@ -46,6 +46,11 @@ AppViewRouter--error-compare = Die Profile konnten nicht abgerufen werden.
# https://profiler.firefox.com/from-url/http%3A%2F%2F127.0.0.1%3A3000%2Fprofile.json/
AppViewRouter--error-from-localhost-url-safari = Aufgrund einer spezifischen Einschränkung in Safari kann { -profiler-brand-name } Profile vom lokalen Computer nicht in diesem Browser importieren. Bitte öffnen Sie diese Seite stattdessen in { -firefox-brand-name } oder Chrome.
.title = Safari kann lokale Profile nicht importieren
+# This error message is displayed when the profile is in a newer format version
+# than this build of the Profiler is able to read.
+AppViewRouter--error-profile-version =
+ Dieses Profil verwendet ein Format, das von dieser Version von { -profiler-brand-name } nicht unterstützt wird.
+ Versuchen Sie, die Seite zu aktualisieren, um zu überprüfen, ob ein Update für { -profiler-brand-name } verfügbar ist.
AppViewRouter--route-not-found--home =
.specialMessage = Die URL, die Sie erreichen wollten, wurde nicht erkannt.
diff --git a/locales/el/app.ftl b/locales/el/app.ftl
index 5f879431ee..d3df514dce 100644
--- a/locales/el/app.ftl
+++ b/locales/el/app.ftl
@@ -49,6 +49,11 @@ AppViewRouter--error-from-localhost-url-safari =
εισαγάγει προφίλ από τη συσκευή σε αυτό το πρόγραμμα περιήγησης.
Ανοίξτε αυτήν τη σελίδα στο { -firefox-brand-name } ή το Chrome.
.title = Το Safari δεν μπορεί να εισαγάγει τοπικά προφίλ
+# This error message is displayed when the profile is in a newer format version
+# than this build of the Profiler is able to read.
+AppViewRouter--error-profile-version =
+ Αυτό το προφίλ χρησιμοποιεί μια μορφή που δεν υποστηρίζεται από αυτήν την έκδοση του { -profiler-brand-name }.
+ Δοκιμάστε να ανανεώσετε τη σελίδα για να ελέγξετε αν διατίθεται ενημέρωση για το { -profiler-brand-name }.
AppViewRouter--route-not-found--home =
.specialMessage = Δεν αναγνωρίστηκε το URL που προσπαθήσατε να μεταβείτε.
@@ -894,6 +899,10 @@ TrackMemoryGraph--relative-memory-at-this-time2 = { $value }
# $value (String) - the memory range across the graph (e.g. "5MB")
TrackMemoryGraph--memory-range-in-graph2 = { $value }
.label = εύρος μνήμης στο γράφημα
+# Variables:
+# $value (String) - count of allocations and deallocations since the previous sample
+TrackMemoryGraph--allocations-and-deallocations-since-the-previous-sample2 = { $value }
+ .label = εκχωρήσεις και αποδεσμεύσεις από το προηγούμενο δείγμα
## TrackProcessCPUGraph
## This is used to show the CPU usage of a process over time in the timeline.
@@ -948,6 +957,11 @@ TrackPower--tooltip-average-power-watt = { $value } W
# $value (String) - the power value at this location
TrackPower--tooltip-average-power-milliwatt = { $value } mW
.label = Μέση ισχύς στην τρέχουσα επιλογή
+# This is used in the tooltip when the power value uses the microwatt unit.
+# Variables:
+# $value (String) - the power value at this location
+TrackPower--tooltip-average-power-microwatt = { $value } μW
+ .label = Μέση ισχύς στην τρέχουσα επιλογή
# This is used in the tooltip when the energy used in the current range uses the
# kilowatt-hour unit.
# Variables:
diff --git a/locales/en-GB/app.ftl b/locales/en-GB/app.ftl
index 7ea46c3eba..e40039d84c 100644
--- a/locales/en-GB/app.ftl
+++ b/locales/en-GB/app.ftl
@@ -49,6 +49,11 @@ AppViewRouter--error-from-localhost-url-safari =
to import profiles from the local machine in this browser. Please open
this page in { -firefox-brand-name } or Chrome instead.
.title = Safari cannot import local profiles
+# This error message is displayed when the profile is in a newer format version
+# than this build of the Profiler is able to read.
+AppViewRouter--error-profile-version =
+ This profile uses a format that is not supported by this version of { -profiler-brand-name }.
+ Try refreshing the page to check if there is an update available for { -profiler-brand-name }.
AppViewRouter--route-not-found--home =
.specialMessage = The URL you tried to reach was not recognised.
diff --git a/locales/en-US/app.ftl b/locales/en-US/app.ftl
index 5625f479b5..86409569e7 100644
--- a/locales/en-US/app.ftl
+++ b/locales/en-US/app.ftl
@@ -54,6 +54,12 @@ AppViewRouter--error-from-localhost-url-safari =
this page in { -firefox-brand-name } or Chrome instead.
.title = Safari cannot import local profiles
+# This error message is displayed when the profile is in a newer format version
+# than this build of the Profiler is able to read.
+AppViewRouter--error-profile-version =
+ This profile uses a format that is not supported by this version of { -profiler-brand-name }.
+ Try refreshing the page to check if there is an update available for { -profiler-brand-name }.
+
AppViewRouter--route-not-found--home =
.specialMessage = The URL you tried to reach was not recognized.
diff --git a/locales/es-CL/app.ftl b/locales/es-CL/app.ftl
index f14e73b5ff..7591ba2890 100644
--- a/locales/es-CL/app.ftl
+++ b/locales/es-CL/app.ftl
@@ -46,6 +46,12 @@ AppViewRouter--error-compare = No se pudieron recuperar los perfiles.
# https://profiler.firefox.com/from-url/http%3A%2F%2F127.0.0.1%3A3000%2Fprofile.json/
AppViewRouter--error-from-localhost-url-safari = Debido a una limitación específica en Safari, { -profiler-brand-name } no puede importar perfiles de la máquina local en este navegador. Por favor, abre esta página en { -firefox-brand-name } o Chrome en su lugar.
.title = Safari no puede importar perfiles locales
+# This error message is displayed when the profile is in a newer format version
+# than this build of the Profiler is able to read.
+AppViewRouter--error-profile-version =
+ Este perfil utiliza un formato no compatible con esta versión de { -profiler-brand-name }.
+
+ Intenta actualizar la página para comprobar si hay alguna actualización disponible para { -profiler-brand-name }.
AppViewRouter--route-not-found--home =
.specialMessage = La URL a la que intentaste acceder no fue reconocida.
diff --git a/locales/fr/app.ftl b/locales/fr/app.ftl
index e96cdee9a3..4ac9c5e908 100644
--- a/locales/fr/app.ftl
+++ b/locales/fr/app.ftl
@@ -46,6 +46,11 @@ AppViewRouter--error-compare = Impossible de récupérer les profils.
# https://profiler.firefox.com/from-url/http%3A%2F%2F127.0.0.1%3A3000%2Fprofile.json/
AppViewRouter--error-from-localhost-url-safari = En raison d’une limitation spécifique à Safari, { -profiler-brand-name } ne peut pas importer de profils depuis la machine locale dans ce navigateur. Veuillez ouvrir cette page dans { -firefox-brand-name } ou Chrome à la place.
.title = Safari ne peut pas importer de profils locaux
+# This error message is displayed when the profile is in a newer format version
+# than this build of the Profiler is able to read.
+AppViewRouter--error-profile-version =
+ Ce profil utilise un format qui n’est pas pris en charge par cette version de { -profiler-brand-name }.
+ Essayez d’actualiser la page pour vérifier si une mise à jour est disponible pour { -profiler-brand-name }.
AppViewRouter--route-not-found--home =
.specialMessage = L’URL que vous avez tenté d’atteindre n’a pas été trouvée
diff --git a/locales/fur/app.ftl b/locales/fur/app.ftl
index 2f3c985016..3389b02751 100644
--- a/locales/fur/app.ftl
+++ b/locales/fur/app.ftl
@@ -811,6 +811,32 @@ TrackNameButton--hide-track =
TrackNameButton--hide-process =
.title = Plate procès
+## TrackMemoryGraph
+## This is used to show the memory graph of that process in the timeline part of
+## the UI. To learn more about it, visit:
+## https://profiler.firefox.com/docs/#/./memory-allocations?id=memory-track
+
+# Variables:
+# $value (String) - the relative memory at this time (e.g. "5MB")
+TrackMemoryGraph--relative-memory-at-this-time2 = { $value }
+ .label = memorie relative a chest pont
+# Variables:
+# $value (String) - the memory range across the graph (e.g. "5MB")
+TrackMemoryGraph--memory-range-in-graph2 = { $value }
+ .label = interval di memorie tal grafic
+# Variables:
+# $value (String) - count of allocations and deallocations since the previous sample
+TrackMemoryGraph--allocations-and-deallocations-since-the-previous-sample2 = { $value }
+ .label = assegnazion e disassegnazion dal campion di prime
+
+## TrackProcessCPUGraph
+## This is used to show the CPU usage of a process over time in the timeline.
+
+# Variables:
+# $value (String) - the CPU usage at this sample (e.g. "50%")
+TrackProcessCPUGraph--cpu = { $value }
+ .label = CPU
+
## TrackPower
## This is used to show the power used by the CPU and other chips in a computer,
## graphed over time.
@@ -856,6 +882,11 @@ TrackPower--tooltip-average-power-watt = { $value } W
# $value (String) - the power value at this location
TrackPower--tooltip-average-power-milliwatt = { $value } mW
.label = Consum medi te selezion atuâl
+# This is used in the tooltip when the power value uses the microwatt unit.
+# Variables:
+# $value (String) - the power value at this location
+TrackPower--tooltip-average-power-microwatt = { $value } μW
+ .label = Consum medi te selezion corinte
# This is used in the tooltip when the energy used in the current range uses the
# kilowatt-hour unit.
# Variables:
diff --git a/locales/fy-NL/app.ftl b/locales/fy-NL/app.ftl
index addadbbaa5..2fbae9169a 100644
--- a/locales/fy-NL/app.ftl
+++ b/locales/fy-NL/app.ftl
@@ -49,6 +49,11 @@ AppViewRouter--error-from-localhost-url-safari =
profilen fan de lokale kompjûter yn dizze browser ymportearje. Iepenje yn stee dêrfan
dizze side yn { -firefox-brand-name } of Chrome.
.title = Safari kan geen lokale profielen importeren
+# This error message is displayed when the profile is in a newer format version
+# than this build of the Profiler is able to read.
+AppViewRouter--error-profile-version =
+ Dit profyl brûkt in yndieling dy’t net troch dizze ferzje fan { -profiler-brand-name } stipe wurdt.
+ Probearje de side te ferfarskjen om te kontrolearjen oft der in fernijing beskikber is foar { -profiler-brand-name }.
AppViewRouter--route-not-found--home =
.specialMessage = De URL dy’t jo probearre te berikken, waard net werkend.
diff --git a/locales/ia/app.ftl b/locales/ia/app.ftl
index 119fd5d9b8..fefc4679d5 100644
--- a/locales/ia/app.ftl
+++ b/locales/ia/app.ftl
@@ -49,6 +49,11 @@ AppViewRouter--error-from-localhost-url-safari =
importar profilos ab le local apparato in iste browser. Per favor aperi
in vice iste pagina in { -firefox-brand-name } o Chrome.
.title = Safari non pote importar profilos local
+# This error message is displayed when the profile is in a newer format version
+# than this build of the Profiler is able to read.
+AppViewRouter--error-profile-version =
+ Iste profilo usa un formato que non es supportate per iste version de { -profiler-brand-name }.
+ Essaya refrescar le pagina pro verificar si il ha un actualisation disponibile pro { -profiler-brand-name }.
AppViewRouter--route-not-found--home =
.specialMessage = Le URL que tu tentava attinger non ha essite recognoscite.
diff --git a/locales/it/app.ftl b/locales/it/app.ftl
index 09ba75b1cd..29b5ef0bc8 100644
--- a/locales/it/app.ftl
+++ b/locales/it/app.ftl
@@ -46,6 +46,11 @@ AppViewRouter--error-compare = Impossibile recuperare i profili.
# https://profiler.firefox.com/from-url/http%3A%2F%2F127.0.0.1%3A3000%2Fprofile.json/
AppViewRouter--error-from-localhost-url-safari = A causa di una limitazione specifica di Safari, { -profiler-brand-name } non può importare profili dal dispositivo locale in questo browser. Aprire questa pagina in { -firefox-brand-name } o Chrome.
.title = Impossibile importare profili locali in Safari
+# This error message is displayed when the profile is in a newer format version
+# than this build of the Profiler is able to read.
+AppViewRouter--error-profile-version =
+ Questo profilo utilizza un formato non compatibile con questa versione di { -profiler-brand-name }.
+ Ricarica la pagina per verificare se è disponibile un aggiornamento per { -profiler-brand-name }.
AppViewRouter--route-not-found--home =
.specialMessage = L’URL che hai cercato di raggiungere non è stato riconosciuto.
diff --git a/locales/nl/app.ftl b/locales/nl/app.ftl
index ca9147c060..57a9fd737e 100644
--- a/locales/nl/app.ftl
+++ b/locales/nl/app.ftl
@@ -49,6 +49,11 @@ AppViewRouter--error-from-localhost-url-safari =
profielen van de lokale computer in deze browser importeren. Open in plaats daarvan
deze pagina in { -firefox-brand-name } of Chrome.
.title = Safari kan geen lokale profielen importeren
+# This error message is displayed when the profile is in a newer format version
+# than this build of the Profiler is able to read.
+AppViewRouter--error-profile-version =
+ Dit profiel gebruikt een indeling die niet door deze versie van { -profiler-brand-name } wordt ondersteund.
+ Probeer de pagina te vernieuwen om te controleren of er een update beschikbaar is voor { -profiler-brand-name }.
AppViewRouter--route-not-found--home =
.specialMessage = De URL die u probeerde te bereiken, werd niet herkend.
diff --git a/locales/ru/app.ftl b/locales/ru/app.ftl
index 70f9a7633c..c30a2ab90b 100644
--- a/locales/ru/app.ftl
+++ b/locales/ru/app.ftl
@@ -49,6 +49,11 @@ AppViewRouter--error-from-localhost-url-safari =
импортировать профили с локальной машины в этот браузер. Пожалуйста, откройте
эту страницу в { -firefox-brand-name } или Chrome.
.title = Safari не может импортировать локальные профили
+# This error message is displayed when the profile is in a newer format version
+# than this build of the Profiler is able to read.
+AppViewRouter--error-profile-version =
+ Этот профиль использует формат, который не поддерживается этой версией { -profiler-brand-name }.
+ Попробуйте обновить страницу, чтобы проверить, доступно ли обновление для { -profiler-brand-name }.
AppViewRouter--route-not-found--home =
.specialMessage = URL-адрес, который вы пытались открыть, не был распознан.
diff --git a/locales/sv-SE/app.ftl b/locales/sv-SE/app.ftl
index 79e9a8fd44..1799a97736 100644
--- a/locales/sv-SE/app.ftl
+++ b/locales/sv-SE/app.ftl
@@ -49,6 +49,11 @@ AppViewRouter--error-from-localhost-url-safari =
{ -profiler-brand-name } importera profiler från den lokala datorn i den här webbläsaren. Öppna
den här sidan i { -firefox-brand-name } eller Chrome istället.
.title = Safari kan inte importera lokala profiler
+# This error message is displayed when the profile is in a newer format version
+# than this build of the Profiler is able to read.
+AppViewRouter--error-profile-version =
+ Den här profilen använder ett format som inte stöds av den här versionen av { -profiler-brand-name }.
+ Testa att uppdatera sidan för att kontrollera om det finns en tillgänglig uppdatering för { -profiler-brand-name }.
AppViewRouter--route-not-found--home =
.specialMessage = Webbadressen du försökte nå kändes inte igen.
diff --git a/locales/zh-TW/app.ftl b/locales/zh-TW/app.ftl
index bc16bcecaf..6f3fb97161 100644
--- a/locales/zh-TW/app.ftl
+++ b/locales/zh-TW/app.ftl
@@ -46,6 +46,11 @@ AppViewRouter--error-compare = 無法取得效能檢測檔。
# https://profiler.firefox.com/from-url/http%3A%2F%2F127.0.0.1%3A3000%2Fprofile.json/
AppViewRouter--error-from-localhost-url-safari = 由於 Safari 的特殊限制,{ -profiler-brand-name } 無法從這套瀏覽器自本機匯入效能檢測檔。請改用 { -firefox-brand-name } 或 Chrome 開啟此頁面。
.title = 無法使用 Safari 匯入本機效能檢測檔
+# This error message is displayed when the profile is in a newer format version
+# than this build of the Profiler is able to read.
+AppViewRouter--error-profile-version =
+ 此版本的 { -profiler-brand-name } 不支援此效能檢測檔使用的格式。
+ 請嘗試重新整理頁面,看看是否有 { -profiler-brand-name } 的更新可以使用。
AppViewRouter--route-not-found--home =
.specialMessage = 無法處理您嘗試開啟的網址。
diff --git a/package.json b/package.json
index b0f2b85b7d..96e20725f8 100644
--- a/package.json
+++ b/package.json
@@ -70,7 +70,7 @@
"@codemirror/lang-rust": "^6.0.2",
"@codemirror/language": "^6.12.3",
"@codemirror/state": "^6.6.0",
- "@codemirror/view": "^6.43.0",
+ "@codemirror/view": "^6.43.1",
"@firefox-devtools/react-contextmenu": "^5.2.4",
"@fluent/bundle": "^0.19.1",
"@fluent/langneg": "^0.7.0",
@@ -100,7 +100,7 @@
"mixedtuplemap": "^1.0.0",
"namedtuplemap": "^1.0.0",
"photon-colors": "^3.3.2",
- "protobufjs": "^8.6.3",
+ "protobufjs": "^8.6.5",
"query-string": "^9.4.0",
"react": "~19.1.8",
"react-dom": "~19.1.8",
@@ -132,7 +132,7 @@
"@types/jest": "^30.0.0",
"@types/node": "^24.13.2",
"@types/query-string": "^6.3.0",
- "@types/react": "^19.2.15",
+ "@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@types/redux-logger": "^3.0.6",
"@types/tgwf__co2": "^0.14.2",
@@ -143,7 +143,7 @@
"babel-plugin-module-resolver": "^5.0.3",
"browserslist": "^4.28.2",
"browserslist-to-esbuild": "^2.1.1",
- "caniuse-lite": "^1.0.30001793",
+ "caniuse-lite": "^1.0.30001799",
"cross-env": "^10.1.0",
"cross-spawn": "^7.0.6",
"devtools-license-check": "^0.9.0",
@@ -169,12 +169,12 @@
"lockfile-lint": "^5.0.0",
"npm-run-all2": "^8.0.4",
"open": "^11.0.0",
- "oxfmt": "^0.51.0",
+ "oxfmt": "^0.56.0",
"patch-package": "^8.0.1",
"postcss": "^8.5.15",
"postinstall-postinstall": "^2.1.0",
"rimraf": "^6.1.3",
- "stylelint": "^17.12.0",
+ "stylelint": "^17.13.0",
"stylelint-config-idiomatic-order": "^10.0.0",
"stylelint-config-standard": "^40.0.0",
"typescript": "^6.0.3",
diff --git a/profiler-cli/guide.txt b/profiler-cli/guide.txt
index bc1cca6bfb..65cef7e2eb 100644
--- a/profiler-cli/guide.txt
+++ b/profiler-cli/guide.txt
@@ -117,6 +117,7 @@ HANDLE SYSTEM
t-0, t-1 Thread handles (from "profile info")
m-1234 Marker handles (from "thread markers")
f-12 Function handles (from "thread samples", "thread functions")
+ c-0, c-1 Counter handles (from "counter list" or "profile info")
ts-6 Timestamp handles (named points in time, usable with "zoom push")
Handle lifetime and stability:
@@ -127,6 +128,8 @@ HANDLE SYSTEM
m-N thread markers No -- rebuilt each time the daemon starts
f-N thread samples, Yes -- direct index into the profile's function
thread functions table; same profile always yields the same f-N
+ c-N counter list Yes -- direct index into the profile's counter
+ array; same profile always yields the same c-N
ts-N thread markers No -- position-based, session-scoped
──────────────────────────────────────────────────────────────────────────
@@ -215,6 +218,31 @@ FILTERS
profiler-cli filter push --during-marker --search Paint
+COUNTERS
+
+ Counters are time series the profiler records alongside samples: memory usage,
+ network bandwidth, process CPU, power, and similar. Each counter has a handle
+ (c-0, c-1, ...) and carries its own display metadata (label, unit, graph type).
+
+ profiler-cli counter list List all counters with one-line summaries
+ profiler-cli counter info c-0 Detailed info and stats for one counter
+
+ Counters also appear in "profile info", listed under their owning process
+ next to that process's threads (much like the timeline track list).
+
+ The stats shown come from the counter's own tooltip schema, so they match the
+ timeline tooltips. Each counter reports its whole-range aggregates, e.g. the
+ memory range for Memory, data transferred for Bandwidth, or energy used (with a
+ CO2e estimate) for Power.
+
+ All counter stats respect the current zoom: with no zoom they cover the whole
+ profile; after "zoom push" they cover the committed range. Combine with zoom to
+ see, for example, how much memory a specific time window allocated:
+
+ profiler-cli zoom push 2.7,3.1
+ profiler-cli counter info c-0
+
+
JSON OUTPUT
Add --json to any command to get structured JSON output, suitable for piping to jq
diff --git a/profiler-cli/package.json b/profiler-cli/package.json
index 5d528a63d7..e5dd4fbae9 100644
--- a/profiler-cli/package.json
+++ b/profiler-cli/package.json
@@ -1,6 +1,6 @@
{
"name": "@firefox-devtools/profiler-cli",
- "version": "0.2.1",
+ "version": "0.3.0",
"description": "Command-line interface for querying Firefox Profiler profiles with persistent daemon sessions",
"scripts": {
"prepublishOnly": "node ../scripts/verify-profiler-cli-build.mjs"
diff --git a/profiler-cli/schemas.txt b/profiler-cli/schemas.txt
index 2a13b1389f..602560a352 100644
--- a/profiler-cli/schemas.txt
+++ b/profiler-cli/schemas.txt
@@ -19,12 +19,39 @@ profiler-cli profile info --json
processes: [{
pid, name, cpuMs,
threads: [{ threadHandle, threadIndex, name, tid, cpuMs }],
- remainingThreads?: { count, combinedCpuMs, maxCpuMs }
+ remainingThreads?: { count, combinedCpuMs, maxCpuMs },
+ counters?: [CounterSummary]
}],
remainingProcesses?: { count, combinedCpuMs, maxCpuMs },
context: SessionContext
}
+CounterSummary:
+ {
+ counterHandle, counterIndex, name, label, category,
+ unit, graphType,
+ color, pid, mainThreadIndex, mainThreadHandle, mainThreadName,
+ rangeSampleCount,
+ stats: [{ source, label, value, formattedValue, carbon? }]
+ }
+
+profiler-cli counter list --json
+ {
+ type: "counter-list",
+ counters: [CounterSummary],
+ context: SessionContext
+ }
+
+profiler-cli counter info --json
+ {
+ type: "counter-info",
+ ...CounterSummary,
+ description,
+ sampleCount,
+ rangeStart, rangeEnd,
+ context: SessionContext
+ }
+
profiler-cli thread samples --json
{
type: "thread-samples",
diff --git a/profiler-cli/src/commands/counter.ts b/profiler-cli/src/commands/counter.ts
new file mode 100644
index 0000000000..15a8e5d9a5
--- /dev/null
+++ b/profiler-cli/src/commands/counter.ts
@@ -0,0 +1,49 @@
+/* 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/. */
+
+/**
+ * `profiler-cli counter` command.
+ */
+
+import type { Command } from 'commander';
+import { sendCommand } from '../client';
+import { formatOutput } from '../output';
+import { addGlobalOptions } from './shared';
+
+export function registerCounterCommand(
+ program: Command,
+ sessionDir: string
+): void {
+ const counter = program
+ .command('counter')
+ .description('Counter-level commands');
+
+ addGlobalOptions(
+ counter
+ .command('list')
+ .description('List all counters with one-line summaries')
+ ).action(async (opts) => {
+ const result = await sendCommand(
+ sessionDir,
+ { command: 'counter', subcommand: 'list' },
+ opts.session
+ );
+ console.log(formatOutput(result, opts.json ?? false));
+ });
+
+ addGlobalOptions(
+ counter
+ .command('info [handle]')
+ .description('Show detailed information about a counter (e.g. c-0)')
+ .option('--counter ', 'Counter handle')
+ ).action(async (handleArg: string | undefined, opts) => {
+ const counterHandle = handleArg ?? opts.counter;
+ const result = await sendCommand(
+ sessionDir,
+ { command: 'counter', subcommand: 'info', counter: counterHandle },
+ opts.session
+ );
+ console.log(formatOutput(result, opts.json ?? false));
+ });
+}
diff --git a/profiler-cli/src/constants.ts b/profiler-cli/src/constants.ts
index 3050cbaaac..d8be3144d0 100644
--- a/profiler-cli/src/constants.ts
+++ b/profiler-cli/src/constants.ts
@@ -8,6 +8,7 @@
// These globals are defined via esbuild's define option.
declare const __BUILD_HASH__: string;
+declare const __PACKAGE_NAME__: string;
declare const __VERSION__: string;
/**
@@ -16,6 +17,11 @@ declare const __VERSION__: string;
*/
export const BUILD_HASH = __BUILD_HASH__;
+/**
+ * Package name from profiler-cli/package.json, injected at build time.
+ */
+export const PACKAGE_NAME = __PACKAGE_NAME__;
+
/**
* Package version from profiler-cli/package.json, injected at build time.
*/
diff --git a/profiler-cli/src/daemon.ts b/profiler-cli/src/daemon.ts
index 992b827f62..bc2375de9b 100644
--- a/profiler-cli/src/daemon.ts
+++ b/profiler-cli/src/daemon.ts
@@ -11,6 +11,7 @@ import * as net from 'net';
import * as fs from 'fs';
import { ProfileQuerier } from '../../src/profile-query';
import type { LoadPhase } from '../../src/profile-query/loader';
+import { ProfileVersionError } from 'firefox-profiler/profile-logic/errors';
import type {
ClientCommand,
ClientMessage,
@@ -28,7 +29,27 @@ import {
ensureSessionDir,
} from './session';
import { assertExhaustiveCheck } from 'firefox-profiler/utils/types';
-import { BUILD_HASH } from './constants';
+import { BUILD_HASH, PACKAGE_NAME } from './constants';
+
+/**
+ * Build a user-facing message for a profile load failure. When the profile is
+ * too new for this build, append instructions on how to update the CLI.
+ */
+function formatProfileLoadError(error: unknown): string {
+ if (
+ error instanceof ProfileVersionError ||
+ (error instanceof Error && error.name === 'ProfileVersionError')
+ ) {
+ const versionError = error as ProfileVersionError;
+ return (
+ `This profile is version ${versionError.profileVersion}, but this profiler-cli only ` +
+ `supports up to version ${versionError.supportedVersion} of the ${versionError.formatName} profile format.\n` +
+ `Update to the latest version with:\n` +
+ ` npm install -g ${PACKAGE_NAME}@latest`
+ );
+ }
+ return error instanceof Error ? error.message : String(error);
+}
export class Daemon {
private querier: ProfileQuerier | null = null;
@@ -41,7 +62,7 @@ export class Daemon {
private profilePath: string;
private symbolServerUrl?: string;
private loadPhase: LoadPhase = 'fetching';
- private profileLoadError: Error | null = null;
+ private profileLoadError: string | null = null;
constructor(
sessionDir: string,
@@ -149,8 +170,7 @@ export class Daemon {
console.log('Profile loaded successfully');
} catch (error) {
console.error(`Failed to load profile: ${error}`);
- this.profileLoadError =
- error instanceof Error ? error : new Error(String(error));
+ this.profileLoadError = formatProfileLoadError(error);
}
}
@@ -210,7 +230,7 @@ export class Daemon {
if (this.profileLoadError) {
return {
type: 'error',
- error: `Profile load failed: ${this.profileLoadError.message}`,
+ error: `Profile load failed: ${this.profileLoadError}`,
};
}
switch (this.loadPhase) {
@@ -245,7 +265,7 @@ export class Daemon {
if (this.profileLoadError) {
return {
type: 'error',
- error: `Profile load failed: ${this.profileLoadError.message}`,
+ error: `Profile load failed: ${this.profileLoadError}`,
};
}
if (this.loadPhase !== 'ready' || !this.querier) {
@@ -360,6 +380,18 @@ export class Daemon {
default:
throw assertExhaustiveCheck(command);
}
+ case 'counter':
+ switch (command.subcommand) {
+ case 'list':
+ return this.querier!.counterList();
+ case 'info':
+ if (!command.counter) {
+ throw new Error('counter handle required for counter info');
+ }
+ return this.querier!.counterInfo(command.counter);
+ default:
+ throw assertExhaustiveCheck(command);
+ }
case 'sample':
switch (command.subcommand) {
case 'info':
diff --git a/profiler-cli/src/formatters.ts b/profiler-cli/src/formatters.ts
index f62da72cd5..6dee2303bc 100644
--- a/profiler-cli/src/formatters.ts
+++ b/profiler-cli/src/formatters.ts
@@ -35,6 +35,9 @@ import type {
SampleFilterSpec,
ProfileLogsResult,
ThreadSelectResult,
+ CounterSummary,
+ CounterListResult,
+ CounterInfoResult,
} from './protocol';
import { truncateFunctionName } from '../../src/profile-query/function-list';
import { describeSpec } from '../../src/profile-query/filter-stack';
@@ -428,6 +431,10 @@ Name: ${result.name}\n`;
if (process.remainingThreads) {
output += ` + ${process.remainingThreads.count} more threads with combined CPU time ${process.remainingThreads.combinedCpuMs.toFixed(3)}ms and max CPU time ${process.remainingThreads.maxCpuMs.toFixed(3)}ms (use --all to see all)\n`;
}
+
+ for (const counter of process.counters ?? []) {
+ output += ` ${counter.counterHandle}: ${counter.label}${formatCounterStats(counter)}\n`;
+ }
}
if (result.remainingProcesses) {
@@ -451,6 +458,85 @@ Name: ${result.name}\n`;
return output;
}
+function formatCounterStatInline(
+ stat: CounterSummary['stats'][number]
+): string {
+ const value = stat.carbon
+ ? `${stat.formattedValue} (${stat.carbon})`
+ : stat.formattedValue;
+ return `${stat.label}: ${value}`;
+}
+
+/** The ` - stat; stat [N samples]` trailer shared by counter list and profile info. */
+function formatCounterStats(counter: CounterSummary): string {
+ const stats =
+ counter.stats.length > 0
+ ? ` - ${counter.stats.map(formatCounterStatInline).join('; ')}`
+ : '';
+ return `${stats} [${counter.rangeSampleCount} samples]`;
+}
+
+function formatCounterSummaryLine(counter: CounterSummary): string {
+ return ` ${counter.counterHandle}: ${counter.label} (${counter.category})${formatCounterStats(counter)}`;
+}
+
+/**
+ * Format a CounterListResult as plain text.
+ */
+export function formatCounterListResult(
+ result: WithContext
+): string {
+ const contextHeader = formatContextHeader(result.context);
+ if (result.counters.length === 0) {
+ return `${contextHeader}\n\nNo counters in this profile.`;
+ }
+ const lines = result.counters.map(formatCounterSummaryLine);
+ return `${contextHeader}\n\nCounters (${result.counters.length}):\n${lines.join('\n')}`;
+}
+
+/**
+ * Format a CounterInfoResult as plain text.
+ */
+export function formatCounterInfoResult(
+ result: WithContext
+): string {
+ const contextHeader = formatContextHeader(result.context);
+ const lines = [
+ contextHeader,
+ '',
+ `Counter ${result.counterHandle}: ${result.label}`,
+ ` Name: ${result.name}`,
+ ` Category: ${result.category}`,
+ ];
+ if (result.description) {
+ lines.push(` Description: ${result.description}`);
+ }
+ lines.push(` Unit: ${result.unit || '(none)'}`);
+ lines.push(` Graph type: ${result.graphType}`);
+ lines.push(
+ ` Main thread: ${result.mainThreadHandle} (${result.mainThreadName})`
+ );
+ lines.push(
+ ` Samples: ${result.sampleCount} total, ${result.rangeSampleCount} in current range`
+ );
+ if (result.rangeStart !== null && result.rangeEnd !== null) {
+ const zeroAt = result.context.rootRange.start;
+ lines.push(
+ ` Time span: ${formatDuration(result.rangeStart - zeroAt)} → ${formatDuration(result.rangeEnd - zeroAt)}`
+ );
+ }
+ if (result.stats.length > 0) {
+ lines.push(' Stats (current range):');
+ for (const stat of result.stats) {
+ const value = stat.carbon
+ ? `${stat.formattedValue} (${stat.carbon})`
+ : stat.formattedValue;
+ lines.push(` ${stat.label}: ${value}`);
+ }
+ }
+ return lines.join('\n');
+}
+
/**
* Helper function to format a call tree node recursively.
*
diff --git a/profiler-cli/src/index.ts b/profiler-cli/src/index.ts
index 05b7a640a7..c637516b9b 100644
--- a/profiler-cli/src/index.ts
+++ b/profiler-cli/src/index.ts
@@ -37,6 +37,7 @@ import { registerProfileCommand } from './commands/profile';
import { registerThreadCommand } from './commands/thread';
import { registerMarkerCommand } from './commands/marker';
import { registerFunctionCommand } from './commands/function';
+import { registerCounterCommand } from './commands/counter';
import { registerZoomCommand } from './commands/zoom';
import { registerFilterCommand } from './commands/filter';
import { registerSessionCommand } from './commands/session';
@@ -85,6 +86,8 @@ Examples:
profiler-cli thread samples
profiler-cli thread functions --search GC --min-self 1
profiler-cli thread markers --search DOMEvent --category Graphics
+ profiler-cli counter list
+ profiler-cli counter info c-0
profiler-cli zoom push 2.7,3.1
profiler-cli filter push --excludes-function f-184
profiler-cli status
@@ -180,6 +183,7 @@ Examples:
registerThreadCommand(program, SESSION_DIR);
registerMarkerCommand(program, SESSION_DIR);
registerFunctionCommand(program, SESSION_DIR);
+ registerCounterCommand(program, SESSION_DIR);
registerZoomCommand(program, SESSION_DIR);
registerFilterCommand(program, SESSION_DIR);
registerSessionCommand(program, SESSION_DIR);
diff --git a/profiler-cli/src/output.ts b/profiler-cli/src/output.ts
index 36d8fc6345..a983f136cb 100644
--- a/profiler-cli/src/output.ts
+++ b/profiler-cli/src/output.ts
@@ -28,6 +28,8 @@ import {
formatProfileLogsResult,
formatThreadPageLoadResult,
formatThreadSelectResult,
+ formatCounterListResult,
+ formatCounterInfoResult,
} from './formatters';
/**
@@ -88,6 +90,10 @@ export function formatOutput(
return formatThreadPageLoadResult(result);
case 'thread-select':
return formatThreadSelectResult(result);
+ case 'counter-list':
+ return formatCounterListResult(result);
+ case 'counter-info':
+ return formatCounterInfoResult(result);
default:
throw assertExhaustiveCheck(result);
}
diff --git a/profiler-cli/src/protocol.ts b/profiler-cli/src/protocol.ts
index 330dfe479f..3e817f9b43 100644
--- a/profiler-cli/src/protocol.ts
+++ b/profiler-cli/src/protocol.ts
@@ -50,6 +50,9 @@ export type {
ProfileInfoResult,
ProfileLogsResult,
ThreadSelectResult,
+ CounterSummary,
+ CounterListResult,
+ CounterInfoResult,
} from '../../src/profile-query/types';
export type { CallTreeCollectionOptions } from '../../src/profile-query/formatters/call-tree';
@@ -79,6 +82,8 @@ import type {
FilterStackResult,
ProfileLogsResult,
ThreadSelectResult,
+ CounterListResult,
+ CounterInfoResult,
} from '../../src/profile-query/types';
import type { CallTreeCollectionOptions } from '../../src/profile-query/formatters/call-tree';
@@ -141,6 +146,11 @@ export type ClientCommand =
subcommand: 'info' | 'select' | 'stack';
marker?: string;
}
+ | {
+ command: 'counter';
+ subcommand: 'list' | 'info';
+ counter?: string;
+ }
| { command: 'sample'; subcommand: 'info' | 'select'; sample?: string }
| {
command: 'function';
@@ -195,7 +205,9 @@ export type CommandResult =
| WithContext
| WithContext
| WithContext
- | WithContext;
+ | WithContext
+ | WithContext
+ | WithContext;
export interface SessionMetadata {
id: string;
diff --git a/profiler-cli/src/test/unit/counter-formatting.test.ts b/profiler-cli/src/test/unit/counter-formatting.test.ts
new file mode 100644
index 0000000000..a9b7ca0958
--- /dev/null
+++ b/profiler-cli/src/test/unit/counter-formatting.test.ts
@@ -0,0 +1,150 @@
+/* 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/. */
+
+import {
+ formatCounterListResult,
+ formatCounterInfoResult,
+} from '../../formatters';
+import type {
+ CounterSummary,
+ CounterListResult,
+ CounterInfoResult,
+ SessionContext,
+ WithContext,
+} from 'firefox-profiler/profile-query/types';
+
+function createContext(): SessionContext {
+ return {
+ selectedThreadHandle: 't-0',
+ selectedThreads: [{ threadIndex: 0, name: 'GeckoMain' }],
+ currentViewRange: null,
+ rootRange: { start: 0, end: 3000 },
+ };
+}
+
+function makeCounter(overrides: Partial = {}): CounterSummary {
+ return {
+ counterHandle: 'c-0',
+ counterIndex: 0,
+ name: 'malloc',
+ label: 'Memory',
+ category: 'Memory',
+ unit: 'bytes',
+ graphType: 'line-accumulated',
+ color: 'orange',
+ pid: '123',
+ mainThreadIndex: 0,
+ mainThreadHandle: 't-0',
+ mainThreadName: 'GeckoMain',
+ rangeSampleCount: 7,
+ stats: [
+ {
+ source: 'count-range',
+ label: 'memory range in graph',
+ value: 27,
+ formattedValue: '27B',
+ },
+ ],
+ ...overrides,
+ };
+}
+
+describe('formatCounterListResult', function () {
+ it('renders one line per counter with its stats', function () {
+ const result: WithContext = {
+ context: createContext(),
+ type: 'counter-list',
+ counters: [
+ makeCounter(),
+ makeCounter({
+ counterHandle: 'c-1',
+ name: 'eth0',
+ label: 'Bandwidth',
+ category: 'Bandwidth',
+ graphType: 'line-rate',
+ stats: [
+ {
+ source: 'count-range',
+ label: 'Data transferred in the visible range',
+ value: 2048,
+ formattedValue: '2KB',
+ },
+ ],
+ }),
+ ],
+ };
+
+ const output = formatCounterListResult(result);
+ expect(output).toContain('Counters (2):');
+ expect(output).toContain('c-0: Memory (Memory)');
+ expect(output).toContain('memory range in graph: 27B');
+ expect(output).toContain('[7 samples]');
+ expect(output).toContain('c-1: Bandwidth (Bandwidth)');
+ expect(output).toContain('Data transferred in the visible range: 2KB');
+ });
+
+ it('reports when there are no counters', function () {
+ const result: WithContext = {
+ context: createContext(),
+ type: 'counter-list',
+ counters: [],
+ };
+
+ expect(formatCounterListResult(result)).toContain(
+ 'No counters in this profile.'
+ );
+ });
+});
+
+describe('formatCounterInfoResult', function () {
+ function makeInfo(
+ overrides: Partial = {}
+ ): WithContext {
+ return {
+ context: createContext(),
+ ...makeCounter(),
+ type: 'counter-info',
+ description: 'Amount of allocated memory',
+ sampleCount: 7,
+ rangeStart: 0,
+ rangeEnd: 10,
+ ...overrides,
+ };
+ }
+
+ it('renders the counter detail block', function () {
+ const output = formatCounterInfoResult(makeInfo());
+ expect(output).toContain('Counter c-0: Memory');
+ expect(output).toContain('Name: malloc');
+ expect(output).toContain('Category: Memory');
+ expect(output).toContain('Unit: bytes');
+ expect(output).toContain('Graph type: line-accumulated');
+ expect(output).toContain('Main thread: t-0 (GeckoMain)');
+ expect(output).toContain('Description: Amount of allocated memory');
+ expect(output).toContain('memory range in graph: 27B');
+ });
+
+ it('shows a CO2e estimate next to a stat when present', function () {
+ const output = formatCounterInfoResult(
+ makeInfo({
+ label: 'Power',
+ category: 'power',
+ unit: 'pWh',
+ graphType: 'line-rate',
+ stats: [
+ {
+ source: 'committed-range-total',
+ label: 'Energy used in the visible range',
+ value: 5,
+ formattedValue: '5 Wh',
+ carbon: '2 g CO₂e',
+ },
+ ],
+ })
+ );
+ expect(output).toContain(
+ 'Energy used in the visible range: 5 Wh (2 g CO₂e)'
+ );
+ });
+});
diff --git a/scripts/build-profiler-cli.mjs b/scripts/build-profiler-cli.mjs
index 9e90b69c28..a8f8775c44 100644
--- a/scripts/build-profiler-cli.mjs
+++ b/scripts/build-profiler-cli.mjs
@@ -5,7 +5,7 @@ import esbuild from 'esbuild';
import { chmodSync, readFileSync } from 'fs';
import { nodeBaseConfig } from './lib/esbuild-configs.mjs';
-const { version } = JSON.parse(
+const { name, version } = JSON.parse(
readFileSync(new URL('../profiler-cli/package.json', import.meta.url), 'utf8')
);
@@ -22,6 +22,7 @@ const profilerCliConfig = {
},
define: {
__BUILD_HASH__: JSON.stringify(BUILD_HASH),
+ __PACKAGE_NAME__: JSON.stringify(name),
__VERSION__: JSON.stringify(version),
// SOURCE_MAP_WORKER_PATH is injected by the browser build. The CLI doesn't
// use source map workers but the shared code references this constant.
diff --git a/src/actions/profile-view.ts b/src/actions/profile-view.ts
index 51ff3dcdb4..0c51a7a172 100644
--- a/src/actions/profile-view.ts
+++ b/src/actions/profile-view.ts
@@ -2015,6 +2015,7 @@ export function toggleBottomBoxFullscreen(): ThunkAction {
export function handleCallNodeTransformShortcut(
event: React.KeyboardEvent,
threadsKey: ThreadsKey,
+ callNodeInfo: CallNodeInfo,
callNodeIndex: IndexIntoCallNodeTable
): ThunkAction {
return (dispatch, getState) => {
@@ -2023,7 +2024,6 @@ export function handleCallNodeTransformShortcut(
}
const threadSelectors = getThreadSelectorsFromThreadsKey(threadsKey);
const unfilteredThread = threadSelectors.getThread(getState());
- const callNodeInfo = threadSelectors.getCallNodeInfo(getState());
const implementation = getImplementationFilter(getState());
const inverted = getInvertCallstack(getState());
const callNodePath = callNodeInfo.getCallNodePathFromIndex(callNodeIndex);
@@ -2051,6 +2051,15 @@ export function handleCallNodeTransformShortcut(
})
);
break;
+ case 'S':
+ dispatch(
+ addTransformToStack(threadsKey, {
+ type: 'focus-self',
+ funcIndex,
+ implementation,
+ })
+ );
+ break;
case 'M':
dispatch(
addTransformToStack(threadsKey, {
diff --git a/src/app-logic/constants.ts b/src/app-logic/constants.ts
index 6cba739b59..8d39768163 100644
--- a/src/app-logic/constants.ts
+++ b/src/app-logic/constants.ts
@@ -12,7 +12,7 @@ export const GECKO_PROFILE_VERSION = 34;
// 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 = 64;
+export const PROCESSED_PROFILE_VERSION = 65;
// 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/AppViewRouter.tsx b/src/components/app/AppViewRouter.tsx
index fc04a205ff..8c025eab8b 100644
--- a/src/components/app/AppViewRouter.tsx
+++ b/src/components/app/AppViewRouter.tsx
@@ -87,6 +87,9 @@ class AppViewRouterImpl extends PureComponent {
if (view.error) {
if (view.error.name === 'SafariLocalhostHTTPLoadError') {
message = 'AppViewRouter--error-from-localhost-url-safari';
+ } else if (view.error.name === 'ProfileVersionError') {
+ message = 'AppViewRouter--error-profile-version';
+ additionalMessage = {view.error.toString()}
;
} else {
console.error(view.error);
additionalMessage = (
diff --git a/src/components/app/DetailsContainer.css b/src/components/app/DetailsContainer.css
index 5c24522df0..083505c802 100644
--- a/src/components/app/DetailsContainer.css
+++ b/src/components/app/DetailsContainer.css
@@ -6,22 +6,23 @@
contain: size;
}
-.DetailsContainer .resizableWithSplitterInner > * {
- min-width: 0;
- flex: 1;
-}
-
.DetailsContainerResizableSidebarWrapper {
max-width: 600px;
}
/* overriding defaults from ResizableWithSplitter.css */
-.DetailsContainer .resizableWithSplitterSplitter {
+.DetailsContainerResizableSidebarWrapper > .resizableWithSplitterSplitter {
border-top: 1px solid var(--panel-border-color);
border-left: 1px solid var(--panel-border-color);
background: var(--panel-background-color); /* Same background as sidebars */
}
-.DetailsContainer .resizableWithSplitterSplitter:hover {
+.DetailsContainerResizableSidebarWrapper
+ > .resizableWithSplitterSplitter:hover {
background: var(--panel-background-color); /* same as the border above */
}
+
+.DetailsContainerResizableSidebarWrapper > .resizableWithSplitterInner > * {
+ min-width: 0;
+ flex: 1;
+}
diff --git a/src/components/app/ProfileViewer.css b/src/components/app/ProfileViewer.css
index 447647b6a1..36d0c4d8e8 100644
--- a/src/components/app/ProfileViewer.css
+++ b/src/components/app/ProfileViewer.css
@@ -47,10 +47,7 @@
}
.profileViewer {
- /* Create a stacking context, so that the KeyboardShortcut can overlay the profile
- viewer. */
position: relative;
- z-index: 0;
display: flex;
min-width: 0; /* This allows Flexible Layout to shrink this further than its min-content */
flex: 1;
diff --git a/src/components/calltree/CallTree.tsx b/src/components/calltree/CallTree.tsx
index 401247eee5..a86053f130 100644
--- a/src/components/calltree/CallTree.tsx
+++ b/src/components/calltree/CallTree.tsx
@@ -2,11 +2,9 @@
* 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/. */
import { PureComponent } from 'react';
-import memoize from 'memoize-immutable';
import explicitConnect from 'firefox-profiler/utils/connect';
import { TreeView } from 'firefox-profiler/components/shared/TreeView';
import { CallTreeEmptyReasons } from './CallTreeEmptyReasons';
-import { Icon } from 'firefox-profiler/components/shared/Icon';
import {
getInvertCallstack,
getImplementationFilter,
@@ -30,7 +28,6 @@ import {
changeTableViewOptions,
updateBottomBoxContentsAndMaybeOpen,
} from 'firefox-profiler/actions/profile-view';
-import { assertExhaustiveCheck } from 'firefox-profiler/utils/types';
import type {
State,
@@ -46,13 +43,10 @@ import type {
import type { CallTree as CallTreeType } from 'firefox-profiler/profile-logic/call-tree';
import type { CallNodeInfo } from 'firefox-profiler/profile-logic/call-node-info';
-import type {
- Column,
- MaybeResizableColumn,
-} from 'firefox-profiler/components/shared/TreeView';
import type { ConnectedProps } from 'firefox-profiler/utils/connect';
import './CallTree.css';
+import { nameColumn, libColumn, treeColumnsForWeightType } from './columns';
type StateProps = {
readonly threadsKey: ThreadsKey;
@@ -86,124 +80,11 @@ type DispatchProps = {
type Props = ConnectedProps<{}, StateProps, DispatchProps>;
class CallTreeImpl extends PureComponent {
- _mainColumn: Column = {
- propName: 'name',
- titleL10nId: '',
- };
- _appendageColumn: Column = {
- propName: 'lib',
- titleL10nId: '',
- };
_treeView: TreeView | null = null;
_takeTreeViewRef = (treeView: TreeView | null) => {
this._treeView = treeView;
};
- /**
- * Call Trees can have different types of "weights" for the data. Choose the
- * appropriate labels for the call tree based on this weight.
- */
- _weightTypeToColumns = memoize(
- (weightType: WeightType): MaybeResizableColumn[] => {
- switch (weightType) {
- case 'tracing-ms':
- return [
- {
- propName: 'totalPercent',
- titleL10nId: '',
- initialWidth: 55,
- hideDividerAfter: true,
- },
- {
- propName: 'total',
- titleL10nId: 'CallTree--tracing-ms-total',
- minWidth: 30,
- initialWidth: 70,
- resizable: true,
- headerWidthAdjustment: 55 /* totalPercent initialWidth */,
- },
- {
- propName: 'self',
- titleL10nId: 'CallTree--tracing-ms-self',
- minWidth: 40,
- initialWidth: 80,
- resizable: true,
- },
- {
- propName: 'icon',
- titleL10nId: '',
- component: Icon as any,
- initialWidth: 20,
- },
- ];
- case 'samples':
- return [
- {
- propName: 'totalPercent',
- titleL10nId: '',
- initialWidth: 55,
- hideDividerAfter: true,
- },
- {
- propName: 'total',
- titleL10nId: 'CallTree--samples-total',
- minWidth: 30,
- initialWidth: 70,
- resizable: true,
- headerWidthAdjustment: 55 /* totalPercent initialWidth */,
- },
- {
- propName: 'self',
- titleL10nId: 'CallTree--samples-self',
- minWidth: 40,
- initialWidth: 80,
- resizable: true,
- },
- {
- propName: 'icon',
- titleL10nId: '',
- component: Icon as any,
- initialWidth: 20,
- },
- ];
- case 'bytes':
- return [
- {
- propName: 'totalPercent',
- titleL10nId: '',
- initialWidth: 55,
- hideDividerAfter: true,
- },
- {
- propName: 'total',
- titleL10nId: 'CallTree--bytes-total',
- minWidth: 30,
- initialWidth: 140,
- resizable: true,
- headerWidthAdjustment: 55 /* totalPercent initialWidth */,
- },
- {
- propName: 'self',
- titleL10nId: 'CallTree--bytes-self',
- minWidth: 40,
- initialWidth: 100,
- resizable: true,
- },
- {
- propName: 'icon',
- titleL10nId: '',
- component: Icon as any,
- initialWidth: 20,
- },
- ];
- default:
- throw assertExhaustiveCheck(weightType, 'Unhandled WeightType.');
- }
- },
- // Use a Map cache, as the function only takes one argument, which is a simple string.
- { cache: new Map() }
- );
-
override componentDidMount() {
this.focus();
this.maybeProcureInterestingInitialSelection();
@@ -276,6 +157,7 @@ class CallTreeImpl extends PureComponent {
rightClickedCallNodeIndex,
handleCallNodeTransformShortcut,
threadsKey,
+ callNodeInfo,
} = this.props;
const nodeIndex =
rightClickedCallNodeIndex !== null
@@ -284,7 +166,7 @@ class CallTreeImpl extends PureComponent {
if (nodeIndex === null) {
return;
}
- handleCallNodeTransformShortcut(event, threadsKey, nodeIndex);
+ handleCallNodeTransformShortcut(event, threadsKey, callNodeInfo, nodeIndex);
};
_onEnterOrDoubleClick = (nodeId: IndexIntoCallNodeTable) => {
@@ -366,9 +248,9 @@ class CallTreeImpl extends PureComponent {
return (
= {
+ propName: 'name',
+ titleL10nId: '',
+};
+
+export const libColumn: Column = {
+ propName: 'lib',
+ titleL10nId: '',
+};
+
+export const treeColumnsForTracingMs: MaybeResizableColumn[] =
+ [
+ {
+ propName: 'totalPercent',
+ titleL10nId: '',
+ initialWidth: 55,
+ hideDividerAfter: true,
+ },
+ {
+ propName: 'total',
+ titleL10nId: 'CallTree--tracing-ms-total',
+ minWidth: 30,
+ initialWidth: 70,
+ resizable: true,
+ headerWidthAdjustment: 55 /* totalPercent initialWidth */,
+ },
+ {
+ propName: 'self',
+ titleL10nId: 'CallTree--tracing-ms-self',
+ minWidth: 40,
+ initialWidth: 80,
+ resizable: true,
+ },
+ {
+ propName: 'icon',
+ titleL10nId: '',
+ component: Icon as any,
+ initialWidth: 20,
+ },
+ ];
+
+export const treeColumnsForSamples: MaybeResizableColumn[] =
+ [
+ {
+ propName: 'totalPercent',
+ titleL10nId: '',
+ initialWidth: 55,
+ hideDividerAfter: true,
+ },
+ {
+ propName: 'total',
+ titleL10nId: 'CallTree--samples-total',
+ minWidth: 30,
+ initialWidth: 70,
+ resizable: true,
+ headerWidthAdjustment: 55 /* totalPercent initialWidth */,
+ },
+ {
+ propName: 'self',
+ titleL10nId: 'CallTree--samples-self',
+ minWidth: 40,
+ initialWidth: 80,
+ resizable: true,
+ },
+ {
+ propName: 'icon',
+ titleL10nId: '',
+ component: Icon as any,
+ initialWidth: 20,
+ },
+ ];
+
+export const treeColumnsForBytes: MaybeResizableColumn[] =
+ [
+ {
+ propName: 'totalPercent',
+ titleL10nId: '',
+ initialWidth: 55,
+ hideDividerAfter: true,
+ },
+ {
+ propName: 'total',
+ titleL10nId: 'CallTree--bytes-total',
+ minWidth: 30,
+ initialWidth: 140,
+ resizable: true,
+ headerWidthAdjustment: 55 /* totalPercent initialWidth */,
+ },
+ {
+ propName: 'self',
+ titleL10nId: 'CallTree--bytes-self',
+ minWidth: 40,
+ initialWidth: 100,
+ resizable: true,
+ },
+ {
+ propName: 'icon',
+ titleL10nId: '',
+ component: Icon as any,
+ initialWidth: 20,
+ },
+ ];
+
+export function treeColumnsForWeightType(
+ weightType: WeightType
+): MaybeResizableColumn[] {
+ switch (weightType) {
+ case 'tracing-ms':
+ return treeColumnsForTracingMs;
+ case 'samples':
+ return treeColumnsForSamples;
+ case 'bytes':
+ return treeColumnsForBytes;
+ default:
+ throw assertExhaustiveCheck(weightType, 'Unhandled WeightType.');
+ }
+}
diff --git a/src/components/flame-graph/Canvas.tsx b/src/components/flame-graph/Canvas.tsx
index 1c163512b2..73dd9394da 100644
--- a/src/components/flame-graph/Canvas.tsx
+++ b/src/components/flame-graph/Canvas.tsx
@@ -70,7 +70,6 @@ export type OwnProps = {
readonly scrollToSelectionGeneration: number;
readonly categories: CategoryList;
readonly interval: Milliseconds;
- readonly isInverted: boolean;
readonly callTreeSummaryStrategy: CallTreeSummaryStrategy;
readonly ctssSamples: SamplesLikeTable;
readonly ctssSampleCategoriesAndSubcategories: SampleCategoriesAndSubcategories;
@@ -244,13 +243,13 @@ class FlameGraphCanvasImpl extends React.PureComponent {
// Only draw the stack frames that are vertically within view.
// The graph is drawn from bottom to top, in order of increasing depth.
for (let depth = startDepth; depth < endDepth; depth++) {
- // Get the timing information for a row of stack frames.
- const stackTiming = flameGraphTiming[depth];
-
- if (!stackTiming) {
+ if (depth < 0 || depth >= flameGraphTiming.rowCount) {
continue;
}
+ // Get the timing information for a row of stack frames.
+ const stackTiming = flameGraphTiming.getRow(depth);
+
const cssRowTop: CssPixels =
(maxStackDepthPlusOne - depth - 1) * ROW_HEIGHT - viewportTop;
const cssRowBottom: CssPixels =
@@ -373,10 +372,11 @@ class FlameGraphCanvasImpl extends React.PureComponent {
return null;
}
- const stackTiming = flameGraphTiming[depth];
- if (!stackTiming) {
+ if (depth < 0 || depth >= flameGraphTiming.rowCount) {
return null;
}
+
+ const stackTiming = flameGraphTiming.getRow(depth);
const callNodeIndex = stackTiming.callNode[flameGraphTimingIndex];
if (callNodeIndex === undefined) {
return null;
@@ -443,7 +443,7 @@ class FlameGraphCanvasImpl extends React.PureComponent {
const { depth, flameGraphTimingIndex } = hoveredItem;
const { flameGraphTiming } = this.props;
- const stackTiming = flameGraphTiming[depth];
+ const stackTiming = flameGraphTiming.getRow(depth);
const callNodeIndex = stackTiming.callNode[flameGraphTimingIndex];
return callNodeIndex;
}
@@ -477,12 +477,10 @@ class FlameGraphCanvasImpl extends React.PureComponent {
const depth = Math.floor(
maxStackDepthPlusOne - (y + viewportTop) / ROW_HEIGHT
);
- const stackTiming = flameGraphTiming[depth];
-
- if (!stackTiming) {
+ if (depth < 0 || depth >= flameGraphTiming.rowCount) {
return null;
}
-
+ const stackTiming = flameGraphTiming.getRow(depth);
for (let i = 0; i < stackTiming.length; i++) {
const start = stackTiming.start[i];
const end = stackTiming.end[i];
diff --git a/src/components/flame-graph/ConnectedFlameGraph.tsx b/src/components/flame-graph/ConnectedFlameGraph.tsx
new file mode 100644
index 0000000000..8895b8812b
--- /dev/null
+++ b/src/components/flame-graph/ConnectedFlameGraph.tsx
@@ -0,0 +1,241 @@
+/* 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/. */
+import * as React from 'react';
+
+import { explicitConnectWithForwardRef } from '../../utils/connect';
+import { FlameGraph, type FlameGraphHandle } from './FlameGraph';
+
+import {
+ getCategories,
+ getCommittedRange,
+ getPreviewSelection,
+ getScrollToSelectionGeneration,
+ getProfileInterval,
+ getInnerWindowIDToPageMap,
+ getProfileUsesMultipleStackTypes,
+} from 'firefox-profiler/selectors/profile';
+import { selectedThreadSelectors } from 'firefox-profiler/selectors/per-thread';
+import { getSelectedThreadsKey } from '../../selectors/url-state';
+import {
+ changeSelectedCallNode,
+ changeRightClickedCallNode,
+ handleCallNodeTransformShortcut,
+ updateBottomBoxContentsAndMaybeOpen,
+} from 'firefox-profiler/actions/profile-view';
+
+import type {
+ Thread,
+ CategoryList,
+ Milliseconds,
+ StartEndRange,
+ WeightType,
+ SamplesLikeTable,
+ PreviewSelection,
+ CallTreeSummaryStrategy,
+ IndexIntoCallNodeTable,
+ ThreadsKey,
+ InnerWindowID,
+ Page,
+ SampleCategoriesAndSubcategories,
+} from 'firefox-profiler/types';
+
+import type { FlameGraphTiming } from 'firefox-profiler/profile-logic/flame-graph';
+import type { CallNodeInfo } from 'firefox-profiler/profile-logic/call-node-info';
+
+import type {
+ CallTree,
+ CallTreeTimings,
+} from 'firefox-profiler/profile-logic/call-tree';
+
+import type { ConnectedProps } from 'firefox-profiler/utils/connect';
+
+export type ConnectedFlameGraphHandle = FlameGraphHandle;
+
+type StateProps = {
+ readonly thread: Thread;
+ readonly weightType: WeightType;
+ readonly innerWindowIDToPageMap: Map | null;
+ readonly maxStackDepthPlusOne: number;
+ readonly timeRange: StartEndRange;
+ readonly previewSelection: PreviewSelection | null;
+ readonly flameGraphTiming: FlameGraphTiming;
+ readonly callTree: CallTree;
+ readonly callNodeInfo: CallNodeInfo;
+ readonly threadsKey: ThreadsKey;
+ readonly selectedCallNodeIndex: IndexIntoCallNodeTable | null;
+ readonly rightClickedCallNodeIndex: IndexIntoCallNodeTable | null;
+ readonly scrollToSelectionGeneration: number;
+ readonly categories: CategoryList;
+ readonly interval: Milliseconds;
+ readonly callTreeSummaryStrategy: CallTreeSummaryStrategy;
+ readonly ctssSamples: SamplesLikeTable;
+ readonly ctssSampleCategoriesAndSubcategories: SampleCategoriesAndSubcategories;
+ readonly tracedTiming: CallTreeTimings | null;
+ readonly displayStackType: boolean;
+};
+
+type DispatchProps = {
+ readonly changeSelectedCallNode: typeof changeSelectedCallNode;
+ readonly changeRightClickedCallNode: typeof changeRightClickedCallNode;
+ readonly handleCallNodeTransformShortcut: typeof handleCallNodeTransformShortcut;
+ readonly updateBottomBoxContentsAndMaybeOpen: typeof updateBottomBoxContentsAndMaybeOpen;
+};
+
+type Props = ConnectedProps<{}, StateProps, DispatchProps>;
+
+class ConnectedFlameGraphImpl
+ extends React.PureComponent
+ implements ConnectedFlameGraphHandle
+{
+ _flameGraph: React.RefObject = React.createRef();
+
+ // eslint-disable-next-line react/no-unused-class-component-methods -- called via ConnectedFlameGraphHandle ref from FlameGraphViewImpl
+ focus() {
+ this._flameGraph.current?.focus();
+ }
+
+ _onSelectedCallNodeChange = (
+ callNodeIndex: IndexIntoCallNodeTable | null
+ ) => {
+ const { callNodeInfo, threadsKey, changeSelectedCallNode } = this.props;
+ changeSelectedCallNode(
+ threadsKey,
+ callNodeInfo.getCallNodePathFromIndex(callNodeIndex)
+ );
+ };
+
+ _onRightClickedCallNodeChange = (
+ callNodeIndex: IndexIntoCallNodeTable | null
+ ) => {
+ const { callNodeInfo, threadsKey, changeRightClickedCallNode } = this.props;
+ changeRightClickedCallNode(
+ threadsKey,
+ callNodeInfo.getCallNodePathFromIndex(callNodeIndex)
+ );
+ };
+
+ _onCallNodeEnterOrDoubleClick = (
+ callNodeIndex: IndexIntoCallNodeTable | null
+ ) => {
+ if (callNodeIndex === null) {
+ return;
+ }
+ const { callTree, updateBottomBoxContentsAndMaybeOpen } = this.props;
+ const bottomBoxInfo = callTree.getBottomBoxInfoForCallNode(callNodeIndex);
+ updateBottomBoxContentsAndMaybeOpen('flame-graph', bottomBoxInfo);
+ };
+
+ _onKeyboardTransformShortcut = (
+ event: React.KeyboardEvent,
+ nodeIndex: IndexIntoCallNodeTable
+ ) => {
+ const { threadsKey, callNodeInfo, handleCallNodeTransformShortcut } =
+ this.props;
+ handleCallNodeTransformShortcut(event, threadsKey, callNodeInfo, nodeIndex);
+ };
+
+ override render() {
+ const {
+ thread,
+ threadsKey,
+ maxStackDepthPlusOne,
+ flameGraphTiming,
+ callTree,
+ callNodeInfo,
+ timeRange,
+ previewSelection,
+ rightClickedCallNodeIndex,
+ selectedCallNodeIndex,
+ scrollToSelectionGeneration,
+ callTreeSummaryStrategy,
+ categories,
+ interval,
+ innerWindowIDToPageMap,
+ weightType,
+ ctssSamples,
+ ctssSampleCategoriesAndSubcategories,
+ tracedTiming,
+ displayStackType,
+ } = this.props;
+
+ return (
+
+ );
+ }
+}
+
+export const ConnectedFlameGraph = explicitConnectWithForwardRef<
+ {},
+ StateProps,
+ DispatchProps,
+ ConnectedFlameGraphHandle
+>({
+ mapStateToProps: (state) => ({
+ thread: selectedThreadSelectors.getFilteredThread(state),
+ weightType: selectedThreadSelectors.getWeightTypeForCallTree(state),
+ // Use the filtered call node max depth, rather than the preview filtered one, so
+ // that the viewport height is stable across preview selections.
+ maxStackDepthPlusOne:
+ selectedThreadSelectors.getFilteredCallNodeMaxDepthPlusOne(state),
+ flameGraphTiming: selectedThreadSelectors.getFlameGraphTiming(state),
+ callTree: selectedThreadSelectors.getCallTree(state),
+ timeRange: getCommittedRange(state),
+ previewSelection: getPreviewSelection(state),
+ callNodeInfo: selectedThreadSelectors.getCallNodeInfo(state),
+ categories: getCategories(state),
+ threadsKey: getSelectedThreadsKey(state),
+ selectedCallNodeIndex:
+ selectedThreadSelectors.getSelectedCallNodeIndex(state),
+ rightClickedCallNodeIndex:
+ selectedThreadSelectors.getRightClickedCallNodeIndex(state),
+ scrollToSelectionGeneration: getScrollToSelectionGeneration(state),
+ interval: getProfileInterval(state),
+ callTreeSummaryStrategy:
+ selectedThreadSelectors.getCallTreeSummaryStrategy(state),
+ innerWindowIDToPageMap: getInnerWindowIDToPageMap(state),
+ ctssSamples: selectedThreadSelectors.getPreviewFilteredCtssSamples(state),
+ ctssSampleCategoriesAndSubcategories:
+ selectedThreadSelectors.getPreviewFilteredCtssSampleCategoriesAndSubcategories(
+ state
+ ),
+ tracedTiming: selectedThreadSelectors.getTracedTiming(state),
+ displayStackType: getProfileUsesMultipleStackTypes(state),
+ }),
+ mapDispatchToProps: {
+ changeSelectedCallNode,
+ changeRightClickedCallNode,
+ handleCallNodeTransformShortcut,
+ updateBottomBoxContentsAndMaybeOpen,
+ },
+ options: { forwardRef: true },
+ component: ConnectedFlameGraphImpl,
+});
diff --git a/src/components/flame-graph/FlameGraph.tsx b/src/components/flame-graph/FlameGraph.tsx
index 376d0f3025..159aaa0ea5 100644
--- a/src/components/flame-graph/FlameGraph.tsx
+++ b/src/components/flame-graph/FlameGraph.tsx
@@ -3,30 +3,8 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import * as React from 'react';
-import { explicitConnectWithForwardRef } from '../../utils/connect';
import { FlameGraphCanvas } from './Canvas';
-
-import {
- getCategories,
- getCommittedRange,
- getPreviewSelection,
- getScrollToSelectionGeneration,
- getProfileInterval,
- getInnerWindowIDToPageMap,
- getProfileUsesMultipleStackTypes,
-} from 'firefox-profiler/selectors/profile';
-import { selectedThreadSelectors } from 'firefox-profiler/selectors/per-thread';
-import {
- getSelectedThreadsKey,
- getInvertCallstack,
-} from '../../selectors/url-state';
import { ContextMenuTrigger } from 'firefox-profiler/components/shared/ContextMenuTrigger';
-import {
- changeSelectedCallNode,
- changeRightClickedCallNode,
- handleCallNodeTransformShortcut,
- updateBottomBoxContentsAndMaybeOpen,
-} from 'firefox-profiler/actions/profile-view';
import { extractNonInvertedCallTreeTimings } from 'firefox-profiler/profile-logic/call-tree';
import { ensureExists } from 'firefox-profiler/utils/types';
@@ -54,8 +32,6 @@ import type {
CallTreeTimings,
} from 'firefox-profiler/profile-logic/call-tree';
-import type { ConnectedProps } from 'firefox-profiler/utils/connect';
-
import './FlameGraph.css';
const STACK_FRAME_HEIGHT = 16;
@@ -67,7 +43,7 @@ const STACK_FRAME_HEIGHT = 16;
*/
const SELECTABLE_THRESHOLD = 0.001;
-type StateProps = {
+export type Props = {
readonly thread: Thread;
readonly weightType: WeightType;
readonly innerWindowIDToPageMap: Map | null;
@@ -83,26 +59,32 @@ type StateProps = {
readonly scrollToSelectionGeneration: number;
readonly categories: CategoryList;
readonly interval: Milliseconds;
- readonly isInverted: boolean;
readonly callTreeSummaryStrategy: CallTreeSummaryStrategy;
readonly ctssSamples: SamplesLikeTable;
readonly ctssSampleCategoriesAndSubcategories: SampleCategoriesAndSubcategories;
readonly tracedTiming: CallTreeTimings | null;
readonly displayStackType: boolean;
+ readonly contextMenuId?: string;
+ readonly onSelectedCallNodeChange: (
+ callNodeIndex: IndexIntoCallNodeTable | null
+ ) => void;
+ readonly onRightClickedCallNodeChange: (
+ callNodeIndex: IndexIntoCallNodeTable | null
+ ) => void;
+ readonly onCallNodeEnterOrDoubleClick: (
+ callNodeIndex: IndexIntoCallNodeTable | null
+ ) => void;
+ readonly onKeyboardTransformShortcut: (
+ event: React.KeyboardEvent,
+ nodeIndex: IndexIntoCallNodeTable
+ ) => void;
};
-type DispatchProps = {
- readonly changeSelectedCallNode: typeof changeSelectedCallNode;
- readonly changeRightClickedCallNode: typeof changeRightClickedCallNode;
- readonly handleCallNodeTransformShortcut: typeof handleCallNodeTransformShortcut;
- readonly updateBottomBoxContentsAndMaybeOpen: typeof updateBottomBoxContentsAndMaybeOpen;
-};
-type Props = ConnectedProps<{}, StateProps, DispatchProps>;
export interface FlameGraphHandle {
focus(): void;
}
-class FlameGraphImpl
+export class FlameGraph
extends React.PureComponent
implements FlameGraphHandle
{
@@ -116,44 +98,13 @@ class FlameGraphImpl
document.removeEventListener('copy', this._onCopy, false);
}
- _onSelectedCallNodeChange = (
- callNodeIndex: IndexIntoCallNodeTable | null
- ) => {
- const { callNodeInfo, threadsKey, changeSelectedCallNode } = this.props;
- changeSelectedCallNode(
- threadsKey,
- callNodeInfo.getCallNodePathFromIndex(callNodeIndex)
- );
- };
-
- _onRightClickedCallNodeChange = (
- callNodeIndex: IndexIntoCallNodeTable | null
- ) => {
- const { callNodeInfo, threadsKey, changeRightClickedCallNode } = this.props;
- changeRightClickedCallNode(
- threadsKey,
- callNodeInfo.getCallNodePathFromIndex(callNodeIndex)
- );
- };
-
- _onCallNodeEnterOrDoubleClick = (
- callNodeIndex: IndexIntoCallNodeTable | null
- ) => {
- if (callNodeIndex === null) {
- return;
- }
- const { callTree, updateBottomBoxContentsAndMaybeOpen } = this.props;
- const bottomBoxInfo = callTree.getBottomBoxInfoForCallNode(callNodeIndex);
- updateBottomBoxContentsAndMaybeOpen('flame-graph', bottomBoxInfo);
- };
-
_shouldDisplayTooltips = () => this.props.rightClickedCallNodeIndex === null;
_takeViewportRef = (viewport: HTMLDivElement | null) => {
this._viewport = viewport;
};
- /* This method is called from MaybeFlameGraph. */
+ /* This method is called from ConnectedFlameGraph. */
/* eslint-disable-next-line react/no-unused-class-component-methods */
focus = () => {
if (this._viewport) {
@@ -169,7 +120,7 @@ class FlameGraphImpl
const callNodeTable = callNodeInfo.getCallNodeTable();
const depth = callNodeTable.depth[callNodeIndex];
- const row = flameGraphTiming[depth];
+ const row = flameGraphTiming.getRow(depth);
const columnIndex = row.callNode.indexOf(callNodeIndex);
return row.end[columnIndex] - row.start[columnIndex] > SELECTABLE_THRESHOLD;
};
@@ -194,7 +145,7 @@ class FlameGraphImpl
const callNodeTable = callNodeInfo.getCallNodeTable();
const depth = callNodeTable.depth[callNodeIndex];
- const row = flameGraphTiming[depth];
+ const row = flameGraphTiming.getRow(depth);
let columnIndex = row.callNode.indexOf(callNodeIndex);
do {
@@ -215,13 +166,13 @@ class FlameGraphImpl
_handleKeyDown = (event: React.KeyboardEvent) => {
const {
- threadsKey,
callTree,
callNodeInfo,
selectedCallNodeIndex,
rightClickedCallNodeIndex,
- changeSelectedCallNode,
- handleCallNodeTransformShortcut,
+ onSelectedCallNodeChange,
+ onCallNodeEnterOrDoubleClick,
+ onKeyboardTransformShortcut,
} = this.props;
const callNodeTable = callNodeInfo.getCallNodeTable();
@@ -231,10 +182,7 @@ class FlameGraphImpl
) {
if (selectedCallNodeIndex === null) {
// Just select the "root" node if we've got no prior selection.
- changeSelectedCallNode(
- threadsKey,
- callNodeInfo.getCallNodePathFromIndex(0)
- );
+ onSelectedCallNodeChange(0);
return;
}
@@ -242,10 +190,7 @@ class FlameGraphImpl
case 'ArrowDown': {
const prefix = callNodeTable.prefix[selectedCallNodeIndex];
if (prefix !== -1) {
- changeSelectedCallNode(
- threadsKey,
- callNodeInfo.getCallNodePathFromIndex(prefix)
- );
+ onSelectedCallNodeChange(prefix);
}
break;
}
@@ -257,10 +202,7 @@ class FlameGraphImpl
// thus the widest box.
if (callNodeIndex !== undefined && this._wideEnough(callNodeIndex)) {
- changeSelectedCallNode(
- threadsKey,
- callNodeInfo.getCallNodePathFromIndex(callNodeIndex)
- );
+ onSelectedCallNodeChange(callNodeIndex);
}
break;
}
@@ -272,10 +214,7 @@ class FlameGraphImpl
);
if (callNodeIndex !== undefined) {
- changeSelectedCallNode(
- threadsKey,
- callNodeInfo.getCallNodePathFromIndex(callNodeIndex)
- );
+ onSelectedCallNodeChange(callNodeIndex);
}
break;
}
@@ -298,11 +237,11 @@ class FlameGraphImpl
}
if (event.key === 'Enter') {
- this._onCallNodeEnterOrDoubleClick(nodeIndex);
+ onCallNodeEnterOrDoubleClick(nodeIndex);
return;
}
- handleCallNodeTransformShortcut(event, threadsKey, nodeIndex);
+ onKeyboardTransformShortcut(event, nodeIndex);
};
_onCopy = (event: ClipboardEvent) => {
@@ -336,13 +275,16 @@ class FlameGraphImpl
callTreeSummaryStrategy,
categories,
interval,
- isInverted,
innerWindowIDToPageMap,
weightType,
ctssSamples,
ctssSampleCategoriesAndSubcategories,
tracedTiming,
displayStackType,
+ contextMenuId = 'CallNodeContextMenu',
+ onSelectedCallNodeChange,
+ onRightClickedCallNodeChange,
+ onCallNodeEnterOrDoubleClick,
} = this.props;
// Get the CallTreeTimingsNonInverted out of tracedTiming. We pass this
@@ -363,7 +305,7 @@ class FlameGraphImpl
return (