diff --git a/.gitignore b/.gitignore index 14f36789cc..7f04f29a05 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ coverage webpack.local-config.js *.orig *.rej +.idea/ diff --git a/docs-user/guide-filtering-call-trees.md b/docs-user/guide-filtering-call-trees.md index c0b3133970..10b3b6da04 100644 --- a/docs-user/guide-filtering-call-trees.md +++ b/docs-user/guide-filtering-call-trees.md @@ -74,6 +74,18 @@ Merging takes a call node and removes it from the call tree. Any self time for t Focusing on a function or call node removes all of the ancestor call nodes—the children call nodes remain. If a stack does not contain that function or node, then it is removed. This effectively focuses on a subtree or a set of subtrees on the call tree. +### Focus on Function Self + +Focus on function self is similar to focus on function, but more restrictive: it only keeps samples where the focused function is the innermost implementation-filtered frame. This helps you analyze where within a function the self time is being spent, by removing samples where the function is calling other code. + +For example, if you focus-self on a JavaScript function with the JS implementation filter, you'll only see samples where that JS function has self time, and any native (C++) calls below it will be shown as descendants. This is particularly useful for: + +- Finding which parts of a function are slow (by looking at the assembly or source lines) +- Understanding what engine internals are being called by a JS function (by switching implementation filter after focusing) +- Eliminating noise from code your function calls, to concentrate on the function's own execution + +If the same function appears multiple times in a stack (recursion), only the innermost instance is kept as the root. + ### Focus on Category Focusing on the nodes that belong to the same category as the selected node, thereby merging all nodes that belong to another category. diff --git a/eslint.config.mjs b/eslint.config.mjs index 11ca6d0516..6706b48b92 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,6 +1,7 @@ +import { defineConfig } from 'eslint/config'; import js from '@eslint/js'; +import tseslint from 'typescript-eslint'; import tsParser from '@typescript-eslint/parser'; -import tsPlugin from '@typescript-eslint/eslint-plugin'; import babelPlugin from '@babel/eslint-plugin'; import reactPlugin from 'eslint-plugin-react'; import importPlugin from 'eslint-plugin-import'; @@ -11,7 +12,7 @@ import jestDomPlugin from 'eslint-plugin-jest-dom'; import prettierConfig from 'eslint-config-prettier'; import globals from 'globals'; -export default [ +export default defineConfig( // Global ignores { ignores: [ @@ -27,8 +28,8 @@ export default [ // Base JavaScript config js.configs.recommended, - // TypeScript config - ...tsPlugin.configs['flat/recommended'], + // TypeScript config with type checking + ...tseslint.configs.recommendedTypeChecked, // React config reactPlugin.configs.flat.recommended, @@ -50,6 +51,7 @@ export default [ ecmaFeatures: { jsx: true, }, + // Note: projectService is enabled for TypeScript files only, below }, }, plugins: { @@ -178,6 +180,32 @@ export default [ }, ], '@typescript-eslint/no-require-imports': 'off', + + // Disable "no-unsafe" checks which complain about using "any" freely. + '@typescript-eslint/no-unsafe-member-access': 'off', + '@typescript-eslint/no-unsafe-assignment': 'off', + '@typescript-eslint/no-unsafe-call': 'off', + '@typescript-eslint/no-unsafe-argument': 'off', + '@typescript-eslint/no-unsafe-return': 'off', + + // Disable this one due to false positives when narrowing return types, + // see https://github.com/typescript-eslint/typescript-eslint/issues/6951 + // (it can make `yarn ts` fail after `yarn lint-fix`) + '@typescript-eslint/no-unnecessary-type-assertion': 'off', + + // Consider enabling these in the future + '@typescript-eslint/unbound-method': 'off', + '@typescript-eslint/only-throw-error': 'off', + '@typescript-eslint/no-floating-promises': 'off', + '@typescript-eslint/require-await': 'off', + '@typescript-eslint/no-redundant-type-constituents': 'off', + '@typescript-eslint/no-misused-promises': 'off', + '@typescript-eslint/await-thenable': 'off', + '@typescript-eslint/restrict-plus-operands': 'off', + '@typescript-eslint/no-base-to-string': 'off', + '@typescript-eslint/restrict-template-expressions': 'off', + '@typescript-eslint/prefer-promise-reject-errors': 'off', + '@typescript-eslint/no-array-delete': 'off', }, linterOptions: { // This property is specified both here in addition to the command line in @@ -189,7 +217,27 @@ export default [ }, }, - // Source files - enable stricter TypeScript rules + // Enable type-aware lints for TypeScript files. + { + files: ['**/*.ts', '**/*.tsx'], + languageOptions: { + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + }, + }, + + // Explicitly disable type-aware rules for JS files, to avoid the following error: + // > You have used a rule which requires type information, but don't have + // > parserOptions set to generate type information for this file. + { + files: ['**/*.js', '**/*.mjs', '**/*.cjs'], + ...tseslint.configs.disableTypeChecked, + }, + + // For non-test files, only allow import, no require(). (For test files we + // allow require() because we use it for JSON fixtures.) { files: ['src/**/*.ts', 'src/**/*.tsx'], rules: { @@ -266,5 +314,5 @@ export default [ }, // Prettier config (must be last to override other formatting rules) - prettierConfig, -]; + prettierConfig +); diff --git a/jest-resolver.js b/jest-resolver.js new file mode 100644 index 0000000000..beb8262852 --- /dev/null +++ b/jest-resolver.js @@ -0,0 +1,57 @@ +/* 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/. */ + +// Custom Jest resolver that respects the "browser" field in package.json +// This allows tests to use browser implementations instead of Node.js implementations +// +// Set JEST_ENVIRONMENT=node to use Node.js implementations (default: browser) + +const fs = require('fs'); +const path = require('path'); + +// Determine environment mode: "browser" or "node" +const USE_BROWSER = process.env.JEST_ENVIRONMENT !== 'node'; + +// Read package.json once at module load time +const PROJECT_ROOT = __dirname; +const BROWSER_MAPPINGS = parseBrowserMappingsFromPackageJson(PROJECT_ROOT); + +module.exports = (request, options) => { + const resolved = options.defaultResolver(request, options); + + if (USE_BROWSER) { + return BROWSER_MAPPINGS[resolved] ?? resolved; + } + + return resolved; +}; + +function parseBrowserMappingsFromPackageJson(projectRoot) { + const browserMappings = {}; + const packageJsonPath = path.join(projectRoot, 'package.json'); + try { + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + const browserField = packageJson.browser; + + if (browserField && typeof browserField === 'object') { + // Pre-validate all browser mappings and convert to absolute paths + for (const [source, target] of Object.entries(browserField)) { + const absoluteSource = path.resolve(projectRoot, source); + const absoluteTarget = path.resolve(projectRoot, target); + + if (!fs.existsSync(absoluteTarget)) { + console.warn( + `Warning: Browser mapping target does not exist: ${target}` + ); + continue; + } + + browserMappings[absoluteSource] = absoluteTarget; + } + } + } catch (error) { + console.error(`Error reading package.json for browser field: ${error}`); + } + return browserMappings; +} diff --git a/jest.config.js b/jest.config.js index 54682dfe31..dbb1aa6adc 100644 --- a/jest.config.js +++ b/jest.config.js @@ -6,6 +6,9 @@ module.exports = { testMatch: ['/src/**/*.test.{js,jsx,ts,tsx}'], moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx'], + // Use custom resolver that respects the "browser" field in package.json + resolver: './jest-resolver.js', + testEnvironment: './src/test/custom-environment', setupFilesAfterEnv: ['jest-extended/all', './src/test/setup.ts'], diff --git a/locales/be/app.ftl b/locales/be/app.ftl index d5700daa2c..2d70f8efab 100644 --- a/locales/be/app.ftl +++ b/locales/be/app.ftl @@ -407,6 +407,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. @@ -415,6 +422,14 @@ MarkerSettings--panel-search = .title = Паказваць толькі маркеры, якія адпавядаюць пэўнаму імені 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. @@ -535,6 +550,8 @@ MenuButtons--metaInfo--profiling-started = Запіс пачаўся: MenuButtons--metaInfo--profiling-session = Працягласць запісу: MenuButtons--metaInfo--main-process-started = Асноўны працэс пачаўся: MenuButtons--metaInfo--main-process-ended = Асноўны працэс скончыўся: +MenuButtons--metaInfo--file-name = Назва файла: +MenuButtons--metaInfo--file-size = Памер файла: MenuButtons--metaInfo--interval = Інтэрвал: MenuButtons--metaInfo--buffer-capacity = Ёмістасць буфера: MenuButtons--metaInfo--buffer-duration = Працягласць буфера: diff --git a/locales/de/app.ftl b/locales/de/app.ftl index 6861b370e6..9ebd91f04e 100644 --- a/locales/de/app.ftl +++ b/locales/de/app.ftl @@ -315,6 +315,7 @@ Home--additional-content-title = Bestehende Profile laden Home--additional-content-content = Sie können eine Profildatei per Ziehen und Ablegen hierher bewegen, um sie zu laden, oder: Home--compare-recordings-info = Sie können auch Aufnahmen vergleichen. Öffnen Sie die Vergleichsschnittstelle. Home--your-recent-uploaded-recordings-title = Ihre kürzlich hochgeladenen Aufzeichnungen +Home--dark-mode-title = Dunkler Modus # We replace the elements such as and with links to the # documentation to use these tools. Home--load-files-from-other-tools2 = @@ -405,6 +406,13 @@ MarkerContextMenu--select-the-sender-thread = Absender-Thread „{ $thre # $filter (String) - Search string that will be used to filter the markers. MarkerFiltersContextMenu--drop-samples-outside-of-markers-matching = Stichproben außerhalb der Markierungen verwerfen, die mit „{ $filter }“ übereinstimmen +## MarkerCopyTableContextMenu +## This is the menu when the copy icon is clicked in Marker Chart and Marker +## Table panels. + +MarkerCopyTableContextMenu--copy-table-as-plain = Markierungstabelle als Reintext kopieren +MarkerCopyTableContextMenu--copy-table-as-markdown = Markierungstabelle als Markdown kopieren + ## MarkerSettings ## This is used in all panels related to markers. @@ -413,6 +421,14 @@ MarkerSettings--panel-search = .title = Nur Markierungen anzeigen, die zu einem bestimmten Namen passen MarkerSettings--marker-filters = .title = Filter für Markierungen +MarkerSettings--copy-table = + .title = Tabelle als Text kopieren +# 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 = Die Anzahl der Zeilen überschreitet das Limit: { $rows } > { $maxRows }. Nur die ersten { $maxRows } Zeilen werden kopiert. ## MarkerSidebar ## This is the sidebar component that is used in Marker Table panel. @@ -1139,6 +1155,17 @@ AssemblyView--show-button = # Assembly refers to the low-level programming language. AssemblyView--hide-button = .title = Assembly-Ansicht ausblenden +# The "◀" button above the assembly view. +AssemblyView--prev-button = + .title = Zurück +# The "▶" button above the assembly view. +AssemblyView--next-button = + .title = Weiter +# 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 } von { $total } ## UploadedRecordingsHome ## This is the page that displays all the profiles that user has uploaded. diff --git a/locales/el/app.ftl b/locales/el/app.ftl index f493e6c5d0..0b2651b9eb 100644 --- a/locales/el/app.ftl +++ b/locales/el/app.ftl @@ -432,6 +432,8 @@ MarkerSettings--panel-search = .title = Εμφάνιση μόνο των σημαδιών που αντιστοιχούν σε ένα συγκεκριμένο όνομα MarkerSettings--marker-filters = .title = Φίλτρα δείκτη +MarkerSettings--copy-table = + .title = Αντιγραφή πίνακα ως κειμένου ## MarkerSidebar ## This is the sidebar component that is used in Marker Table panel. diff --git a/locales/en-CA/app.ftl b/locales/en-CA/app.ftl index 51933b6ae6..1452fdf931 100644 --- a/locales/en-CA/app.ftl +++ b/locales/en-CA/app.ftl @@ -337,6 +337,7 @@ Home--additional-content-title = Load existing profiles Home--additional-content-content = You can drag and drop a profile file here to load it, or: Home--compare-recordings-info = You can also compare recordings. Open the comparing interface. Home--your-recent-uploaded-recordings-title = Your recent uploaded recordings +Home--dark-mode-title = Dark mode # We replace the elements such as and with links to the # documentation to use these tools. Home--load-files-from-other-tools2 = @@ -429,6 +430,13 @@ MarkerContextMenu--select-the-sender-thread = Select the sender thread “{ $filter }” +## MarkerCopyTableContextMenu +## This is the menu when the copy icon is clicked in Marker Chart and Marker +## Table panels. + +MarkerCopyTableContextMenu--copy-table-as-plain = Copy marker table as plain text +MarkerCopyTableContextMenu--copy-table-as-markdown = Copy marker table as Markdown + ## MarkerSettings ## This is used in all panels related to markers. @@ -437,6 +445,14 @@ MarkerSettings--panel-search = .title = Only display markers that match a certain name MarkerSettings--marker-filters = .title = Marker Filters +MarkerSettings--copy-table = + .title = Copy table as text +# 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 = The number of rows exceeds the limit: { $rows } > { $maxRows }. Only the first { $maxRows } rows will be copied. ## MarkerSidebar ## This is the sidebar component that is used in Marker Table panel. diff --git a/locales/en-GB/app.ftl b/locales/en-GB/app.ftl index 931cae3497..e4c74fd58c 100644 --- a/locales/en-GB/app.ftl +++ b/locales/en-GB/app.ftl @@ -337,6 +337,7 @@ Home--additional-content-title = Load existing profiles Home--additional-content-content = You can drag and drop a profile file here to load it, or: Home--compare-recordings-info = You can also compare recordings. Open the comparing interface. Home--your-recent-uploaded-recordings-title = Your recent uploaded recordings +Home--dark-mode-title = Dark mode # We replace the elements such as and with links to the # documentation to use these tools. Home--load-files-from-other-tools2 = @@ -429,6 +430,13 @@ MarkerContextMenu--select-the-sender-thread = Select the sender thread “{ $filter }” +## MarkerCopyTableContextMenu +## This is the menu when the copy icon is clicked in Marker Chart and Marker +## Table panels. + +MarkerCopyTableContextMenu--copy-table-as-plain = Copy marker table as plain text +MarkerCopyTableContextMenu--copy-table-as-markdown = Copy marker table as Markdown + ## MarkerSettings ## This is used in all panels related to markers. @@ -437,6 +445,14 @@ MarkerSettings--panel-search = .title = Only display markers that match a certain name MarkerSettings--marker-filters = .title = Marker Filters +MarkerSettings--copy-table = + .title = Copy table as text +# 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 = The number of rows exceeds the limit: { $rows } > { $maxRows }. Only the first { $maxRows } rows will be copied. ## MarkerSidebar ## This is the sidebar component that is used in Marker Table panel. @@ -1163,6 +1179,17 @@ AssemblyView--show-button = # Assembly refers to the low-level programming language. AssemblyView--hide-button = .title = Hide the assembly view +# The "◀" button above the assembly view. +AssemblyView--prev-button = + .title = Previous +# The "▶" button above the assembly view. +AssemblyView--next-button = + .title = Next +# 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 } of { $total } ## UploadedRecordingsHome ## This is the page that displays all the profiles that user has uploaded. diff --git a/locales/en-US/app.ftl b/locales/en-US/app.ftl index 9eb89ea769..72757131d3 100644 --- a/locales/en-US/app.ftl +++ b/locales/en-US/app.ftl @@ -86,6 +86,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 @@ -364,6 +379,8 @@ Home--additional-content-content = You can drag and drop a prof Home--compare-recordings-info = You can also compare recordings. Open the comparing interface. Home--your-recent-uploaded-recordings-title = Your recent uploaded recordings +Home--dark-mode-title = Dark mode + # We replace the elements such as and with links to the # documentation to use these tools. Home--load-files-from-other-tools2 = @@ -468,6 +485,16 @@ MarkerContextMenu--select-the-sender-thread = MarkerFiltersContextMenu--drop-samples-outside-of-markers-matching = 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 = + Copy marker table as plain text + +MarkerCopyTableContextMenu--copy-table-as-markdown = + Copy marker table as Markdown + ## MarkerSettings ## This is used in all panels related to markers. @@ -478,6 +505,17 @@ MarkerSettings--panel-search = MarkerSettings--marker-filters = .title = Marker Filters +MarkerSettings--copy-table = + .title = Copy table as text + +# 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 = + The number of rows exceeds the limit: { $rows } > { $maxRows }. Only the first { $maxRows } rows will be copied. + ## MarkerSidebar ## This is the sidebar component that is used in Marker Table panel. @@ -1103,6 +1141,13 @@ TransformNavigator--focus-subtree = Focus Node: { $item } # $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: @@ -1284,6 +1329,20 @@ AssemblyView--show-button = AssemblyView--hide-button = .title = Hide the assembly view +# The "◀" button above the assembly view. +AssemblyView--prev-button = + .title = Previous + +# The "▶" button above the assembly view. +AssemblyView--next-button = + .title = Next + +# 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 } of { $total } + ## UploadedRecordingsHome ## This is the page that displays all the profiles that user has uploaded. ## See: https://profiler.firefox.com/uploaded-recordings/ diff --git a/locales/es-CL/app.ftl b/locales/es-CL/app.ftl index 1e11d63210..1d5cdae746 100644 --- a/locales/es-CL/app.ftl +++ b/locales/es-CL/app.ftl @@ -272,6 +272,7 @@ Home--additional-content-title = Cargar perfiles existentes Home--additional-content-content = Puedes arrastrar y soltar un archivo de perfil aquí para cargarlo, o: Home--compare-recordings-info = También puedes comparar los registros. Abre la interfaz de comparación. Home--your-recent-uploaded-recordings-title = Tus registros subidos recientemente +Home--dark-mode-title = Modo oscuro # We replace the elements such as and with links to the # documentation to use these tools. Home--load-files-from-other-tools2 = @@ -360,6 +361,13 @@ MarkerContextMenu--select-the-sender-thread = Selecciona el hilo remitente "{ $filter }” +## MarkerCopyTableContextMenu +## This is the menu when the copy icon is clicked in Marker Chart and Marker +## Table panels. + +MarkerCopyTableContextMenu--copy-table-as-plain = Copiar la tabla de marcadores como texto sin formato +MarkerCopyTableContextMenu--copy-table-as-markdown = Copiar la tabla de marcadores como Markdown + ## MarkerSettings ## This is used in all panels related to markers. @@ -368,6 +376,14 @@ MarkerSettings--panel-search = .title = Solo mostrar marcadores que coincidan con cierto nombre MarkerSettings--marker-filters = .title = Filtros de marcador +MarkerSettings--copy-table = + .title = Copiar tabla como texto +# 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 = El número de filas supera el límite: { $rows } > { $maxRows }. Solo se copiarán las primeras { $maxRows } filas. ## MarkerSidebar ## This is the sidebar component that is used in Marker Table panel. @@ -1090,6 +1106,17 @@ AssemblyView--show-button = # Assembly refers to the low-level programming language. AssemblyView--hide-button = .title = Ocultar la vista de ensamblaje +# The "◀" button above the assembly view. +AssemblyView--prev-button = + .title = Anterior +# The "▶" button above the assembly view. +AssemblyView--next-button = + .title = Siguiente +# 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 } de { $total } ## UploadedRecordingsHome ## This is the page that displays all the profiles that user has uploaded. diff --git a/locales/fr/app.ftl b/locales/fr/app.ftl index 576c7959c2..636a7e3c17 100644 --- a/locales/fr/app.ftl +++ b/locales/fr/app.ftl @@ -266,6 +266,7 @@ Home--additional-content-title = Charger des profils existants Home--additional-content-content = Vous pouvez glisser-déposer un fichier de profil ici pour le charger, ou : Home--compare-recordings-info = Vous pouvez également comparer des enregistrements. Ouvrir l’interface de comparaison. Home--your-recent-uploaded-recordings-title = Vos enregistrements récemment envoyés +Home--dark-mode-title = Mode sombre # We replace the elements such as and with links to the # documentation to use these tools. Home--load-files-from-other-tools2 = @@ -354,6 +355,13 @@ MarkerContextMenu--select-the-sender-thread = Sélectionner le thread expéditeu # $filter (String) - Search string that will be used to filter the markers. MarkerFiltersContextMenu--drop-samples-outside-of-markers-matching = Abandonner les échantillons en dehors des marqueurs correspondant à « { $filter } » +## MarkerCopyTableContextMenu +## This is the menu when the copy icon is clicked in Marker Chart and Marker +## Table panels. + +MarkerCopyTableContextMenu--copy-table-as-plain = Copier le tableau des marqueurs en texte brut +MarkerCopyTableContextMenu--copy-table-as-markdown = Copier le tableau des marqueurs en Markdown + ## MarkerSettings ## This is used in all panels related to markers. @@ -362,6 +370,14 @@ MarkerSettings--panel-search = .title = Afficher uniquement les marqueurs qui correspondent à un certain nom MarkerSettings--marker-filters = .title = Filtres de marqueurs +MarkerSettings--copy-table = + .title = Copier le tableau sous forme de texte +# 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 = Le nombre de lignes dépasse la limite : { $rows } > { $maxRows }. Seules les { $maxRows } premières lignes seront copiées. ## MarkerSidebar ## This is the sidebar component that is used in Marker Table panel. @@ -1084,6 +1100,17 @@ AssemblyView--show-button = # Assembly refers to the low-level programming language. AssemblyView--hide-button = .title = Masquer la vue assembleur +# The "◀" button above the assembly view. +AssemblyView--prev-button = + .title = Précédent +# The "▶" button above the assembly view. +AssemblyView--next-button = + .title = Suivant +# 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 } sur { $total } ## UploadedRecordingsHome ## This is the page that displays all the profiles that user has uploaded. diff --git a/locales/fur/app.ftl b/locales/fur/app.ftl index 48055ab51b..a20e32ab9e 100644 --- a/locales/fur/app.ftl +++ b/locales/fur/app.ftl @@ -42,6 +42,14 @@ AppViewRouter--error-from-localhost-url-safari = AppViewRouter--route-not-found--home = .specialMessage = L’URL che tu âs cirût di contatâ nol è stât ricognossût. +## Backtrace +## This is used to display a backtrace (call stack) for a marker or sample. + +# Variables: +# $function (String) - Name of the function that was inlined. +Backtrace--inlining-badge = (incorporade) + .title = Il compiladôr al veve incorporade la funzion { $function } in chel che le clamave. + ## CallNodeContextMenu ## This is used as a context menu for the Call Tree, Flame Graph and Stack Chart ## panels. @@ -407,6 +415,16 @@ MarkerTable--duration = Durade MarkerTable--name = Non MarkerTable--details = Detais +## MarkerTooltip +## This is the component for Marker Tooltip panel. + +# This is used as the tooltip for the filter button in marker tooltips. +# Variables: +# $filter (String) - Search string that will be used to filter the markers. +MarkerTooltip--filter-button-tooltip = + .title = Mostre nome i marcadôrs che a corispuindin a: “{ $filter }” + .aria-label = Mostre nome i marcadôrs che a corispuindin a: “{ $filter }” + ## MenuButtons ## These strings are used for the buttons at the top of the profile viewer. diff --git a/locales/fy-NL/app.ftl b/locales/fy-NL/app.ftl index c76008ff97..acab8586bf 100644 --- a/locales/fy-NL/app.ftl +++ b/locales/fy-NL/app.ftl @@ -337,6 +337,7 @@ Home--additional-content-title = Besteande profilen lade Home--additional-content-content = Jo kinne in profylbestân hjirhinne fersleepje om it te laden, of: Home--compare-recordings-info = Jo kinne ek opnamen fergelykje. De fergelikingsinterface iepenje. Home--your-recent-uploaded-recordings-title = Jo resint opladen opnamen +Home--dark-mode-title = Donkere modus # We replace the elements such as and with links to the # documentation to use these tools. Home--load-files-from-other-tools2 = @@ -429,6 +430,13 @@ MarkerContextMenu--select-the-sender-thread = Selektearje de ôfstjoerderthread # $filter (String) - Search string that will be used to filter the markers. MarkerFiltersContextMenu--drop-samples-outside-of-markers-matching = Samples bûten markearringen oerienkommend mei ‘{ $filter }’ bûten beskôging litte +## MarkerCopyTableContextMenu +## This is the menu when the copy icon is clicked in Marker Chart and Marker +## Table panels. + +MarkerCopyTableContextMenu--copy-table-as-plain = Markearringstabel as platte tekst kopiearje +MarkerCopyTableContextMenu--copy-table-as-markdown = Markearringstabel as Markdown kopiearje + ## MarkerSettings ## This is used in all panels related to markers. @@ -437,6 +445,14 @@ MarkerSettings--panel-search = .title = Allinnich markearringen toane dy’t oerienkommen mei in bepaalde namme MarkerSettings--marker-filters = .title = Markearringsfilters +MarkerSettings--copy-table = + .title = Tabel as tekst kopiearje +# 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 = It oantal rigen giet oer de limyt: { $rows } > { $maxRows }. Allinnich de earste { $maxRows } rigen wurde kopiearre. ## MarkerSidebar ## This is the sidebar component that is used in Marker Table panel. @@ -1163,6 +1179,17 @@ AssemblyView--show-button = # Assembly refers to the low-level programming language. AssemblyView--hide-button = .title = De gearstallingswerjefte ferstopje +# The "◀" button above the assembly view. +AssemblyView--prev-button = + .title = Foarige +# The "▶" button above the assembly view. +AssemblyView--next-button = + .title = Folgjende +# 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 } fan { $total } ## UploadedRecordingsHome ## This is the page that displays all the profiles that user has uploaded. diff --git a/locales/ia/app.ftl b/locales/ia/app.ftl index 99a8617407..f07de8bba9 100644 --- a/locales/ia/app.ftl +++ b/locales/ia/app.ftl @@ -328,6 +328,7 @@ Home--additional-content-title = Cargar profilos existente Home--additional-content-content = Tu pote traher e deponer hic un file profilo pro cargar lo, o: Home--compare-recordings-info = Tu pote alsi comparar registrationes. Aperir le interfacie de comparation. Home--your-recent-uploaded-recordings-title = Tu registrationes incargate recentemente +Home--dark-mode-title = Modo obscur # We replace the elements such as and with links to the # documentation to use these tools. Home--load-files-from-other-tools2 = @@ -414,6 +415,13 @@ MarkerContextMenu--select-the-sender-thread = Selige le argumento mittente “{ $filter }” +## MarkerCopyTableContextMenu +## This is the menu when the copy icon is clicked in Marker Chart and Marker +## Table panels. + +MarkerCopyTableContextMenu--copy-table-as-plain = Copiar tabella del marcatores como testo normal +MarkerCopyTableContextMenu--copy-table-as-markdown = Copiar tabella del marcatores como Markdown + ## MarkerSettings ## This is used in all panels related to markers. @@ -422,6 +430,14 @@ MarkerSettings--panel-search = .title = Solo monstra marcatores que concorda con un certe nomine MarkerSettings--marker-filters = .title = Filtros de marcatores +MarkerSettings--copy-table = + .title = Copiar tabella como texto +# 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 = Le numero de rangos supera le limite: { $rows } > { $maxRows }. Sera copiate solo le prime { $maxRows } rangos. ## MarkerSidebar ## This is the sidebar component that is used in Marker Table panel. @@ -1146,6 +1162,17 @@ AssemblyView--show-button = # Assembly refers to the low-level programming language. AssemblyView--hide-button = .title = Celar le vista assembly +# The "◀" button above the assembly view. +AssemblyView--prev-button = + .title = Precedente +# The "▶" button above the assembly view. +AssemblyView--next-button = + .title = Sequente +# 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 } de { $total } ## UploadedRecordingsHome ## This is the page that displays all the profiles that user has uploaded. diff --git a/locales/it/app.ftl b/locales/it/app.ftl index 115c1c7508..eab848f17e 100644 --- a/locales/it/app.ftl +++ b/locales/it/app.ftl @@ -259,6 +259,7 @@ Home--additional-content-title = Carica profili esistenti Home--additional-content-content = È possibile trascinare e rilasciare qui un profilo per caricarlo, oppure: Home--compare-recordings-info = È anche possibile confrontare diverse registrazioni. Apri l’interfaccia per il confronto. Home--your-recent-uploaded-recordings-title = Le tue registrazioni caricate di recente +Home--dark-mode-title = Modalità scura # We replace the elements such as and with links to the # documentation to use these tools. Home--load-files-from-other-tools2 = @@ -345,6 +346,13 @@ MarkerContextMenu--select-the-sender-thread = Seleziona il thread di origine “ # $filter (String) - Search string that will be used to filter the markers. MarkerFiltersContextMenu--drop-samples-outside-of-markers-matching = Scarta campioni al di fuori dei marker corrispondenti a “{ $filter }” +## MarkerCopyTableContextMenu +## This is the menu when the copy icon is clicked in Marker Chart and Marker +## Table panels. + +MarkerCopyTableContextMenu--copy-table-as-plain = Copia tabella dei marker come testo normale +MarkerCopyTableContextMenu--copy-table-as-markdown = Copia tabella dei marker come Markdown + ## MarkerSettings ## This is used in all panels related to markers. @@ -353,6 +361,14 @@ MarkerSettings--panel-search = .title = Visualizza solo marker che corrispondono a un determinato nome MarkerSettings--marker-filters = .title = Filtri per i marker +MarkerSettings--copy-table = + .title = Copia tabella come testo +# 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 = Il numero di righe supera il limite: { $rows } > { $maxRows }. Verranno copiate solo le prime { $maxRows } righe. ## MarkerSidebar ## This is the sidebar component that is used in Marker Table panel. @@ -1075,6 +1091,17 @@ AssemblyView--show-button = # Assembly refers to the low-level programming language. AssemblyView--hide-button = .title = Nascondi la vista assembly +# The "◀" button above the assembly view. +AssemblyView--prev-button = + .title = Precedente +# The "▶" button above the assembly view. +AssemblyView--next-button = + .title = Successivo +# 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 } di { $total } ## UploadedRecordingsHome ## This is the page that displays all the profiles that user has uploaded. diff --git a/locales/nl/app.ftl b/locales/nl/app.ftl index 2f9cb7d349..bc8ffc1db6 100644 --- a/locales/nl/app.ftl +++ b/locales/nl/app.ftl @@ -337,6 +337,7 @@ Home--additional-content-title = Bestaande profielen laden Home--additional-content-content = U kunt een profielbestand hierheen verslepen om het te laden, of: Home--compare-recordings-info = U kunt ook opnamen vergelijken. De vergelijkingsinterface openen. Home--your-recent-uploaded-recordings-title = Uw onlangs geüploade opnamen +Home--dark-mode-title = Donkere modus # We replace the elements such as and with links to the # documentation to use these tools. Home--load-files-from-other-tools2 = @@ -429,6 +430,13 @@ MarkerContextMenu--select-the-sender-thread = Selecteer de afzenderthread ‘{ $filter }’ buiten beschouwing laten +## MarkerCopyTableContextMenu +## This is the menu when the copy icon is clicked in Marker Chart and Marker +## Table panels. + +MarkerCopyTableContextMenu--copy-table-as-plain = Markeringstabel als platte tekst kopiëren +MarkerCopyTableContextMenu--copy-table-as-markdown = Markeringstabel als Markdown kopiëren + ## MarkerSettings ## This is used in all panels related to markers. @@ -437,6 +445,14 @@ MarkerSettings--panel-search = .title = Alleen markeringen tonen die overeenkomen met een bepaalde naam MarkerSettings--marker-filters = .title = Markeringsfilters +MarkerSettings--copy-table = + .title = Tabel als tekst kopiëren +# 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 = Het aantal rijen overschrijdt de limiet: { $rows } > { $maxRows }. Alleen de eerste { $maxRows } rijen worden gekopieerd. ## MarkerSidebar ## This is the sidebar component that is used in Marker Table panel. @@ -1163,6 +1179,17 @@ AssemblyView--show-button = # Assembly refers to the low-level programming language. AssemblyView--hide-button = .title = De samenstellingsweergave verbergen +# The "◀" button above the assembly view. +AssemblyView--prev-button = + .title = Vorige +# The "▶" button above the assembly view. +AssemblyView--next-button = + .title = Volgende +# 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 } van { $total } ## UploadedRecordingsHome ## This is the page that displays all the profiles that user has uploaded. diff --git a/locales/pt-BR/app.ftl b/locales/pt-BR/app.ftl index fb19db7a7c..dfeb3d053f 100644 --- a/locales/pt-BR/app.ftl +++ b/locales/pt-BR/app.ftl @@ -272,6 +272,7 @@ Home--additional-content-title = Carregar profiles existentes Home--additional-content-content = Você pode arrastar e soltar aqui um arquivo de profile para carregar, ou: Home--compare-recordings-info = Você também pode comparar gravações. Abra a interface de comparação. Home--your-recent-uploaded-recordings-title = Suas gravações enviadas recentemente +Home--dark-mode-title = Modo escuro # We replace the elements such as and with links to the # documentation to use these tools. Home--load-files-from-other-tools2 = O { -profiler-brand-name } também pode importar profiles de outros criadores de profile, como o Linux perf, o Android SimplePerf, o painel de desempenho do Chrome, o Android Studio, ou qualquer arquivo nos formatos dhat ou Trace Event do Google. Saiba como criar seu próprio importador. @@ -352,6 +353,13 @@ MarkerContextMenu--select-the-sender-thread = Selecionar o thread remetente “< # $filter (String) - Search string that will be used to filter the markers. MarkerFiltersContextMenu--drop-samples-outside-of-markers-matching = Descartar amostras fora dos marcadores correspondentes a “{ $filter }” +## MarkerCopyTableContextMenu +## This is the menu when the copy icon is clicked in Marker Chart and Marker +## Table panels. + +MarkerCopyTableContextMenu--copy-table-as-plain = Copiar tabela de marcadores como texto simples +MarkerCopyTableContextMenu--copy-table-as-markdown = Copiar tabela de marcadores como Markdown + ## MarkerSettings ## This is used in all panels related to markers. @@ -360,6 +368,14 @@ MarkerSettings--panel-search = .title = Só exibir marcadores que correspondem a um determinado nome MarkerSettings--marker-filters = .title = Filtros de marcação +MarkerSettings--copy-table = + .title = Copiar tabela como texto +# 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 = O número de linhas excede o limite: { $rows } > { $maxRows }. Somente as primeiras { $maxRows } linhas serão copiadas. ## MarkerSidebar ## This is the sidebar component that is used in Marker Table panel. @@ -1086,6 +1102,17 @@ AssemblyView--show-button = # Assembly refers to the low-level programming language. AssemblyView--hide-button = .title = Ocultar a exibição em assembly +# The "◀" button above the assembly view. +AssemblyView--prev-button = + .title = Anterior +# The "▶" button above the assembly view. +AssemblyView--next-button = + .title = Próximo +# 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 } de { $total } ## UploadedRecordingsHome ## This is the page that displays all the profiles that user has uploaded. diff --git a/locales/ru/app.ftl b/locales/ru/app.ftl index 24e3cdfcba..b0d2d5b19a 100644 --- a/locales/ru/app.ftl +++ b/locales/ru/app.ftl @@ -81,7 +81,7 @@ CallNodeContextMenu--transform-focus-function-inverted = Сфокусирова .title = { CallNodeContextMenu--transform-focus-function-title } CallNodeContextMenu--transform-focus-subtree = Сфокусироваться только на поддереве .title = - Фокусировка на поддереве приведет к удалению любого сэмпла, который не включает эту + Фокусировка на поддереве приведёт к удалению любого сэмпла, который не включает эту конкретную часть дерева вызовов. Она извлекает ветвь дерева вызовов, однако делает это только для этого единственного узла вызова. Все остальные вызовы функции игнорируются. @@ -94,7 +94,7 @@ CallNodeContextMenu--transform-focus-category = Сфокусироваться тем самым объединяя все узлы, принадлежащие к другой категории. CallNodeContextMenu--transform-collapse-function-subtree = Свернуть функцию .title = - Сворачивание функции приведет к удалению всего, что она вызвала, и назначению + Сворачивание функции приведёт к удалению всего, что она вызвала, и назначению функции всего времени. Это может помочь упростить профиль, который вызывает код, не нуждающийся в анализе. # This is used as the context menu item to apply the "Collapse resource" transform. @@ -102,8 +102,8 @@ CallNodeContextMenu--transform-collapse-function-subtree = Свернуть фу # $nameForResource (String) - Name of the resource to collapse. CallNodeContextMenu--transform-collapse-resource = Свернуть { $nameForResource } .title = - Сворачивание ресурса сведет все вызовы к этому - ресурсу в один свернутый узел вызова. + Сворачивание ресурса сведёт все вызовы к этому + ресурсу в один свёрнутый узел вызова. CallNodeContextMenu--transform-collapse-recursion = Свернуть рекурсию .title = Сворачивание рекурсии удаляет вызовы, которые многократно рекурсируют в @@ -335,6 +335,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 = @@ -380,7 +381,7 @@ ListOfPublishedProfiles--uploaded-profile-information-list-empty = Профил # This string is used below the 'Your recent uploaded recordings' list section. # Variables: # $profilesRestCount (Number) - Remaining numbers of the uploaded profiles which are not listed under 'Your recent uploaded recordings'. -ListOfPublishedProfiles--uploaded-profile-information-label = Просматривайте и управляйте всеми своими записями (еще { $profilesRestCount }) +ListOfPublishedProfiles--uploaded-profile-information-label = Просматривайте и управляйте всеми своими записями (ещё { $profilesRestCount }) # Depending on the number of uploaded profiles, the message is different. # Variables: # $uploadedProfileCount (Number) - Total numbers of the uploaded profiles. @@ -427,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. @@ -435,6 +443,14 @@ MarkerSettings--panel-search = .title = Отображать только маркеры, совпадающие с определённым именем 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. @@ -773,7 +789,7 @@ StackSettings--call-tree-strategy-timing = Тайминги .title = Суммировать, используя выборочные стеки выполняемого кода с течением времени StackSettings--call-tree-strategy-js-allocations = Распределения JavaScript .title = Суммировать, используя выделенные байты JavaScript (без отмены выделения) -StackSettings--call-tree-strategy-native-retained-allocations = Сохраненная память +StackSettings--call-tree-strategy-native-retained-allocations = Сохранённая память .title = Суммировать, используя байты памяти, которые были выделены и никогда не освобождались при текущем выборе предварительного просмотра StackSettings--call-tree-native-allocations = Выделенная память .title = Суммировать, используя выделенные байты памяти @@ -1172,6 +1188,17 @@ AssemblyView--show-button = # Assembly refers to the low-level programming language. AssemblyView--hide-button = .title = Скрыть вид сборки +# 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/sv-SE/app.ftl b/locales/sv-SE/app.ftl index 3d219db975..f9dbf24446 100644 --- a/locales/sv-SE/app.ftl +++ b/locales/sv-SE/app.ftl @@ -332,6 +332,7 @@ Home--additional-content-title = Ladda befintliga profiler Home--additional-content-content = Du kan dra och släppa en profilfil här för att ladda den, eller: Home--compare-recordings-info = Du kan också jämföra inspelningar.Öppna gränssnitt för att jämföra. Home--your-recent-uploaded-recordings-title = Dina senaste uppladdade inspelningar +Home--dark-mode-title = Mörkt läge # We replace the elements such as and with links to the # documentation to use these tools. Home--load-files-from-other-tools2 = @@ -424,6 +425,13 @@ MarkerContextMenu--select-the-sender-thread = Välj avsändartråden "{ # $filter (String) - Search string that will be used to filter the markers. MarkerFiltersContextMenu--drop-samples-outside-of-markers-matching = Kasta prover utanför markörer som matchar "{ $filter }" +## MarkerCopyTableContextMenu +## This is the menu when the copy icon is clicked in Marker Chart and Marker +## Table panels. + +MarkerCopyTableContextMenu--copy-table-as-plain = Kopiera markörtabell som vanlig text +MarkerCopyTableContextMenu--copy-table-as-markdown = Kopiera markörtabell som Markdown + ## MarkerSettings ## This is used in all panels related to markers. @@ -432,6 +440,14 @@ MarkerSettings--panel-search = .title = Visa endast markörer som matchar ett visst namn MarkerSettings--marker-filters = .title = Markörfilter +MarkerSettings--copy-table = + .title = Kopiera tabell som text +# 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 = Antalet rader överskrider gränsen: { $rows } > { $maxRows }. Endast de första { $maxRows } raderna kommer att kopieras. ## MarkerSidebar ## This is the sidebar component that is used in Marker Table panel. @@ -1158,6 +1174,17 @@ AssemblyView--show-button = # Assembly refers to the low-level programming language. AssemblyView--hide-button = .title = Dölj assembly-vyn +# The "◀" button above the assembly view. +AssemblyView--prev-button = + .title = Föregående +# The "▶" button above the assembly view. +AssemblyView--next-button = + .title = Nästa +# 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 } av { $total } ## UploadedRecordingsHome ## This is the page that displays all the profiles that user has uploaded. diff --git a/locales/tr/app.ftl b/locales/tr/app.ftl index 009537f71c..8c2f18f593 100644 --- a/locales/tr/app.ftl +++ b/locales/tr/app.ftl @@ -262,6 +262,7 @@ Home--additional-content-title = Mevcut profilleri yükleyin Home--additional-content-content = Profil dosyasını buraya sürükleyip bırakarak yükleyebilirsiniz ya da: Home--compare-recordings-info = Ayrıca kayıtları karşılaştırabilirsiniz. Karşılaştırma arayüzünü aç. Home--your-recent-uploaded-recordings-title = Son yüklediğiniz kayıtlar +Home--dark-mode-title = Koyu mod # We replace the elements such as and with links to the # documentation to use these tools. Home--load-files-from-other-tools2 = { -profiler-brand-name } ayrıca Linux perf, Android SimplePerf, Chrome performans paneli, Android Studio gibi diğer profilleyicilerden ve dhat biçimini veya Google’ın trace etkinliği biçimini kullanan herhangi bir dosyadan profilleri içe aktarabilir. Kendi içe aktarıcınızı yazmayı öğrenin. @@ -335,6 +336,13 @@ MarkerContextMenu--select-the-receiver-thread = “{ $threadName }{ $threadName }” gönderen iş parçacığını seç +## MarkerCopyTableContextMenu +## This is the menu when the copy icon is clicked in Marker Chart and Marker +## Table panels. + +MarkerCopyTableContextMenu--copy-table-as-plain = İşaretçi tablosunu düz metin olarak kopyala +MarkerCopyTableContextMenu--copy-table-as-markdown = İşaretçi tablosunu Markdown olarak kopyala + ## MarkerSettings ## This is used in all panels related to markers. @@ -343,6 +351,8 @@ MarkerSettings--panel-search = .title = Yalnızca belirli bir adla eşleşen işaretçileri görüntüler MarkerSettings--marker-filters = .title = İşaretçi filtreleri +MarkerSettings--copy-table = + .title = Tabloyu metin olarak kopyala ## MarkerSidebar ## This is the sidebar component that is used in Marker Table panel. diff --git a/locales/zh-CN/app.ftl b/locales/zh-CN/app.ftl index 7b087d0fdb..feae44c7d0 100644 --- a/locales/zh-CN/app.ftl +++ b/locales/zh-CN/app.ftl @@ -342,6 +342,13 @@ MarkerContextMenu--select-the-sender-thread = 选择 Sender 线程“{ $ # $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. @@ -350,6 +357,14 @@ MarkerSettings--panel-search = .title = 只显示匹配特定名称的标记 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. @@ -443,6 +458,7 @@ MenuButtons--metaInfo--profiling-session = 记录长度: MenuButtons--metaInfo--main-process-started = 主进程开始: MenuButtons--metaInfo--main-process-ended = 主进程结束: MenuButtons--metaInfo--file-name = 文件名: +MenuButtons--metaInfo--file-size = 文件大小: MenuButtons--metaInfo--interval = 间隔: MenuButtons--metaInfo--buffer-capacity = 缓冲容量: MenuButtons--metaInfo--buffer-duration = 缓冲间隔: diff --git a/locales/zh-TW/app.ftl b/locales/zh-TW/app.ftl index af2f33ef56..518a5f9127 100644 --- a/locales/zh-TW/app.ftl +++ b/locales/zh-TW/app.ftl @@ -259,6 +259,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 = @@ -341,6 +342,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. @@ -349,6 +357,14 @@ MarkerSettings--panel-search = .title = 只顯示符合特定名稱的標記 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. @@ -1065,6 +1081,17 @@ AssemblyView--show-button = # Assembly refers to the low-level programming language. AssemblyView--hide-button = .title = 隱藏機器碼畫面 +# 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/package.json b/package.json index 0589a213b7..e8ee81c575 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,9 @@ "version": ">= 22 < 23" } }, + "browser": { + "./src/utils/gz.ts": "./src/utils/gz.browser.ts" + }, "scripts": { "build:clean": "rimraf dist && mkdirp dist", "build:quiet": "yarn build:clean && cross-env NODE_ENV=development webpack", @@ -44,6 +47,7 @@ "start-docs": "ws -d docs-user/ -p 3000", "start-photon": "node res/photon/server", "test": "node bin/output-fixing-commands.js cross-env LC_ALL=C TZ=UTC NODE_ENV=test jest", + "test-node": "node bin/output-fixing-commands.js cross-env LC_ALL=C TZ=UTC NODE_ENV=test JEST_ENVIRONMENT=node jest", "test-all": "run-p --max-parallel 4 ts license-check lint test test-alex test-lockfile", "test-build-coverage": "yarn test --coverage --coverageReporters=html", "test-serve-coverage": "ws -d coverage/ -p 4343", @@ -62,9 +66,9 @@ "@codemirror/lang-cpp": "^6.0.3", "@codemirror/lang-javascript": "^6.2.4", "@codemirror/lang-rust": "^6.0.2", - "@codemirror/language": "^6.11.3", - "@codemirror/state": "^6.5.2", - "@codemirror/view": "^6.38.8", + "@codemirror/language": "^6.12.1", + "@codemirror/state": "^6.5.3", + "@codemirror/view": "^6.39.11", "@firefox-devtools/react-contextmenu": "^5.2.3", "@fluent/bundle": "^0.19.1", "@fluent/langneg": "^0.7.0", @@ -90,7 +94,7 @@ "mixedtuplemap": "^1.0.0", "namedtuplemap": "^1.0.0", "photon-colors": "^3.3.2", - "protobufjs": "^7.5.4", + "protobufjs": "^8.0.0", "query-string": "^9.3.1", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -108,23 +112,23 @@ "workbox-window": "^7.4.0" }, "devDependencies": { - "@babel/cli": "^7.28.3", - "@babel/core": "^7.28.5", - "@babel/eslint-parser": "^7.28.5", + "@babel/cli": "^7.28.6", + "@babel/core": "^7.28.6", + "@babel/eslint-parser": "^7.28.6", "@babel/eslint-plugin": "^7.27.1", "@babel/plugin-proposal-class-properties": "^7.18.6", - "@babel/preset-env": "^7.28.5", + "@babel/preset-env": "^7.28.6", "@babel/preset-react": "^7.28.5", "@babel/preset-typescript": "^7.28.5", - "@eslint/js": "^9.39.1", + "@eslint/js": "^9.39.2", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.9.1", - "@testing-library/react": "^16.3.0", + "@testing-library/react": "^16.3.1", "@types/clamp": "^1.0.3", "@types/common-tags": "^1.8.4", "@types/jest": "^30.0.0", "@types/minimist": "^1.2.5", - "@types/node": "^22.19.2", + "@types/node": "^22.19.7", "@types/query-string": "^6.3.0", "@types/react": "^18.3.27", "@types/react-dom": "^18.3.1", @@ -132,34 +136,34 @@ "@types/react-transition-group": "^4.4.5", "@types/redux-logger": "^3.0.6", "@types/tgwf__co2": "^0.14.2", - "@typescript-eslint/eslint-plugin": "^8.48.0", - "@typescript-eslint/parser": "^8.48.0", + "@typescript-eslint/eslint-plugin": "^8.51.0", + "@typescript-eslint/parser": "^8.51.0", "alex": "^11.0.1", - "autoprefixer": "^10.4.22", + "autoprefixer": "^10.4.23", "babel-jest": "^30.2.0", "babel-loader": "^10.0.0", "babel-plugin-module-resolver": "^5.0.2", - "browserslist": "^4.28.0", - "caniuse-lite": "^1.0.30001754", + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001762", "circular-dependency-plugin": "^5.2.1", "copy-webpack-plugin": "^13.0.1", "cross-env": "^10.1.0", "css-loader": "^7.1.2", "cssnano": "^7.1.2", "devtools-license-check": "^0.9.0", - "eslint": "^9.39.1", + "eslint": "^9.39.2", "eslint-config-prettier": "^10.1.8", "eslint-import-resolver-alias": "^1.1.2", "eslint-plugin-import": "^2.32.0", - "eslint-plugin-jest": "^29.2.1", + "eslint-plugin-jest": "^29.12.1", "eslint-plugin-jest-dom": "^5.5.0", "eslint-plugin-jest-formatting": "^3.1.0", "eslint-plugin-react": "^7.37.5", - "eslint-plugin-testing-library": "^7.13.4", + "eslint-plugin-testing-library": "^7.15.4", "fake-indexeddb": "^6.2.5", "fetch-mock": "^12.6.0", "file-loader": "^6.2.0", - "globals": "^16.5.0", + "globals": "^17.0.0", "html-webpack-plugin": "^5.6.5", "husky": "^4.3.8", "jest": "^30.2.0", @@ -178,13 +182,14 @@ "prettier": "^3.7.4", "rimraf": "^6.1.2", "style-loader": "^4.0.0", - "stylelint": "^16.26.1", + "stylelint": "^17.0.0", "stylelint-config-idiomatic-order": "^10.0.0", - "stylelint-config-standard": "^39.0.1", + "stylelint-config-standard": "^40.0.0", "typescript": "^5.9.3", - "webpack": "^5.103.0", + "typescript-eslint": "^8.52.0", + "webpack": "^5.104.1", "webpack-cli": "^6.0.1", - "webpack-dev-server": "^5.2.2", + "webpack-dev-server": "^5.2.3", "workbox-cli": "^7.4.0", "yargs": "^18.0.0" }, diff --git a/res/css/categories.css b/res/css/categories.css index 8d1d533854..1535799653 100644 --- a/res/css/categories.css +++ b/res/css/categories.css @@ -20,6 +20,18 @@ --category-color-darkgrey: var(--grey-50); } +:root.dark-mode { + --category-color-purple: var(--purple-60); + --category-color-green: var(--green-80); + --category-color-orange: var(--orange-60); + --category-color-yellow: var(--yellow-70); + --category-color-magenta: var(--magenta-70); + --category-color-gray: var(--grey-50); + --category-color-grey: var(--grey-50); + --category-color-darkgray: var(--grey-60); + --category-color-darkgrey: var(--grey-60); +} + /** * These classes should be used to create a small color swatch to describe a * category. They should be used with the class `colored-square` that's defined diff --git a/res/css/focus.css b/res/css/focus.css index a6291dcff1..3925aa3858 100644 --- a/res/css/focus.css +++ b/res/css/focus.css @@ -18,15 +18,15 @@ input[type='range']:focus-visible, select:focus-visible, button:focus-visible { box-shadow: - 0 0 0 1px var(--blue-50) inset, - 0 0 0 1px var(--blue-50), - 0 0 0 4px var(--blue-50-a30); + 0 0 0 1px var(--focus-border-color) inset, + 0 0 0 1px var(--focus-border-color), + 0 0 0 4px var(--focus-shadow-color); outline: 0; } a:focus-visible { box-shadow: - 0 0 0 2px var(--blue-50), - 0 0 0 6px var(--blue-50-a30); + 0 0 0 2px var(--focus-border-color), + 0 0 0 6px var(--focus-shadow-color); outline: 0; } diff --git a/res/css/global.css b/res/css/global.css index 72c99c9064..28de1bcff0 100644 --- a/res/css/global.css +++ b/res/css/global.css @@ -2,6 +2,118 @@ * 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/. */ +:root { + --base-foreground-color: #000; + --base-background-color: #fff; + --base-border-color: var(--grey-30); + --base-shadow-color: rgb(0 0 0 / 0.2); + --link-foreground-color: LinkText; + --link-visited-foreground-color: VisitedText; + --link-active-foreground-color: ActiveText; + --clickable-foreground-color: var(--grey-90); + --clickable-background-color: var(--grey-90-a10); + --clickable-border-color: var(--grey-90-a30); + --clickable-hover-background-color: var(--grey-90-a20); + --clickable-active-background-color: var(--grey-90-a30); + --clickable-ghost-hover-background-color: var(--grey-90-a10); + --clickable-ghost-active-background-color: var(--grey-90-a20); + --clickable-checked-background-color: var(--blue-60); + --clickable-checked-hover-background-color: var(--blue-70); + --clickable-checked-active-background-color: var(--blue-80); + --focus-border-color: var(--blue-50); + --focus-shadow-color: var(--blue-50-a30); + --kbd-foreground-color: var(--base-foreground-color); + --kbd-background-color: #f6f6f6; + --kbd-border-color: #ccc; + --kbd-shadow-color: #bbb; + --home-border-color: #ccc; + --home-shadow-color: #0b1f50; + --row-odd-background-color: #f5f5f5; + --home-foreground-color: #000; + --home-background-color: #fff; + --menu-foreground-color: #000; + --menu-background-color: #fff; + --lowered-foreground-color: #000; + --lowered-background-color: var(--grey-10); + --raised-foreground-color: #000; + --raised-background-color: #fff; + --panel-foreground-color: var(--grey-70); + --panel-background-color: var(--grey-10); + --panel-border-color: var(--grey-30); + --selected-track-background-color: #edf6ff; + --tooltip-number-foreground-color: var(--grey-60); + --colored-border-color: rgb(0 0 0 / 0.1); + --wide-splitter-color: #ccc; + --wide-splitter-hover-color: #bbb; + --wide-splitter-pressed-color: #aaa; + + color: var(--base-foreground-color); +} + +:root.dark-mode { + color-scheme: dark; + + --grey-85: color-mix(var(--grey-80) 60%, var(--grey-90)); + --base-foreground-color: var(--grey-20); + --base-background-color: #18181a; + --base-border-color: var(--grey-70); + --base-shadow-color: rgb(0 0 0 / 0.5); + --link-foreground-color: var(--blue-40); + --link-visited-foreground-color: var(--purple-40); + --link-active-foreground-color: var(--red-50); + --clickable-foreground-color: var(--grey-20); + --clickable-background-color: var(--grey-10-a10); + --clickable-border-color: var(--grey-10-a40); + --clickable-hover-background-color: var(--grey-10-a20); + --clickable-active-background-color: var(--grey-10-a40); + --clickable-ghost-hover-background-color: var(--grey-10-a10); + --clickable-ghost-active-background-color: var(--grey-10-a20); + --clickable-checked-background-color: var(--blue-50); + --clickable-checked-hover-background-color: var(--blue-60); + --clickable-checked-active-background-color: var(--blue-70); + --focus-border-color: var(--blue-40); + --focus-shadow-color: var(--blue-50-a30); + --kbd-background-color: var(--grey-70); + --kbd-border-color: var(--grey-60); + --kbd-shadow-color: var(--grey-50); + --home-border-color: var(--ink-70); + --home-shadow-color: var(--ink-90); + --row-odd-background-color: var(--raised-background-color); + --home-foreground-color: var(--grey-20); + --home-background-color: var(--ink-90); + --menu-foreground-color: var(--grey-20); + --menu-background-color: var(--grey-80); + --lowered-foreground-color: var(--grey-40); + --lowered-background-color: var(--grey-90); + --raised-foreground-color: var(--grey-20); + --raised-background-color: #232327; + --panel-foreground-color: var(--grey-20); + --panel-background-color: var(--raised-background-color); + --panel-border-color: var(--grey-60); + --selected-track-background-color: color-mix( + in hsl, + var(--grey-90) 60%, + var(--teal-50) + ); + --tooltip-number-foreground-color: var(--grey-40); + --colored-border-color: rgb(237 237 240 / 0.1); + --wide-splitter-color: var(--grey-70); + --wide-splitter-hover-color: var(--grey-60); + --wide-splitter-pressed-color: var(--grey-60); +} + +a { + color: var(--link-foreground-color); +} + +a:visited { + color: var(--link-visited-foreground-color); +} + +a:active { + color: var(--link-active-foreground-color); +} + /** * This class should be used to create a small colored square. It's used * especially for categories and network mime types. @@ -11,7 +123,7 @@ width: 9px; height: 9px; box-sizing: border-box; - border: 0.5px solid rgb(0 0 0 / 0.1); + border: 0.5px solid var(--colored-border-color); margin-right: 3px; /* Opt-out of forced colors so the color is applied */ @@ -19,7 +131,7 @@ } .colored-border { - border: 2px solid rgb(0 0 0 / 0.1); + border: 2px solid var(--colored-border-color); margin-right: 3px; /* Opt-out of forced colors so the color is applied */ @@ -29,3 +141,15 @@ .colored-border.ellipsis { opacity: 0; } + +.splitter-layout.splitter-layout > .layout-splitter { + background-color: var(--wide-splitter-color); +} + +.splitter-layout.splitter-layout > .layout-splitter:hover { + background-color: var(--wide-splitter-hover-color); +} + +.splitter-layout.splitter-layout.layout-changing > .layout-splitter { + background-color: var(--wide-splitter-pressed-color); +} diff --git a/res/css/photon/button.css b/res/css/photon/button.css index aeff4be412..280ac9a87c 100644 --- a/res/css/photon/button.css +++ b/res/css/photon/button.css @@ -4,6 +4,15 @@ /* See https://design.firefox.com/photon/components/buttons.html for the spec */ .photon-button { + --internal-primary-foreground-color: #fff; + --internal-primary-background-color: var(--blue-60); + --internal-primary-hover-background-color: var(--blue-70); + --internal-primary-active-background-color: var(--blue-80); + --internal-destructive-foreground-color: #fff; + --internal-destructive-background-color: var(--red-60); + --internal-destructive-hover-background-color: var(--red-70); + --internal-destructive-active-background-color: var(--red-80); + /* These flex and sizing properties aren't necessary when a real + + + + + + ); + } +} + +export const AssemblyViewNativeSymbolNavigator = explicitConnect< + {}, + StateProps, + DispatchProps +>({ + mapStateToProps: (state) => ({ + assemblyViewCurrentNativeSymbolEntryIndex: + getAssemblyViewCurrentNativeSymbolEntryIndex(state), + assemblyViewNativeSymbolEntryCount: + getAssemblyViewNativeSymbolEntryCount(state), + }), + mapDispatchToProps: { + changeAssemblyViewNativeSymbolEntryIndex, + }, + component: AssemblyViewNativeSymbolNavigatorImpl, +}); diff --git a/src/components/app/BottomBox.css b/src/components/app/BottomBox.css index 683dce9a99..d337882d52 100644 --- a/src/components/app/BottomBox.css +++ b/src/components/app/BottomBox.css @@ -3,9 +3,14 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ .bottom-box-pane { + --internal-sourceview-background-color: var(--grey-20); + --internal-close-icon: url(../../../res/img/svg/close-dark.svg); + --internal-assembly-icon: url(../../../res/img/svg/asm-icon.svg); + display: flex; height: 100%; /* direct child of SplitterLayout */ flex-flow: column nowrap; + color: var(--base-foreground-color); } .bottom-sourceview-wrapper, @@ -15,14 +20,14 @@ min-height: 0; flex: 1; flex-flow: column; - background: var(--grey-20); + background: var(--internal-sourceview-background-color); } .bottom-box .layout-splitter { position: relative; /* containing block for absolute ::before */ width: 1px; border: none; - background-color: var(--grey-30) !important; + background-color: var(--base-border-color) !important; } /* Provide 3px extra grabbable surface on each side of the splitter */ @@ -41,12 +46,13 @@ flex-flow: row; align-items: center; justify-content: space-between; - border-bottom: 1px solid var(--grey-30); - background: var(--grey-10); + border-bottom: 1px solid var(--panel-border-color); + background: var(--panel-background-color); line-height: 18px; } -.bottom-box-title { +.bottom-box-title, +.bottom-box-title-trailer { overflow: hidden; margin: 0 8px; font: inherit; @@ -54,6 +60,10 @@ white-space: nowrap; } +.bottom-box-title-trailer { + margin: 0; +} + .bottom-box-header-trailing-buttons { display: flex; height: 100%; @@ -62,7 +72,9 @@ } .bottom-close-button, -.bottom-assembly-button { +.bottom-assembly-button, +.bottom-prev-button, +.bottom-next-button { width: 24px; height: 24px; flex-shrink: 0; @@ -71,17 +83,29 @@ background-size: 16px 16px; } +.bottom-prev-button, +.bottom-next-button { + width: 16px; + font-size: 9px; +} + +.bottom-next-button { + margin-right: 8px; +} + .bottom-close-button { - background-image: url(../../../res/img/svg/close-dark.svg); + background-image: var(--internal-close-icon); } .bottom-assembly-button { - background-image: url(../../../res/img/svg/asm-icon.svg); + background-image: var(--internal-assembly-icon); } .codeLoadingOverlay, .sourceCodeErrorOverlay, .assemblyCodeErrorOverlay { + --internal-background-color: rgb(240 240 240 / 0.8); + /** * Put the overlay on top of everything in .bottom-main, but centered * horizontally and vertically. We center using margin: auto, and enforce @@ -99,7 +123,7 @@ padding: 15px; border-radius: 10px; margin: auto; - background-color: rgb(240 240 240 / 0.8); + background-color: var(--internal-background-color); gap: 15px; inset: 0; overflow-wrap: break-word; @@ -132,3 +156,17 @@ .codeErrorOverlay { padding-left: 20px; } + +:root.dark-mode { + .bottom-box-pane { + --internal-sourceview-background-color: var(--grey-80); + --internal-close-icon: url(../../../res/img/svg/close-light.svg); + --internal-assembly-icon: url(../../../res/img/svg/asm-icon-light.svg); + } + + .codeLoadingOverlay, + .sourceCodeErrorOverlay, + .assemblyCodeErrorOverlay { + --internal-background-color: rgb(16 16 16 / 0.8); + } +} diff --git a/src/components/app/BottomBox.tsx b/src/components/app/BottomBox.tsx index c2631c42fb..3e3d161feb 100644 --- a/src/components/app/BottomBox.tsx +++ b/src/components/app/BottomBox.tsx @@ -9,30 +9,28 @@ import classNames from 'classnames'; import { SourceView } from '../shared/SourceView'; import { AssemblyView } from '../shared/AssemblyView'; import { AssemblyViewToggleButton } from './AssemblyViewToggleButton'; +import { AssemblyViewNativeSymbolNavigator } from './AssemblyViewNativeSymbolNavigator'; import { IonGraphView } from '../shared/IonGraphView'; import { CodeLoadingOverlay } from './CodeLoadingOverlay'; import { CodeErrorOverlay } from './CodeErrorOverlay'; import { getSourceViewScrollGeneration, - getSourceViewLineNumber, + getSourceViewScrollToLineNumber, + getSourceViewHighlightedLine, getAssemblyViewIsOpen, getAssemblyViewNativeSymbol, getAssemblyViewScrollGeneration, + getAssemblyViewScrollToInstructionAddress, + getAssemblyViewHighlightedInstruction, } from 'firefox-profiler/selectors/url-state'; -import { - selectedThreadSelectors, - selectedNodeSelectors, -} from 'firefox-profiler/selectors/per-thread'; +import { selectedThreadSelectors } from 'firefox-profiler/selectors/per-thread'; import { closeBottomBox } from 'firefox-profiler/actions/profile-view'; import { parseFileNameFromSymbolication } from 'firefox-profiler/utils/special-paths'; import { getSourceViewCode, getAssemblyViewCode, } from 'firefox-profiler/selectors/code'; -import { - getPreviewSelectionIsBeingModified, - getSourceViewFile, -} from 'firefox-profiler/selectors/profile'; +import { getSourceViewFile } from 'firefox-profiler/selectors/profile'; import explicitConnect from 'firefox-profiler/utils/connect'; import type { ConnectedProps } from 'firefox-profiler/utils/connect'; @@ -55,16 +53,16 @@ type StateProps = { readonly sourceViewFile: string | null; readonly sourceViewCode: SourceCodeStatus | void; readonly sourceViewScrollGeneration: number; - readonly sourceViewLineNumber?: number; + readonly sourceViewScrollToLineNumber?: number; + readonly sourceViewHighlightedLine: number | null; readonly globalLineTimings: LineTimings; - readonly selectedCallNodeLineTimings: LineTimings; readonly assemblyViewIsOpen: boolean; readonly assemblyViewNativeSymbol: NativeSymbolInfo | null; readonly assemblyViewCode: AssemblyCodeStatus | void; readonly assemblyViewScrollGeneration: number; + readonly assemblyViewScrollToInstructionAddress?: number; + readonly assemblyViewHighlightedInstruction: number | null; readonly globalAddressTimings: AddressTimings; - readonly selectedCallNodeAddressTimings: AddressTimings; - readonly disableOverscan: boolean; }; type DispatchProps = { @@ -160,16 +158,16 @@ class BottomBoxImpl extends React.PureComponent { sourceViewFile, sourceViewCode, globalLineTimings, - disableOverscan, sourceViewScrollGeneration, - sourceViewLineNumber, - selectedCallNodeLineTimings, + sourceViewScrollToLineNumber, + sourceViewHighlightedLine, assemblyViewIsOpen, assemblyViewScrollGeneration, + assemblyViewScrollToInstructionAddress, + assemblyViewHighlightedInstruction, assemblyViewNativeSymbol, assemblyViewCode, globalAddressTimings, - selectedCallNodeAddressTimings, } = this.props; const sourceCode = sourceViewCode && sourceViewCode.type === 'AVAILABLE' @@ -195,6 +193,7 @@ class BottomBoxImpl extends React.PureComponent { // These trailing header buttons go into the bottom-box-bar of the last pane. const trailingHeaderButtons = (
+ {assemblyViewIsOpen ? : null} + ); } else { // There are committed ranges, don't make it button because this will @@ -169,6 +177,9 @@ class ProfileFilterNavigatorBarImpl extends React.PureComponent { selectedItem={selectedItem} uncommittedItem={uncommittedItem} onPop={onPop} + onFirstItemClick={ + isFirstItemClickable ? this._onFirstItemClick : undefined + } /> {pageDataByTabID && pageDataByTabID.size > 0 ? ( diff --git a/src/components/app/ProfileName.css b/src/components/app/ProfileName.css index 4a34f40ec6..a937377d77 100644 --- a/src/components/app/ProfileName.css +++ b/src/components/app/ProfileName.css @@ -3,18 +3,22 @@ * file, you can obtain one at http://mozilla.org/mpl/2.0/. */ .profileNameInput { + --internal-foreground-color: var(--grey-60); + height: 17px; padding: 0 6px; border: none; border-radius: 1px; margin: 4px; - color: var(--grey-60); + color: var(--internal-foreground-color); font: inherit; font-weight: 700; line-height: 17px; } .profileNameButton { + --internal-icon: url(../../../res/img/svg/edit-name-profiler.svg); + overflow: hidden; min-width: 80px; padding: 0 9px; @@ -24,16 +28,17 @@ white-space: nowrap; } -.profileNameButton::before { - flex-shrink: 0; - background-image: url(../../../res/img/svg/edit-name-profiler.svg); +:root.dark-mode { + .profileNameInput { + --internal-foreground-color: var(--base-foreground-color); + } + + .profileNameButton { + --internal-icon: url(../../../res/img/svg/edit-name-profiler-light.svg); + } } -/* Using the style for links rather than for normal inputs, because we don't - * want to trigger a border that would move things around. */ -.profileNameInput:focus-visible { - box-shadow: - 0 0 0 2px var(--blue-50), - 0 0 0 4px var(--blue-50-a30); - outline: 0; +.profileNameButton::before { + flex-shrink: 0; + background-image: var(--internal-icon); } diff --git a/src/components/app/ProfileName.tsx b/src/components/app/ProfileName.tsx index 12e78e7df9..2b6e6e235c 100644 --- a/src/components/app/ProfileName.tsx +++ b/src/components/app/ProfileName.tsx @@ -144,7 +144,7 @@ class ProfileNameImpl extends React.PureComponent { // Make sure and use the profile name and focus generation to support the // back button invalidating the state key={key} - className="profileNameInput" + className="profileNameInput photon-input" style={{ display: isFocused ? undefined : 'none', }} diff --git a/src/components/app/ProfileRootMessage.css b/src/components/app/ProfileRootMessage.css index cd3314ab61..3341e30bf3 100644 --- a/src/components/app/ProfileRootMessage.css +++ b/src/components/app/ProfileRootMessage.css @@ -19,10 +19,10 @@ max-width: 600px; box-sizing: border-box; padding: 3em; - border: 1px solid #ccc; + border: 1px solid var(--home-border-color); border-radius: 3px; - background-color: #fff; - box-shadow: 0 5px 25px #0b1f50; + background-color: var(--home-background-color); + box-shadow: 0 5px 25px var(--home-shadow-color); font-size: 130%; } @@ -38,7 +38,7 @@ .rootMessageAdditional { padding-top: 1em; - border-top: 1px solid #ccc; + border-top: 1px solid var(--home-border-color); margin-top: 1em; font-size: 12px; } diff --git a/src/components/app/ProfileViewer.css b/src/components/app/ProfileViewer.css index e59a7ccdca..36b349703b 100644 --- a/src/components/app/ProfileViewer.css +++ b/src/components/app/ProfileViewer.css @@ -35,13 +35,15 @@ } .profileViewerWrapper { + --internal-background-color: var(--grey-40); + display: flex; min-width: 0; flex: 1; } .profileViewerWrapperBackground { - background-color: var(--grey-40); + background-color: var(--internal-background-color); } .profileViewer { @@ -51,7 +53,7 @@ flex-flow: column nowrap; animation-duration: 200ms; animation-name: profileViewerFadeIn; - background-color: #fff; + background-color: var(--panel-background-color); } .profileViewerFadeOut { @@ -68,6 +70,11 @@ } .profileViewerZipButton { + --internal-background-color: var(--green-50); + --internal-border-color: var(--green-60); + --internal-icon: url(../../../res/img/svg/back-arrow.svg); + --internal-hover-border-color: #000; + /* Position */ width: 18px; height: 18px; @@ -75,18 +82,31 @@ /* Box */ flex: none; padding: 0; - border: 1px solid var(--green-60); + border: 1px solid var(--internal-border-color); border-radius: 3px; margin: 3px 0 3px 3px; /* Other */ - background: var(--green-50) url(../../../res/img/svg/back-arrow.svg) center + background: var(--internal-background-color) var(--internal-icon) center center no-repeat; - color: #000; + color: var(--base-foreground-color); } .profileViewerZipButton:hover { - border-color: #000; + border-color: var(--profile-viewer-zip-hover-border-color); +} + +:root.dark-mode { + .profileViewerWrapper { + --internal-background-color: var(--grey-70); + } + + .profileViewerZipButton { + --internal-background-color: var(--grey-50); + --internal-border-color: var(--grey-60); + --internal-icon: url(../../../res/img/svg/back-arrow-light.svg); + --internal-hover-border-color: var(--grey-20); + } } .profileViewerSplitter { @@ -110,9 +130,10 @@ flex: none; flex-flow: row nowrap; padding: 0; - border-bottom: 1px solid var(--grey-30); + border-bottom: 1px solid var(--base-border-color); margin: 0; - background: var(--grey-10); + background: var(--lowered-background-color); + color: var(--lowered-foreground-color); } .profileViewerSpacer { diff --git a/src/components/app/SymbolicationStatusOverlay.css b/src/components/app/SymbolicationStatusOverlay.css index 91d74b8f48..f7eeef7e8e 100644 --- a/src/components/app/SymbolicationStatusOverlay.css +++ b/src/components/app/SymbolicationStatusOverlay.css @@ -3,6 +3,8 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ .symbolicationStatusOverlay { + --internal-background-color: var(--grey-20); + position: fixed; top: -8px; right: 30%; @@ -13,10 +15,10 @@ padding-right: 10px; padding-left: 10px; border-radius: 0 0 5px 5px; - background: var(--grey-20); + background: var(--internal-background-color); box-shadow: - 0 0 0 0.5px rgb(0 0 0 / 0.1), - 0 2px 4px rgb(0 0 0 / 0.1); + 0 0 0 0.5px var(--base-shadow-color), + 0 2px 4px var(--base-shadow-color); line-height: 20px; text-align: center; text-overflow: ellipsis; @@ -27,6 +29,12 @@ will-change: opacity, transform; } +:root.dark-mode { + .symbolicationStatusOverlay { + --internal-background-color: var(--grey-80); + } +} + .symbolicationStatusOverlay.hidden { opacity: 0; transform: translateY(-30px); diff --git a/src/components/app/TabBar.css b/src/components/app/TabBar.css index 06fd4bab6c..323122fe72 100644 --- a/src/components/app/TabBar.css +++ b/src/components/app/TabBar.css @@ -11,11 +11,19 @@ border: none; margin: 0; background: none; + color: inherit; font: inherit; text-overflow: ellipsis; } .tabBarTabWrapper { + --internal-foreground-color: var(--lowered-foreground-color); + --internal-selected-background-color: var(--raised-background-color); + --internal-selected-highlight-color: var(--blue-50); + --internal-selected-foreground-color: var(--raised-foreground-color); + --internal-hover-highlight-color: var(--grey-40); + --internal-hover-background-color: var(--grey-20); + display: flex; overflow: auto hidden; min-width: 0; /* This makes the tab container actually shrinkable below min-content */ @@ -27,6 +35,13 @@ scrollbar-width: none; } +:root.dark-mode { + .tabBarTabWrapper { + --internal-hover-highlight-color: var(--grey-60); + --internal-hover-background-color: var(--grey-80); + } +} + .tabBarTab { position: relative; min-width: 8em; @@ -35,10 +50,12 @@ border-width: 0 1px; margin: 0 -0.5px 1px; /* The -0.5px horizontal margin makes the 1px border between adjacent tabs overlap. The 1px bottom margin makes space for the .Details-top-bar bottom border. */ background-clip: padding-box; + color: var(--internal-foreground-color); cursor: default; font-size: 12px; text-align: center; transition: + color 200ms, background-color 200ms, border-color 200ms; transition-timing-function: var(--animation-timing); @@ -62,26 +79,27 @@ } .tabBarTab.selected::before { - background-color: var(--blue-50); + background-color: var(--internal-selected-highlight-color); transition: none; /* Switch the background color instantly when a tab is selected */ } .tabBarTab:not(.selected):hover { - border-color: var(--grey-20); - background-color: var(--grey-20); + border-color: var(--internal-hover-background-color); + background-color: var(--internal-hover-background-color); + color: var(--internal-selected-foreground-color); } .tabBarTab:not(.selected):hover::before { - background-color: var(--grey-40); + background-color: var(--internal-hover-highlight-color); } .tabBarTab.selected { z-index: 1; padding: 6px 4px; - border-color: var(--grey-30); + border-color: var(--base-border-color); margin: 0 -0.5px; /* No bottom margin, so that this tab covers the .Details-top-bar bottom border. */ - background: #fff; - color: var(--blue-60); + background: var(--internal-selected-background-color); + color: var(--internal-selected-foreground-color); transition: none; /* Switch the background color instantly when a tab is selected */ } diff --git a/src/components/app/UploadedRecordingsHome.css b/src/components/app/UploadedRecordingsHome.css index 9d9f48f335..a02b3e52e4 100644 --- a/src/components/app/UploadedRecordingsHome.css +++ b/src/components/app/UploadedRecordingsHome.css @@ -8,13 +8,13 @@ max-width: 900px; box-sizing: border-box; padding: 3em 1em; - border: 1px solid #ccc; + border: 1px solid var(--home-border-color); border-radius: 3px; margin: auto; /* Other */ - background: #fff; - box-shadow: 0 5px 25px #0b1f50; + background: var(--base-background-color); + box-shadow: 0 5px 25px var(--home-shadow-color); font-size: 13px; line-height: 1.5; } diff --git a/src/components/app/ZipFileViewer.css b/src/components/app/ZipFileViewer.css index 1e4a1b0e9d..fb5e7cb47b 100644 --- a/src/components/app/ZipFileViewer.css +++ b/src/components/app/ZipFileViewer.css @@ -28,12 +28,12 @@ /* Box */ box-sizing: border-box; flex-direction: column; - border: 1px solid #ccc; + border: 1px solid var(--home-border-color); border-radius: 3px; /* Other */ - background-color: #fff; - box-shadow: 0 5px 25px #0b1f50; + background-color: var(--base-background-color); + box-shadow: 0 5px 25px var(--home-shadow-color); } .zipFileViewerSection > .treeView { @@ -51,10 +51,10 @@ flex-direction: column; align-items: center; justify-content: center; - border-top: 1px solid var(--grey-30); + border-top: 1px solid var(--base-border-color); /* Other */ - background-color: #f5f5f5; + background-color: var(--row-odd-background-color); font-size: 14px; } @@ -63,6 +63,8 @@ } .zipFileViewerUntrustedFilePath { + --internal-background-color: var(--grey-30); + display: inline-block; overflow: hidden; @@ -73,7 +75,13 @@ max-width: 70%; padding: 0 5px; border-radius: 3px; - background-color: var(--grey-30); + background-color: var(--internal-background-color); font-family: monospace; text-overflow: ellipsis; } + +:root.dark-mode { + .zipFileViewerUntrustedFilePath { + --internal-background-color: var(--grey-70); + } +} diff --git a/src/components/flame-graph/Canvas.tsx b/src/components/flame-graph/Canvas.tsx index 58ed593c8d..a6fdecbbd2 100644 --- a/src/components/flame-graph/Canvas.tsx +++ b/src/components/flame-graph/Canvas.tsx @@ -7,7 +7,11 @@ import { withChartViewport, type Viewport } from '../shared/chart/Viewport'; import { ChartCanvas } from '../shared/chart/Canvas'; import { FastFillStyle } from '../../utils'; import TextMeasurement from '../../utils/text-measurement'; -import { mapCategoryColorNameToStackChartStyles } from '../../utils/colors'; +import { + mapCategoryColorNameToStackChartStyles, + getForegroundColor, + getBackgroundColor, +} from '../../utils/colors'; import { formatCallNodeNumberWithUnit, formatPercent, @@ -226,7 +230,7 @@ class FlameGraphCanvasImpl extends React.PureComponent { TEXT_OFFSET_START * cssToDeviceScale ); - fastFillStyle.set('#ffffff'); + fastFillStyle.set(getBackgroundColor()); ctx.fillRect(0, 0, deviceContainerWidth, deviceContainerHeight); const callNodeTable = callNodeInfo.getCallNodeTable(); @@ -304,8 +308,8 @@ class FlameGraphCanvasImpl extends React.PureComponent { ); const background = isHighlighted - ? colorStyles.selectedFillStyle - : colorStyles.unselectedFillStyle; + ? colorStyles.getSelectedFillStyle() + : colorStyles.getUnselectedFillStyle(); fastFillStyle.set(background); ctx.fillRect( @@ -329,8 +333,8 @@ class FlameGraphCanvasImpl extends React.PureComponent { ); if (fittedText) { const foreground = isHighlighted - ? colorStyles.selectedTextColor - : '#000'; + ? colorStyles.getSelectedTextColor() + : getForegroundColor(); fastFillStyle.set(foreground); // TODO - L10N RTL. ctx.fillText(fittedText, deviceTextLeft, deviceTextTop); diff --git a/src/components/flame-graph/FlameGraph.css b/src/components/flame-graph/FlameGraph.css index e0fca42603..afdea0743c 100644 --- a/src/components/flame-graph/FlameGraph.css +++ b/src/components/flame-graph/FlameGraph.css @@ -13,5 +13,5 @@ display: flex; flex: 1; flex-direction: row; - border-top: 1px solid var(--grey-30); + border-top: 1px solid var(--base-border-color); } diff --git a/src/components/flame-graph/MaybeFlameGraph.css b/src/components/flame-graph/MaybeFlameGraph.css index 1641d08ace..fcc1267de5 100644 --- a/src/components/flame-graph/MaybeFlameGraph.css +++ b/src/components/flame-graph/MaybeFlameGraph.css @@ -1,11 +1,19 @@ .flameGraphDisabledMessage { + --internal-background-color: var(--grey-20); + display: flex; flex: 1; flex-direction: column; align-items: center; justify-content: center; padding: 10px; - background-color: var(--grey-20); - box-shadow: inset 0 0 150px rgb(0 0 0 / 0.1); + background-color: var(--internal-background-color); + box-shadow: inset 0 0 150px var(--base-shadow-color); font-size: 120%; } + +:root.dark-mode { + .flameGraphDisabledMessage { + --internal-background-color: var(--grey-80); + } +} diff --git a/src/components/js-tracer/Settings.css b/src/components/js-tracer/Settings.css index 104cb394f6..fb6f943f10 100644 --- a/src/components/js-tracer/Settings.css +++ b/src/components/js-tracer/Settings.css @@ -3,12 +3,14 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ .jsTracerSettings { + --internal-foreground-color: var(--grey-70); + display: flex; height: 25px; flex-flow: row nowrap; padding: 0; - border-bottom: 1px solid var(--grey-30); - color: var(--ink-70); + border-bottom: 1px solid var(--base-border-color); + color: var(--internal-foreground-color); line-height: 25px; } @@ -39,6 +41,8 @@ } .jsTracerSettingsFilter { + --internal-border-color: var(--grey-20); + padding-right: 10px; border-right: 1px solid var(--grey-20); white-space: nowrap; @@ -53,3 +57,13 @@ .jsTracerSettingsFilterInput { margin: 0 4px; } + +:root.dark-mode { + .jsTracerSettings { + --internal-foreground-color: var(--grey-30); + } + + .jsTracerSettingsFilter { + --internal-border-color: var(--grey-80); + } +} diff --git a/src/components/js-tracer/index.css b/src/components/js-tracer/index.css index ba116171de..d385f66e08 100644 --- a/src/components/js-tracer/index.css +++ b/src/components/js-tracer/index.css @@ -9,19 +9,29 @@ } .jsTracerLoader { + --internal-background-color: var(--grey-20); + --internal-border-color: var(--grey-10); + display: flex; flex: 1; flex-flow: column; align-items: center; justify-content: center; - border-top: solid 1px var(--grey-20); - background-color: var(--grey-10); + border-top: solid 1px var(--internal-border-color); + background-color: var(--internal-background-color); font-size: 12px; vertical-align: middle; } +:root.dark-mode { + .jsTracerLoader { + --internal-background-color: var(--grey-80); + --internal-border-color: var(--grey-80); + } +} + .jsTracerCanvasFadeIn { - background-color: #fff; + background-color: var(--base-background-color); opacity: 0; } diff --git a/src/components/marker-chart/Canvas.tsx b/src/components/marker-chart/Canvas.tsx index ec6544ad32..3f49c0d3e6 100644 --- a/src/components/marker-chart/Canvas.tsx +++ b/src/components/marker-chart/Canvas.tsx @@ -1,7 +1,17 @@ /* 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 { GREY_20, GREY_30, BLUE_60, BLUE_80 } from 'photon-colors'; +import { + GREY_20, + GREY_30, + GREY_60, + GREY_90, + BLUE_60, + BLUE_80, + GREY_70, +} from 'photon-colors'; +import { getForegroundColor, getBackgroundColor } from '../../utils/colors'; +import { isDarkMode, lightDark } from '../../utils/dark-mode'; import * as React from 'react'; import { withChartViewport, @@ -49,7 +59,7 @@ import { isValidGraphColor, } from 'firefox-profiler/profile-logic/graph-color'; import { getSchemaFromMarker } from 'firefox-profiler/profile-logic/marker-schema'; -import { getBottomBoxInfoForStackFrame } from 'firefox-profiler/profile-logic/profile-data'; +import { getBottomBoxInfoForStackFrame } from 'firefox-profiler/profile-logic/bottom-box'; import type { ChartCanvasScale, @@ -103,6 +113,37 @@ const LABEL_PADDING = 5; const MARKER_BORDER_COLOR = '#2c77d1'; const DEFAULT_FILL_COLOR = '#8ac4ff'; // Light blue for non-highlighted +function getSeparatorColor() { + return lightDark(GREY_20, GREY_70); +} + +function getBucketBackgroundColor() { + return lightDark(GREY_20, GREY_70); +} + +function getBucketBorderColor() { + return lightDark(GREY_30, GREY_60); +} + +function getHighlightRowBackgroundColor() { + return 'rgba(40, 122, 169, 0.2)'; +} + +function getDefaultMarkerColors(isHighlighted: boolean) { + if (isDarkMode()) { + return { + fillColor: isHighlighted ? 'hsl(208, 81%, 52%)' : 'hsl(208, 88%, 32%)', + strokeColor: isHighlighted ? 'hsl(208, 82%, 58%)' : 'hsl(208, 71%, 40%)', + textColor: isHighlighted ? GREY_90 : GREY_20, + }; + } + return { + fillColor: isHighlighted ? BLUE_60 : DEFAULT_FILL_COLOR, + strokeColor: isHighlighted ? BLUE_80 : MARKER_BORDER_COLOR, + textColor: isHighlighted ? 'white' : 'black', // White text on dark blue, black text on light blue + }; +} + class MarkerChartCanvasImpl extends React.PureComponent { _textMeasurement: TextMeasurement | null = null; @@ -152,15 +193,11 @@ class MarkerChartCanvasImpl extends React.PureComponent { return { fillColor: getFillColor(color), strokeColor: getStrokeColor(color), - textColor: '#000', // Always use black text for unselected markers + textColor: getForegroundColor(), // Always use black/white text for unselected markers }; } // Fall back to default blue colors - return { - fillColor: isHighlighted ? BLUE_60 : DEFAULT_FILL_COLOR, - strokeColor: isHighlighted ? BLUE_80 : MARKER_BORDER_COLOR, - textColor: isHighlighted ? 'white' : 'black', // White text on dark blue, black text on light blue - }; + return getDefaultMarkerColors(isHighlighted); } drawCanvas = ( @@ -235,7 +272,7 @@ class MarkerChartCanvasImpl extends React.PureComponent { this.drawSeparatorsAndLabels(ctx, oldRow, oldRow + 1); } } else { - ctx.fillStyle = '#ffffff'; + ctx.fillStyle = getBackgroundColor(); ctx.fillRect(0, 0, containerWidth, containerHeight); if (rightClickedRow !== undefined) { this.highlightRow(ctx, rightClickedRow); @@ -253,7 +290,7 @@ class MarkerChartCanvasImpl extends React.PureComponent { viewport: { viewportTop, containerWidth }, } = this.props; - ctx.fillStyle = 'rgba(40, 122, 169, 0.2)'; + ctx.fillStyle = getHighlightRowBackgroundColor(); ctx.fillRect( 0, // To include the labels also row * rowHeight - viewportTop, @@ -559,7 +596,7 @@ class MarkerChartCanvasImpl extends React.PureComponent { viewport: { viewportTop, containerWidth }, } = this.props; - ctx.fillStyle = '#fff'; + ctx.fillStyle = getBackgroundColor(); ctx.fillRect( 0, rowIndex * rowHeight - viewportTop, @@ -648,7 +685,7 @@ class MarkerChartCanvasImpl extends React.PureComponent { const usefulContainerWidth = containerWidth - marginRight; // Draw separators - ctx.fillStyle = GREY_20; + ctx.fillStyle = getSeparatorColor(); ctx.fillRect(marginLeft - 1, 0, 1, containerHeight); for (let rowIndex = startRow; rowIndex < endRow; rowIndex++) { // `- 1` at the end, because the top separator is not drawn in the canvas, @@ -660,7 +697,7 @@ class MarkerChartCanvasImpl extends React.PureComponent { const textMeasurement = this._getTextMeasurement(ctx); // Draw the marker names in the left margin. - ctx.fillStyle = '#000000'; + ctx.fillStyle = getForegroundColor(); for (let rowIndex = startRow; rowIndex < endRow; rowIndex++) { const markerTiming = markerTimingAndBuckets[rowIndex]; if (typeof markerTiming === 'string') { @@ -703,16 +740,16 @@ class MarkerChartCanvasImpl extends React.PureComponent { const y = rowIndex * rowHeight - viewportTop; // Draw the backgound. - ctx.fillStyle = GREY_20; + ctx.fillStyle = getBucketBackgroundColor(); ctx.fillRect(0, y - 1, usefulContainerWidth, rowHeight); // Draw the borders./* - ctx.fillStyle = GREY_30; + ctx.fillStyle = getBucketBorderColor(); ctx.fillRect(0, y - 1, usefulContainerWidth, 1); ctx.fillRect(0, y + rowHeight - 1, usefulContainerWidth, 1); // Draw the text. - ctx.fillStyle = '#000000'; + ctx.fillStyle = getForegroundColor(); ctx.fillText(bucketName, LABEL_PADDING + marginLeft, y + TEXT_OFFSET_TOP); } } diff --git a/src/components/marker-chart/index.css b/src/components/marker-chart/index.css index b6c2beabda..66f92babe4 100644 --- a/src/components/marker-chart/index.css +++ b/src/components/marker-chart/index.css @@ -9,5 +9,5 @@ } .markerChartCanvas { - border-top: 1px solid var(--grey-30); + border-top: 1px solid var(--base-border-color); } diff --git a/src/components/marker-table/index.tsx b/src/components/marker-table/index.tsx index 57728e3b64..2ecaad7e11 100644 --- a/src/components/marker-table/index.tsx +++ b/src/components/marker-table/index.tsx @@ -23,6 +23,8 @@ import { } from '../../actions/profile-view'; import { MarkerSettings } from '../shared/MarkerSettings'; import { formatSeconds, formatTimestamp } from '../../utils/format-numbers'; +import { assertExhaustiveCheck } from '../../utils/types'; +import copy from 'copy-to-clipboard'; import './index.css'; @@ -71,6 +73,100 @@ class MarkerTree { this._getMarkerLabel = getMarkerLabel; } + copyTable = ( + format: 'plain' | 'markdown', + onExceeedMaxCopyRows: (rows: number, maxRows: number) => void + ) => { + const lines = []; + + const startLabel = 'Start'; + const durationLabel = 'Duration'; + const nameLabel = 'Name'; + const detailsLabel = 'Details'; + + const header = [startLabel, durationLabel, nameLabel, detailsLabel]; + + let maxStartLength = startLabel.length; + let maxDurationLength = durationLabel.length; + let maxNameLength = nameLabel.length; + + const MAX_COPY_ROWS = 10000; + + let roots = this.getRoots(); + if (roots.length > MAX_COPY_ROWS) { + onExceeedMaxCopyRows(roots.length, MAX_COPY_ROWS); + roots = roots.slice(0, MAX_COPY_ROWS); + } + + for (const index of roots) { + const data = this.getDisplayData(index); + const duration = data.duration ?? ''; + + maxStartLength = Math.max(data.start.length, maxStartLength); + maxDurationLength = Math.max(duration.length, maxDurationLength); + maxNameLength = Math.max(data.name.length, maxNameLength); + + lines.push([ + data.start, + // Use "u" instead, to make the table aligned with fixed-width text. + duration.replace(/μ/g, 'u'), + data.name, + data.details, + ]); + } + + let text = ''; + switch (format) { + case 'plain': { + const formatter = ([start, duration, name, details]: string[]) => { + const line = [ + start.padStart(maxStartLength, ' '), + duration.padStart(maxDurationLength, ' '), + name.padStart(maxNameLength, ' '), + ]; + if (details) { + line.push(details); + } + return line.join(' '); + }; + + text += formatter(header) + '\n' + lines.map(formatter).join('\n'); + break; + } + case 'markdown': { + const formatter = ([start, duration, name, details]: string[]) => { + const line = [ + start.padStart(maxStartLength, ' '), + duration.padStart(maxDurationLength, ' '), + name.padStart(maxNameLength, ' '), + details, + ]; + return '| ' + line.join(' | ') + ' |'; + }; + const sep = + '|' + + [ + '-'.repeat(maxStartLength + 1) + ':', + '-'.repeat(maxDurationLength + 1) + ':', + '-'.repeat(maxNameLength + 1) + ':', + '-'.repeat(9), + ].join('|') + + '|'; + text = + formatter(header) + + '\n' + + sep + + '\n' + + lines.map(formatter).join('\n'); + break; + } + default: + throw assertExhaustiveCheck(format); + } + + copy(text); + }; + getRoots(): MarkerIndex[] { return this._markerIndexes; } @@ -263,7 +359,7 @@ class MarkerTableImpl extends PureComponent { role="tabpanel" aria-labelledby="marker-table-tab-button" > - + {markerIndexes.length === 0 ? ( ) : ( diff --git a/src/components/network-chart/NetworkChartRow.tsx b/src/components/network-chart/NetworkChartRow.tsx index e1bb62e180..6d844477ee 100644 --- a/src/components/network-chart/NetworkChartRow.tsx +++ b/src/components/network-chart/NetworkChartRow.tsx @@ -498,6 +498,9 @@ export class NetworkChartRow extends React.PureComponent< marker={marker} threadsKey={this.props.threadsKey} restrictHeightWidth={true} + // Network Chart doesn't have sticky tooltips yet. But we should convert it + // to false once we implement sticky tooltips for the network chart. + hideFilterButton={true} /> ) : null} diff --git a/src/components/network-chart/index.css b/src/components/network-chart/index.css index 601f93a60e..60991b3aa0 100644 --- a/src/components/network-chart/index.css +++ b/src/components/network-chart/index.css @@ -6,12 +6,13 @@ display: flex; flex: 1; flex-flow: column nowrap; + color: var(--base-foreground-color); cursor: default; user-select: none; } .networkChart .treeViewBody { - border-top: 1px solid var(--grey-30); + border-top: 1px solid var(--base-border-color); } .networkChart .treeViewBody .treeViewBodyInnerWrapper { @@ -20,6 +21,10 @@ } .networkChartRowItem { + --internal-selected-background-color: #bbe0f6; + --internal-item-bar-background-color: var(--grey-20); + --internal-item-uri-optional-foreground-color: var(--grey-40); + position: relative; display: block; width: 100%; @@ -27,14 +32,21 @@ } .networkChartRowItem.odd { - background-color: #f5f5f5; + background-color: var(--row-odd-background-color); } .networkChartRowItem:hover, .networkChartRowItem.isRightClicked, .networkChartRowItem.isSelected, .networkChartRowItem.isHovered { - background-color: #bbe0f6; + background-color: var(--internal-selected-background-color); +} + +:root.dark-mode { + .networkChartRowItem { + --internal-selected-background-color: var(--teal-80); + --internal-item-uri-optional-foreground-color: var(--grey-50); + } } .networkChartRowItemLabel { @@ -60,7 +72,7 @@ margin: 1px 0; /* Because the line's height is 16px, this margin vertically centers the bar */ /* styling properties */ - background-color: var(--grey-20); + background-color: var(--internal-item-bar-background-color); box-shadow: 0 0 0 1px inset var(--marker-color); opacity: 0.7; } @@ -75,7 +87,7 @@ display: inline-block; overflow: hidden; max-width: 90px; - color: var(--grey-40); + color: var(--internal-item-uri-optional-foreground-color); text-overflow: ellipsis; white-space: nowrap; } @@ -84,6 +96,7 @@ display: inline-block; overflow: hidden; max-width: 35%; + color: var(--internal-item-uri-required-foreground-color); text-overflow: ellipsis; white-space: nowrap; } diff --git a/src/components/shared/AssemblyView-codemirror.tsx b/src/components/shared/AssemblyView-codemirror.tsx index b57cb3c850..f0efaa4ec9 100644 --- a/src/components/shared/AssemblyView-codemirror.tsx +++ b/src/components/shared/AssemblyView-codemirror.tsx @@ -19,7 +19,12 @@ * width of the editor, it covers both the gutter and the main area. */ import { EditorView, gutter } from '@codemirror/view'; -import { EditorState, StateField, StateEffect } from '@codemirror/state'; +import { + EditorState, + StateField, + StateEffect, + Compartment, +} from '@codemirror/state'; import { syntaxHighlighting } from '@codemirror/language'; import { classHighlighter } from '@lezer/highlight'; import clamp from 'clamp'; @@ -37,12 +42,16 @@ import { timingsExtension, updateTimingsEffect, StringMarker, + createHighlightedLineExtension, } from 'firefox-profiler/utils/codemirror-shared'; // An "effect" is like a redux action. This effect is used to replace the value // of the state field addressToLineMapField. const updateAddressToLineMapEffect = StateEffect.define(); +// This "compartment" allows us to swap the highlighted line when it changes. +const highlightedLineConf = new Compartment(); + // This "state field" stores the current AddressToLineMap. This field allows the // instructionAddressGutter to map line numbers to addresses. const addressToLineMapField = StateField.define({ @@ -174,23 +183,31 @@ export class AssemblyViewEditor { _view: EditorView; _addressToLineMap: AddressToLineMap; _addressTimings: AddressTimings; + _highlightedAddress: Address | null; // Create a CodeMirror editor and add it as a child element of domParent. constructor( initialAssemblyCode: DecodedInstruction[], addressTimings: AddressTimings, + highlightedAddress: Address | null, domParent: Element ) { this._addressToLineMap = new AddressToLineMap( getInstructionAddresses(initialAssemblyCode) ); this._addressTimings = addressTimings; + this._highlightedAddress = highlightedAddress; + const highlightedLine = + highlightedAddress !== null + ? this._addressToLineMap.addressToLine(highlightedAddress) + : null; let state = EditorState.create({ doc: instructionsToText(initialAssemblyCode), extensions: [ timingsExtension, addressToLineMapField, instructionAddressGutter, + highlightedLineConf.of(createHighlightedLineExtension(highlightedLine)), syntaxHighlighting(classHighlighter), EditorState.readOnly.of(true), EditorView.editable.of(false), @@ -220,6 +237,11 @@ export class AssemblyViewEditor { this._addressTimings, this._addressToLineMap ); + // Recalculate the highlighted line based on the new address-to-line mapping. + const highlightedLine = + this._highlightedAddress !== null + ? this._addressToLineMap.addressToLine(this._highlightedAddress) + : null; // The CodeMirror way of replacing the entire contents is to insert new text // and overwrite the full range of existing text. const text = instructionsToText(assemblyCode); @@ -236,6 +258,9 @@ export class AssemblyViewEditor { effects: [ updateAddressToLineMapEffect.of(this._addressToLineMap), updateTimingsEffect.of(lineTimings), + highlightedLineConf.reconfigure( + createHighlightedLineExtension(highlightedLine) + ), ], }); } @@ -280,4 +305,18 @@ export class AssemblyViewEditor { this.scrollToLine(lineNumber - topSpaceLines); } } + + setHighlightedInstruction(address: Address | null) { + // Store the highlighted address so we can recalculate the line number + // when the address-to-line mapping changes. + this._highlightedAddress = address; + // Convert the address to a line number and update the highlighted line. + const lineNumber = + address !== null ? this._addressToLineMap.addressToLine(address) : null; + this._view.dispatch({ + effects: highlightedLineConf.reconfigure( + createHighlightedLineExtension(lineNumber) + ), + }); + } } diff --git a/src/components/shared/AssemblyView.tsx b/src/components/shared/AssemblyView.tsx index 729699d172..37108a6903 100644 --- a/src/components/shared/AssemblyView.tsx +++ b/src/components/shared/AssemblyView.tsx @@ -9,7 +9,6 @@ import type { NativeSymbolInfo, DecodedInstruction, } from 'firefox-profiler/types'; -import { mapGetKeyWithMaxValue } from 'firefox-profiler/utils'; import type { AssemblyViewEditor } from './AssemblyView-codemirror'; @@ -44,10 +43,10 @@ for understanding where time was actually spent in a program." type AssemblyViewProps = { readonly timings: AddressTimings; readonly assemblyCode: DecodedInstruction[]; - readonly disableOverscan: boolean; readonly nativeSymbol: NativeSymbolInfo | null; - readonly scrollToHotSpotGeneration: number; - readonly hotSpotTimings: AddressTimings; + readonly scrollGeneration: number; + readonly scrollToInstructionAddress?: number; + readonly highlightedInstruction: number | null; }; let editorModulePromise: Promise | null = null; @@ -56,36 +55,6 @@ export class AssemblyView extends React.PureComponent { _ref = React.createRef(); _editor: AssemblyViewEditor | null = null; - /** - * Scroll to the line with the most hits, based on the timings in - * timingsForScrolling. - * - * How is timingsForScrolling different from this.props.timings? - * In the current implementation, this.props.timings are always the "global" - * timings, i.e. they show the line hits for all samples in the current view, - * regardless of the selected call node. However, when opening the assembly - * view from a specific call node, you really want to see the code that's - * relevant to that specific call node, or at least that specific function. - * So timingsForScrolling are the timings that indicate just the line hits - * in the selected call node. This means that the "hotspot" will be somewhere - * in the selected function, and it will even be in the line that's most - * relevant to that specific call node. - * - * Sometimes, timingsForScrolling can be completely empty. This happens, for - * example, when the assembly view is showing a different file than the - * selected call node's function's file, for example because we just loaded - * from a URL and ended up with an arbitrary selected call node. - * In that case, pick the hotspot from the global line timings. - */ - _scrollToHotSpot(timingsForScrolling: AddressTimings) { - const heaviestAddress = - mapGetKeyWithMaxValue(timingsForScrolling.totalAddressHits) ?? - mapGetKeyWithMaxValue(this.props.timings.totalAddressHits); - if (heaviestAddress !== undefined) { - this._scrollToAddressWithSpaceOnTop(heaviestAddress, 5); - } - } - _scrollToAddressWithSpaceOnTop(address: number, topSpaceLines: number) { if (this._editor) { this._editor.scrollToAddressWithSpaceOnTop(address, topSpaceLines); @@ -150,10 +119,16 @@ export class AssemblyView extends React.PureComponent { const editor = new AssemblyViewEditor( this._getAssemblyCodeOrFallback(), this.props.timings, + this.props.highlightedInstruction, domParent ); this._editor = editor; - this._scrollToHotSpot(this.props.hotSpotTimings); + if (this.props.scrollToInstructionAddress !== undefined) { + this._scrollToAddressWithSpaceOnTop( + this.props.scrollToInstructionAddress, + 5 + ); + } })(); } @@ -177,14 +152,24 @@ export class AssemblyView extends React.PureComponent { if ( contentsChanged || - this.props.scrollToHotSpotGeneration !== - prevProps.scrollToHotSpotGeneration + this.props.scrollGeneration !== prevProps.scrollGeneration ) { - this._scrollToHotSpot(this.props.hotSpotTimings); + if (this.props.scrollToInstructionAddress !== undefined) { + this._scrollToAddressWithSpaceOnTop( + this.props.scrollToInstructionAddress, + 5 + ); + } } if (this.props.timings !== prevProps.timings) { this._editor.setTimings(this.props.timings); } + + if ( + this.props.highlightedInstruction !== prevProps.highlightedInstruction + ) { + this._editor.setHighlightedInstruction(this.props.highlightedInstruction); + } } } diff --git a/src/components/shared/Backtrace.css b/src/components/shared/Backtrace.css index 2cb6cf9b76..f7f812fefb 100644 --- a/src/components/shared/Backtrace.css +++ b/src/components/shared/Backtrace.css @@ -1,4 +1,8 @@ .backtrace { + --internal-origin-foreground-color: rgb(0 0 0 / 0.55); + --internal-frame-label-foreground-color: rgb(0 0 0 / 0.6); + --internal-link-hover-background-color: rgb(0 0 0 / 0.05); + padding: 0; margin: 0; } @@ -7,6 +11,14 @@ overflow: hidden; } +:root.dark-mode { + .backtrace { + --internal-origin-foreground-color: rgb(237 237 2400 / 0.55); + --internal-frame-label-foreground-color: rgb(237 237 2400 / 0.6); + --internal-link-hover-background-color: rgb(237 237 2400 / 0.05); + } +} + .backtraceStackFrame { /* Position */ overflow: hidden; @@ -23,20 +35,20 @@ .backtraceStackFrameOrigin { margin-left: 10px; - color: rgb(0 0 0 / 0.55); + color: var(--internal-origin-foreground-color); font-style: normal; } .backtraceStackFrame_isFrameLabel { - color: rgb(0 0 0 / 0.6); + color: var(--internal-frame-label-foreground-color); } .backtraceStackFrame_link { display: block; - color: inherit; + color: inherit !important; text-decoration: none; } .backtraceStackFrame_link:hover { - background-color: rgb(0 0 0 / 0.05); + background-color: var(--internal-link-hover-background-color); } diff --git a/src/components/shared/ButtonWithPanel/ArrowPanel.css b/src/components/shared/ButtonWithPanel/ArrowPanel.css index d7d6e8abdf..afae8912e0 100644 --- a/src/components/shared/ButtonWithPanel/ArrowPanel.css +++ b/src/components/shared/ButtonWithPanel/ArrowPanel.css @@ -14,17 +14,20 @@ --internal-offset-from-top: 15px; --internal-width: var(--width, initial); --internal-button-height: 30px; + --internal-background-color: hsl(0deg 0% 97%); + --internal-border-color: rgb(0 0 0 / 0.25); + --internal-shadow-color: rgb(0 0 0 / 0.35); position: absolute; top: var(--internal-offset-from-top); right: calc(var(--internal-offset-from-right) * -1); min-width: var(--internal-width); - border: 0.5px solid rgb(0 0 0 / 0.25); + border: 0.5px solid var(--internal-border-color); border-radius: 5px; - background: hsl(0deg 0% 97%); + background: var(--internal-background-color); background-clip: padding-box; - box-shadow: 0 8px 12px rgb(0 0 0 / 0.35); - color: black; + box-shadow: 0 8px 12px var(--internal-shadow-color); + color: var(--base-foreground-color); line-height: 1.3; text-align: left; transform-origin: calc(100% - var(--internal-offset-from-right)) @@ -44,6 +47,14 @@ visibility: hidden; } +:root.dark-mode { + .arrowPanel { + --internal-background-color: var(--menu-background-color); + --internal-border-color: var(--grey-60); + --internal-shadow-color: rgb(0 0 0 / 0.4); + } +} + .arrowPanel.open { animation: arrowPanelAppear 200ms cubic-bezier(0.07, 0.95, 0, 1); } @@ -83,8 +94,8 @@ display: block; width: calc(1.42 * var(--internal-offset-from-top)); height: calc(1.42 * var(--internal-offset-from-top)); - border: 0.5px solid rgb(0 0 0 / 0.25); - background: hsl(0deg 0% 97%); + border: 0.5px solid var(--internal-border-color); + background: var(--internal-background-color); background-clip: padding-box; content: ''; transform: rotate(45deg); diff --git a/src/components/shared/CallNodeContextMenu.css b/src/components/shared/CallNodeContextMenu.css index fd94da6e7d..28f67e061c 100644 --- a/src/components/shared/CallNodeContextMenu.css +++ b/src/components/shared/CallNodeContextMenu.css @@ -27,24 +27,43 @@ background-image: url(../../../res/img/svg/focus-icon.svg); } +.callNodeContextMenuIconFocusSelf { + background-image: url(../../../res/img/svg/focus-self-icon.svg); +} + .callNodeContextMenuIconDrop { background-image: url(../../../res/img/svg/drop-icon.svg); } .callNodeContextMenuShortcut { - padding: 0 5px; - border-radius: 3px; + --internal-foreground-color: #000; /* This color is based off of photon grey, but adjusted to have a nice visual look when hovering. */ - background: #dedee3c9; - box-shadow: 1px 1px #0004; + --internal-background-color: #dedee3c9; + --internal-shadow-color: #0004; + + padding: 0 5px; + border-radius: 3px; + background: var(--internal-background-color); + box-shadow: 1px 1px var(--internal-shadow-color); /* Override the hover color. */ - color: #000; + color: var(--internal-foreground-color); margin-inline-start: 12px; } +:root.dark-mode { + .callNodeContextMenu .react-contextmenu-icon { + filter: invert(1); + } + + .callNodeContextMenuShortcut { + --internal-background-color: #d7d7dbc0; + --internal-shadow-color: #b1b1b380; + } +} + @media (forced-colors: active) { .callNodeContextMenuShortcut { background: SelectedItemText; diff --git a/src/components/shared/CallNodeContextMenu.tsx b/src/components/shared/CallNodeContextMenu.tsx index 7e2633f6cf..1a360f01c6 100644 --- a/src/components/shared/CallNodeContextMenu.tsx +++ b/src/components/shared/CallNodeContextMenu.tsx @@ -14,10 +14,7 @@ import { funcHasRecursiveCall, } from 'firefox-profiler/profile-logic/transforms'; import { getFunctionName } from 'firefox-profiler/profile-logic/function-info'; -import { - getBottomBoxInfoForCallNode, - getOriginAnnotationForFunc, -} from 'firefox-profiler/profile-logic/profile-data'; +import { getOriginAnnotationForFunc } from 'firefox-profiler/profile-logic/profile-data'; import { getCategories } from 'firefox-profiler/selectors'; import copy from 'copy-to-clipboard'; @@ -52,12 +49,12 @@ import type { TransformType, ImplementationFilter, IndexIntoCallNodeTable, - CallNodePath, Thread, ThreadsKey, CategoryList, InnerWindowID, Page, + SamplesLikeTable, } from 'firefox-profiler/types'; import type { TabSlug } from 'firefox-profiler/app-logic/tabs-handling'; @@ -68,9 +65,9 @@ import type { BrowserConnectionStatus } from 'firefox-profiler/app-logic/browser type StateProps = { readonly thread: Thread | null; readonly threadsKey: ThreadsKey | null; + readonly previewFilteredCtssSamples: SamplesLikeTable | null; readonly categories: CategoryList; readonly callNodeInfo: CallNodeInfo | null; - readonly rightClickedCallNodePath: CallNodePath | null; readonly rightClickedCallNodeIndex: IndexIntoCallNodeTable | null; readonly implementation: ImplementationFilter; readonly inverted: boolean; @@ -91,6 +88,7 @@ type DispatchProps = { type Props = ConnectedProps<{}, StateProps, DispatchProps>; import './CallNodeContextMenu.css'; +import { getBottomBoxInfoForCallNode } from 'firefox-profiler/profile-logic/bottom-box'; class CallNodeContextMenuImpl extends React.PureComponent { _hidingTimeout: NodeJS.Timeout | null = null; @@ -226,11 +224,13 @@ class CallNodeContextMenuImpl extends React.PureComponent { ); } - const { callNodeIndex, thread, callNodeInfo } = rightClickedCallNodeInfo; + const { callNodeIndex, thread, callNodeInfo, previewFilteredCtssSamples } = + rightClickedCallNodeInfo; const bottomBoxInfo = getBottomBoxInfoForCallNode( callNodeIndex, callNodeInfo, - thread + thread, + previewFilteredCtssSamples ); updateBottomBoxContentsAndMaybeOpen(selectedTab, bottomBoxInfo); } @@ -337,9 +337,10 @@ class CallNodeContextMenuImpl extends React.PureComponent { ); } - const { threadsKey, callNodePath, thread, callNodeIndex, callNodeInfo } = + const { threadsKey, thread, callNodeIndex, callNodeInfo } = rightClickedCallNodeInfo; - const selectedFunc = callNodePath[callNodePath.length - 1]; + const selectedFunc = callNodeInfo.funcForNode(callNodeIndex); + const callNodePath = callNodeInfo.getCallNodePathFromIndex(callNodeIndex); const category = callNodeInfo.categoryForNode(callNodeIndex); switch (type) { case 'focus-subtree': @@ -356,6 +357,13 @@ class CallNodeContextMenuImpl extends React.PureComponent { funcIndex: selectedFunc, }); break; + case 'focus-self': + addTransformToStack(threadsKey, { + type: 'focus-self', + funcIndex: selectedFunc, + implementation, + }); + break; case 'merge-call-node': addTransformToStack(threadsKey, { type: 'merge-call-node', @@ -503,11 +511,12 @@ class CallNodeContextMenuImpl extends React.PureComponent { } const { - callNodePath, + callNodeInfo, + callNodeIndex, thread: { funcTable, stringTable, resourceTable, sources }, } = rightClickedCallNodeInfo; - const funcIndex = callNodePath[callNodePath.length - 1]; + const funcIndex = callNodeInfo.funcForNode(callNodeIndex); if (funcIndex === undefined) { return null; } @@ -533,30 +542,30 @@ class CallNodeContextMenuImpl extends React.PureComponent { getRightClickedCallNodeInfo(): null | { readonly thread: Thread; readonly threadsKey: ThreadsKey; + readonly previewFilteredCtssSamples: SamplesLikeTable; readonly callNodeInfo: CallNodeInfo; - readonly callNodePath: CallNodePath; readonly callNodeIndex: IndexIntoCallNodeTable; } { const { thread, threadsKey, + previewFilteredCtssSamples, callNodeInfo, - rightClickedCallNodePath, rightClickedCallNodeIndex, } = this.props; if ( thread && threadsKey !== null && + previewFilteredCtssSamples !== null && callNodeInfo && - rightClickedCallNodePath && - typeof rightClickedCallNodeIndex === 'number' + rightClickedCallNodeIndex !== null ) { return { thread, threadsKey, + previewFilteredCtssSamples, callNodeInfo, - callNodePath: rightClickedCallNodePath, callNodeIndex: rightClickedCallNodeIndex, }; } @@ -679,6 +688,16 @@ class CallNodeContextMenuImpl extends React.PureComponent { content: 'Focus on subtree only', })} + {this.renderTransformMenuItem({ + l10nId: 'CallNodeContextMenu--transform-focus-self', + shortcut: 'S', + icon: 'FocusSelf', + onClick: this._handleClick, + transform: 'focus-self', + title: '', + content: 'Focus on self only', + })} + {hasCategory ? this.renderTransformMenuItem({ l10nId: 'CallNodeContextMenu--transform-focus-category', @@ -897,8 +916,8 @@ export const CallNodeContextMenu = explicitConnect< let thread = null; let threadsKey = null; + let previewFilteredCtssSamples = null; let callNodeInfo = null; - let rightClickedCallNodePath = null; let rightClickedCallNodeIndex = null; if (rightClickedCallNodeInfo !== null) { @@ -908,17 +927,18 @@ export const CallNodeContextMenu = explicitConnect< thread = selectors.getFilteredThread(state); threadsKey = rightClickedCallNodeInfo.threadsKey; + previewFilteredCtssSamples = + selectors.getPreviewFilteredCtssSamples(state); callNodeInfo = selectors.getCallNodeInfo(state); - rightClickedCallNodePath = rightClickedCallNodeInfo.callNodePath; rightClickedCallNodeIndex = selectors.getRightClickedCallNodeIndex(state); } return { thread, threadsKey, + previewFilteredCtssSamples, categories: getCategories(state), callNodeInfo, - rightClickedCallNodePath, rightClickedCallNodeIndex, implementation: getImplementationFilter(state), inverted: getInvertCallstack(state), diff --git a/src/components/shared/CodeView.css b/src/components/shared/CodeView.css index 0798eb14ad..c10ece4461 100644 --- a/src/components/shared/CodeView.css +++ b/src/components/shared/CodeView.css @@ -3,11 +3,24 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ .codeView { + --codeview-foreground-color: #000; + --codeview-background-color: white; + --codeview-header-background-color: white; + --codeview-header-border-color: var(--grey-30); + --codeview-header-sep-color: #e5e5e5; + --codeview-gutter-foreground-color: black; + --codeview-gutter-background-color: white; + --codeview-gutter-border-color: var(--grey-30); + --codeview-linenum-foreground-color: ##aaa; + --codeview-nonzero-background-color: #edf6ff; + --codeview-highlight-background-color: #fffbcc; + display: flex; min-height: 0; flex: 1; flex-flow: column; - background: white; + background: var(--codeview-background-color); + color: var(--codeview-foreground-color); /* Extra intermediate colors which Photon doesn't have, from https://searchfox.org/mozilla-central/rev/8012bca692dddb307c2787bac1d4dd48cb0d8243/devtools/client/themes/variables.css#34-100 */ @@ -24,13 +37,36 @@ --theme-highlight-punctuation: var(--grey-70); } +:root.dark-mode { + .codeView { + --codeview-foreground-color: var(--grey-20); + --codeview-background-color: var(--grey-90); + --codeview-header-background-color: var(--grey-90); + --codeview-header-border-color: var(--grey-50); + --codeview-header-sep-color: var(--grey-50); + --codeview-gutter-foreground-color: var(--grey-30); + --codeview-gutter-background-color: var(--grey-90); + --codeview-gutter-border-color: var(--grey-50); + --codeview-linenum-foreground-color: var(--grey-30); + --codeview-nonzero-background-color: var(--grey-80); + --codeview-highlight-background-color: var(--yellow-90); + --theme-highlight-keyword: var(--magenta-65); + --theme-highlight-type: var(--purple-40); + --theme-highlight-variable: var(--blue-40); + --theme-highlight-method: var(--green-60); + --theme-highlight-literal: var(--teal-60); + --theme-highlight-comment: var(--grey-40); + --theme-highlight-punctuation: var(--grey-50); + } +} + .codeViewHeader { display: flex; height: 16px; flex-flow: row; padding: 1px 0; - border-bottom: 1px solid var(--grey-30); - background: white; + border-bottom: 1px solid var(--codeview-header-border-color); + background: var(--codeview-header-background-color); } .codeMirrorContainer { @@ -52,7 +88,7 @@ .cm-gutters { border-right: 0 !important; - background-color: white !important; + background-color: var(--codeview-gutter-background-color) !important; } .codeViewHeaderMainColumn { @@ -74,7 +110,7 @@ right: 0; bottom: 3px; width: 1px; - background: #e5e5e5; + background: var(--codeview-header-sep-color); content: ''; } @@ -88,12 +124,12 @@ .cm-total-timings-gutter, .cm-self-timings-gutter { - border-right: 1px solid var(--grey-30); - color: black; + border-right: 1px solid var(--codeview-gutter-border-color); + color: var(--codeview-gutter-foreground-color); } .cm-lineNumbers { - color: #aaa; + color: var(--codeview-linenum-foreground-color); font-variant-numeric: tabular-nums; } @@ -102,7 +138,7 @@ } .cm-instruction-address-gutter { - color: #aaa; + color: var(--codeview-linenum-foreground-color); font-family: ui-monospace, 'Roboto Mono', monospace; font-variant-numeric: tabular-nums; } @@ -122,11 +158,11 @@ } .cm-nonZeroLine { - background-color: #edf6ff; + background-color: var(--codeview-nonzero-background-color); } .cm-highlightedLine { - background-color: #fffbcc; + background-color: var(--codeview-highlight-background-color); } .cm-content { diff --git a/src/components/shared/ContextMenu.css b/src/components/shared/ContextMenu.css index 710baa2fbe..1a1a95d00e 100644 --- a/src/components/shared/ContextMenu.css +++ b/src/components/shared/ContextMenu.css @@ -3,25 +3,51 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ .react-contextmenu { + --internal-shadow-color1: rgb(0 0 0 / 0.1); + --internal-shadow-color2: rgb(0 0 0 / 0.3); + --internal-separator-color: #ddd; + --internal-divider-color: rgb(0 0 0 / 0.15); + --internal-active-foreground-color: #fff; + --internal-active-background-color: var(--blue-60); + --internal-disabled-foreground-color: #888; + --internal-selected-disabled-background-color: var(--blue-50-a30); + --internal-highlight-foreground-color: var(--grey-70); + z-index: 4; /* needs to be on a higher level than .overflowEdgeIndicatorEdge */ display: none; min-width: 160px; max-width: 600px; padding: 5px 0; - border-radius: 3px; + border-radius: 5px; margin: 2px 0 0; - background-color: #fff; + background-color: var(--menu-background-color); box-shadow: - 0 0 0 0.5px rgb(0 0 0 / 0.1), - 0 10px 12px rgb(0 0 0 / 0.3); - color: #000; + 0 0 0 0.5px var(--internal-shadow-color1), + 0 10px 12px var(--internal-shadow-color2); + color: var(--menu-foreground-color); font-size: 12px; text-align: left; user-select: none; } +:root.dark-mode { + .react-contextmenu { + --internal-shadow-color1: rgb(0 0 0 / 0.4); + --internal-shadow-color2: rgb(0 0 0 / 0.6); + --internal-separator-color: var(--base-border-color); + --internal-divider-color: rgb(237 237 240 / 0.15); + --internal-active-foreground-color: var(--grey-20); + --internal-active-background-color: var(--blue-70); + --internal-disabled-foreground-color: var(--grey-50); + --internal-selected-disabled-background-color: var(--blue-60-a30); + --internal-highlight-foreground-color: var(--grey-30); + + border: 1px solid var(--grey-60); + } +} + .react-contextmenu-separator { - border-bottom: 1px solid #ddd; + border-bottom: 1px solid var(--internal-separator-color); margin: 6px 0; } @@ -44,21 +70,21 @@ .react-contextmenu-item.react-contextmenu-item--active, .react-contextmenu-item--selected { - border-color: var(--blue-60); - background-color: var(--blue-60); - color: #fff; + border-color: var(--internal-active-background-color); + background-color: var(--internal-active-background-color); + color: var(--internal-active-foreground-color); text-decoration: none; } .react-contextmenu-item.react-contextmenu-item--disabled, .react-contextmenu-item.react-contextmenu-item--disabled:hover { background-color: transparent; - color: #888; + color: var(--internal-disabled-foreground-color); } .react-contextmenu-item--divider { padding: 2px 0; - border-bottom: 1px solid rgb(0 0 0 / 0.15); + border-bottom: 1px solid var(--internal-divider-color); margin-bottom: 3px; } @@ -71,7 +97,7 @@ width: 0; height: 0; border: 6px solid transparent; - border-left-color: #000; + border-left-color: var(--base-foreground-color); content: ''; } @@ -99,7 +125,7 @@ display: block; width: 3px; height: 6px; - border: solid #000; + border: solid var(--base-foreground-color); border-width: 0 2px 2px 0; content: ''; transform: rotate(45deg); @@ -107,18 +133,18 @@ .react-contextmenu-item.react-contextmenu-item--active.checked, .react-contextmenu-item--selected.checked { - border-color: #fff; + border-color: var(--internal-active-foreground-color); } .react-contextmenu-item--disabled.react-contextmenu-item--selected { - background-color: var(--blue-50-a30); + background-color: var(--internal-selected-disabled-background-color); } .react-contextmenu-item--selected.checked:not( .react-contextmenu-item--disabled )::before { /* Invert the colors of the checkmark when selected */ - border-color: #fff; + border-color: var(--internal-active-foreground-color); } /* Use this wrapper for a content that needs to take the available space or if @@ -142,11 +168,11 @@ /* Soften a bit the style of highlighted words */ .react-contextmenu-item > strong { - color: #38383d; + color: var(--internal-highlight-foreground-color); } .react-contextmenu-item--selected > strong { - color: #fff; + color: var(--internal-active-foreground-color); } @media (forced-colors: active) { diff --git a/src/components/shared/EmptyReasons.css b/src/components/shared/EmptyReasons.css index 9768a125a0..95dcb96705 100644 --- a/src/components/shared/EmptyReasons.css +++ b/src/components/shared/EmptyReasons.css @@ -1,11 +1,19 @@ .EmptyReasons { + --internal-background-color: var(--grey-20); + display: flex; flex: 1; flex-direction: column; align-items: center; justify-content: center; padding: 10px; - background-color: var(--grey-20); - box-shadow: inset 0 0 15px rgb(0 0 0 / 0.1); + background-color: var(--internal-background-color); + box-shadow: inset 0 0 15px var(--base-shadow-color); font-size: 120%; } + +:root.dark-mode { + .EmptyReasons { + --internal-background-color: var(--grey-80); + } +} diff --git a/src/components/shared/FilterNavigatorBar.css b/src/components/shared/FilterNavigatorBar.css index 2a41257b5c..0f32a92fa8 100644 --- a/src/components/shared/FilterNavigatorBar.css +++ b/src/components/shared/FilterNavigatorBar.css @@ -25,6 +25,16 @@ we have removed this animation in the meantime */ } +:root.dark-mode { + .filterNavigatorBar { + --internal-selected-color: var(--selected-color, var(--blue-40)); + --internal-separator-img: url(../../../res/img/svg/scope-bar-separator-light.svg); + --internal-hover-background-color: rgb(255 255 255 / 0.1); + --internal-hover-color: var(--base-foreground-color); + --internal-active-background-color: rgb(255 255 255 / 0.2); + } +} + .filterNavigatorBarItem { position: relative; display: flex; @@ -103,14 +113,6 @@ background-size: 24px 24px; } -.filterNavigatorBarItem:not( - .filterNavigatorBarRootItem, - .filterNavigatorBarLeafItem - )::before, -.filterNavigatorBarItem:not(.filterNavigatorBarLeafItem)::after { - border-color: var(--internal-background-color); -} - .filterNavigatorBarItem.filterNavigatorBarSelectedItem { background-color: var(--internal-selected-background-color); color: var(--internal-selected-color); @@ -122,7 +124,9 @@ } .filterNavigatorBarItem:not(.filterNavigatorBarLeafItem):hover, -.filterNavigatorBarItem:has(button.profileFilterNavigator--tab-selector):hover { +.filterNavigatorBarItem:has( + .profileFilterNavigator--tab-selector.button + ):hover { background-color: var(--internal-hover-background-color); color: var(--internal-hover-color); } @@ -136,7 +140,7 @@ } .filterNavigatorBarItem:not(.filterNavigatorBarLeafItem):active:hover { - background-color: var(--internal-hover-background-color); + background-color: var(--internal-active-background-color); } .filterNavigatorBarItem:not( @@ -144,7 +148,7 @@ .filterNavigatorBarLeafItem ):active:hover::before, .filterNavigatorBarItem:not(.filterNavigatorBarLeafItem):active:hover::after { - border-color: var(--internal-hover-background-color); + border-color: var(--internal-active-background-color); } .filterNavigatorBarUncommittedItem { @@ -153,13 +157,13 @@ @media (forced-colors: active) { .filterNavigatorBar { - --internal-background-color: ButtonFace; - --internal-hover-background-color: SelectedItemText; - --internal-hover-color: SelectedItem; - --internal-active-background-color: SelectedItemText; - --internal-selected-background-color: SelectedItem; - --internal-selected-color: SelectedItemText; - --internal-separator-img: url(../../../res/img/svg/scope-bar-separator-hcm-light.svg); + --internal-background-color: ButtonFace !important; + --internal-hover-background-color: SelectedItemText !important; + --internal-hover-color: SelectedItem !important; + --internal-active-background-color: SelectedItemText !important; + --internal-selected-background-color: SelectedItem !important; + --internal-selected-color: SelectedItemText !important; + --internal-separator-img: url(../../../res/img/svg/scope-bar-separator-hcm-light.svg) !important; } .filterNavigatorBarItem { @@ -168,7 +172,7 @@ /* When the tab selector is active, we want the item to look like a button */ .filterNavigatorBarSelectedItem:has( - button.profileFilterNavigator--tab-selector + .profileFilterNavigator--tab-selector.button ) { background-color: ButtonFace; color: ButtonText; diff --git a/src/components/shared/FilterNavigatorBar.tsx b/src/components/shared/FilterNavigatorBar.tsx index 12011ef64d..d539b820f0 100644 --- a/src/components/shared/FilterNavigatorBar.tsx +++ b/src/components/shared/FilterNavigatorBar.tsx @@ -7,7 +7,9 @@ import classNames from 'classnames'; import './FilterNavigatorBar.css'; type FilterNavigatorBarListItemProps = { - readonly onClick?: null | ((index: number) => unknown); + readonly onClick?: + | null + | ((index: number, event: React.MouseEvent) => unknown); readonly index: number; readonly isFirstItem: boolean; readonly isLastItem: boolean; @@ -18,10 +20,10 @@ type FilterNavigatorBarListItemProps = { }; class FilterNavigatorBarListItem extends React.PureComponent { - _onClick = () => { + _onClick = (event: React.MouseEvent) => { const { index, onClick } = this.props; if (onClick) { - onClick(index); + onClick(index, event); } }; @@ -61,29 +63,58 @@ type Props = { readonly className: string; readonly items: ReadonlyArray; readonly onPop: (param: number) => void; + readonly onFirstItemClick?: (event: React.MouseEvent) => void; readonly selectedItem: number; readonly uncommittedItem?: string; }; export class FilterNavigatorBar extends React.PureComponent { + _onPop = (index: number, _event: React.MouseEvent) => { + const { onPop } = this.props; + onPop(index); + }; + + _onFirstItemClick = (_: number, event: React.MouseEvent) => { + const { onFirstItemClick } = this.props; + if (onFirstItemClick) { + onFirstItemClick(event); + } + }; + override render() { - const { className, items, selectedItem, uncommittedItem, onPop } = - this.props; + const { + className, + items, + selectedItem, + uncommittedItem, + onFirstItemClick, + } = this.props; return (
    - {items.map((item, i) => ( - - {item} - - ))} + {items.map((item, i) => { + let onClick = null; + if (i === 0 && !uncommittedItem && onFirstItemClick) { + onClick = this._onFirstItemClick; + } else if (i === items.length - 1 && !uncommittedItem) { + onClick = null; + } else { + onClick = this._onPop; + } + + return ( + + {item} + + ); + })} {uncommittedItem ? ( void; + readonly onHide: () => void; + readonly onCopy: (format: 'plain' | 'markdown') => void; +}; + +type Props = ConnectedProps; + +class MarkerCopyTableContextMenuImpl extends PureComponent { + copyAsPlain = () => { + const { onCopy } = this.props; + onCopy('plain'); + }; + + copyAsMarkdown = () => { + const { onCopy } = this.props; + onCopy('markdown'); + }; + + override render() { + const { onShow, onHide } = this.props; + return ( + + + + Copy marker table as plain text + + + + + Copy marker table as Markdown + + + + ); + } +} + +export const MarkerCopyTableContextMenu = explicitConnect({ + component: MarkerCopyTableContextMenuImpl, +}); diff --git a/src/components/shared/MarkerSettings.css b/src/components/shared/MarkerSettings.css index 84399301b8..2d189f96ea 100644 --- a/src/components/shared/MarkerSettings.css +++ b/src/components/shared/MarkerSettings.css @@ -3,30 +3,52 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ .markerSettings { + --internal-filter-icon: url(../../../res/img/svg/filter.svg); + --internal-copy-icon: url(../../../res/img/svg/copy-dark.svg); + display: flex; overflow: auto hidden; height: 25px; flex-flow: row nowrap; padding: 0; + background-color: var(--base-background-color); + color: var(--base-foreground-color); line-height: 25px; scrollbar-width: none; white-space: nowrap; } -.filterMarkersButton { +:root.dark-mode { + .markerSettings { + --internal-filter-icon: url(../../../res/img/svg/filter-light.svg); + --internal-copy-icon: url(../../../res/img/svg/copy-light.svg); + } +} + +.filterMarkersButton, +.copyTableButton { position: relative; width: 24px; height: 24px; flex: none; padding-right: 30px; margin: 0 4px; - background-image: url(../../../res/img/svg/filter.svg); background-position: 4px center; background-repeat: no-repeat; } +.filterMarkersButton { + background-image: var(--internal-filter-icon); +} + +.copyTableButton { + margin-right: 16px; + background-image: var(--internal-copy-icon); +} + /* This is the dropdown arrow on the right of the button. */ -.filterMarkersButton::after { +.filterMarkersButton::after, +.copyTableButton::after { position: absolute; top: 2px; right: 2px; @@ -37,6 +59,26 @@ margin-top: 7px; margin-right: 4px; margin-left: 4px; - color: var(--grey-90); + color: var(--clickable-foreground-color); content: ''; } + +.copyTableButtonWarningWrapper { + /* Position */ + position: fixed; + z-index: 4; + top: 0; + right: 0; + left: 0; + + /* Box */ + display: flex; + + /* Other */ + pointer-events: none; +} + +.copyTableButtonWarning { + margin: 0 auto; + pointer-events: auto; +} diff --git a/src/components/shared/MarkerSettings.tsx b/src/components/shared/MarkerSettings.tsx index d7f8383e2e..f5ccb84567 100644 --- a/src/components/shared/MarkerSettings.tsx +++ b/src/components/shared/MarkerSettings.tsx @@ -14,12 +14,20 @@ import { getProfileUsesMultipleStackTypes } from 'firefox-profiler/selectors/pro import { PanelSearch } from './PanelSearch'; import { StackImplementationSetting } from 'firefox-profiler/components/shared/StackImplementationSetting'; import { MarkerFiltersContextMenu } from './MarkerFiltersContextMenu'; +import { MarkerCopyTableContextMenu } from './MarkerCopyTableContextMenu'; import type { ConnectedProps } from 'firefox-profiler/utils/connect'; import 'firefox-profiler/components/shared/PanelSettingsList.css'; import './MarkerSettings.css'; +type OwnProps = { + readonly copyTable?: ( + format: 'plain' | 'markdown', + onExceeedMaxCopyRows: (rows: number, maxRows: number) => void + ) => void; +}; + type StateProps = { readonly searchString: string; readonly allowSwitchingStackType: boolean; @@ -29,7 +37,7 @@ type DispatchProps = { readonly changeMarkersSearchString: typeof changeMarkersSearchString; }; -type Props = ConnectedProps<{}, StateProps, DispatchProps>; +type Props = ConnectedProps; type State = { readonly isMarkerFiltersMenuVisible: boolean; @@ -39,12 +47,22 @@ type State = { // Otherwise, if we check this in onClick event, the state will always be // `false` since the library already hid it on mousedown. readonly isFilterMenuVisibleOnMouseDown: boolean; + readonly isMarkerCopyTableMenuVisible: boolean; + readonly isCopyTableMenuVisibleOnMouseDown: boolean; + readonly copyTableWarningRows: number | null; + readonly copyTableWarningMaxRows: number | null; }; class MarkerSettingsImpl extends PureComponent { + _copyTableWarningTimeout: NodeJS.Timeout | null = null; + override state = { isMarkerFiltersMenuVisible: false, isFilterMenuVisibleOnMouseDown: false, + isMarkerCopyTableMenuVisible: false, + isCopyTableMenuVisibleOnMouseDown: false, + copyTableWarningRows: null, + copyTableWarningMaxRows: null, }; _onSearch = (value: string) => { @@ -72,6 +90,50 @@ class MarkerSettingsImpl extends PureComponent { }); }; + _onClickToggleCopyTableMenu = (event: React.MouseEvent) => { + const { isCopyTableMenuVisibleOnMouseDown } = this.state; + if (isCopyTableMenuVisibleOnMouseDown) { + // Do nothing as we would like to hide the menu if the menu was already visible on mouse down. + return; + } + + const rect = event.currentTarget.getBoundingClientRect(); + // FIXME: Currently we assume that the context menu is 250px wide, but ideally + // we should get the real width. It's not so easy though, because the context + // menu is not rendered yet. + const isRightAligned = rect.right > window.innerWidth - 250; + + showMenu({ + data: null, + id: 'MarkerCopyTableContextMenu', + position: { x: isRightAligned ? rect.right : rect.left, y: rect.bottom }, + target: event.target, + }); + }; + + _onCopyTable = (format: 'plain' | 'markdown') => { + const { copyTable } = this.props; + if (!copyTable) { + return; + } + + copyTable(format, (rows: number, maxRows: number) => { + this.setState({ + copyTableWarningRows: rows, + copyTableWarningMaxRows: maxRows, + }); + if (this._copyTableWarningTimeout) { + clearTimeout(this._copyTableWarningTimeout); + } + this._copyTableWarningTimeout = setTimeout(() => { + this.setState({ + copyTableWarningRows: null, + copyTableWarningMaxRows: null, + }); + }, 3000); + }); + }; + _onShowFiltersContextMenu = () => { this.setState({ isMarkerFiltersMenuVisible: true }); }; @@ -80,15 +142,34 @@ class MarkerSettingsImpl extends PureComponent { this.setState({ isMarkerFiltersMenuVisible: false }); }; + _onShowCopyTableContextMenu = () => { + this.setState({ isMarkerCopyTableMenuVisible: true }); + }; + + _onHideCopyTableContextMenu = () => { + this.setState({ isMarkerCopyTableMenuVisible: false }); + }; + _onMouseDownToggleFilterButton = () => { this.setState((state) => ({ isFilterMenuVisibleOnMouseDown: state.isMarkerFiltersMenuVisible, })); }; + _onMouseDownToggleCopyTableMenu = () => { + this.setState((state) => ({ + isCopyTableMenuVisibleOnMouseDown: state.isMarkerCopyTableMenuVisible, + })); + }; + override render() { - const { searchString, allowSwitchingStackType } = this.props; - const { isMarkerFiltersMenuVisible } = this.state; + const { searchString, allowSwitchingStackType, copyTable } = this.props; + const { + isMarkerFiltersMenuVisible, + isMarkerCopyTableMenuVisible, + copyTableWarningRows, + copyTableWarningMaxRows, + } = this.state; return (
    @@ -99,6 +180,43 @@ class MarkerSettingsImpl extends PureComponent { ) : null} + {copyTable ? ( + +
    ); } } -export const MarkerSettings = explicitConnect<{}, StateProps, DispatchProps>({ +export const MarkerSettings = explicitConnect< + OwnProps, + StateProps, + DispatchProps +>({ mapStateToProps: (state) => ({ searchString: getMarkersSearchString(state), allowSwitchingStackType: getProfileUsesMultipleStackTypes(state), diff --git a/src/components/shared/NetworkSettings.css b/src/components/shared/NetworkSettings.css index 70f3aeac83..583f00b876 100644 --- a/src/components/shared/NetworkSettings.css +++ b/src/components/shared/NetworkSettings.css @@ -7,6 +7,8 @@ height: 25px; flex-flow: row nowrap; padding: 0; + background-color: var(--base-background-color); + color: var(--base-foreground-color); line-height: 25px; } diff --git a/src/components/shared/PageSelectorIcon.tsx b/src/components/shared/PageSelectorIcon.tsx new file mode 100644 index 0000000000..94056fd8b6 --- /dev/null +++ b/src/components/shared/PageSelectorIcon.tsx @@ -0,0 +1,31 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import { Icon } from './Icon'; +import ExtensionFavicon from '../../../res/img/svg/extension-outline.svg'; +import DefaultLinkFavicon from '../../../res/img/svg/globe.svg'; + +type Props = { + readonly favicon: string | null; + readonly origin: string; +}; + +/** + * PageSelectorIcon wraps the Icon component and provides fallback icons + * for pages that don't have a favicon in the profile data. + * + * Fallback logic: + * - If favicon exists (Firefox 134+ provides base64 data URI), use it + * - If origin is moz-extension://, fallback to extension-outline.svg + * - Otherwise (regular pages, about: pages), fallback to globe.svg + */ +export function PageSelectorIcon({ favicon, origin }: Props) { + const iconUrl = + favicon ?? + (origin.startsWith('moz-extension://') + ? ExtensionFavicon + : DefaultLinkFavicon); + + return ; +} diff --git a/src/components/shared/PanelSearch.css b/src/components/shared/PanelSearch.css index 051bd04e05..9021aedffc 100644 --- a/src/components/shared/PanelSearch.css +++ b/src/components/shared/PanelSearch.css @@ -18,16 +18,34 @@ } .panelSearchFieldIntroduction { + --internal-foreground-color: var(--grey-50); + --internal-background-color: var(--base-background-color); + + /* This is grey-90 with 10% opacity, according to the photon design document */ + --internal-shadow-color: rgb(12 12 13 / 0.1); + z-index: -1; /* it needs to be below the input field */ padding: 0 4px; - background-color: white; - box-shadow: 0 1px 4px rgb(12 12 13 / 0.1); /* This is grey-90 with 10% opacity, according to the photon design document */ - color: var(--grey-50); + background-color: var(--base-background-color); + box-shadow: 0 1px 4px var(--internal-shadow-color); + color: var(--internal-foreground-color); font-size: 0.9em; line-height: 2; transform: translate(2px, -2px); } +:root.dark-mode { + .panelSearchFieldIntroduction { + --internal-foreground-color: var(--grey-40); + --internal-background-color: var(--panel-background-color); + + /* This is grey-90 with 10% opacity, according to the photon design document */ + --internal-shadow-color: rgb(12 12 13 / 0.2); + + border: 0.5px solid var(--panel-border-color); + } +} + .panelSearchFieldIntroduction.isDisplayed { transition: transform 150ms var(--animation-curve), diff --git a/src/components/shared/PanelSettingsList.css b/src/components/shared/PanelSettingsList.css index 2a3d6eb0a4..6f4ab1b377 100644 --- a/src/components/shared/PanelSettingsList.css +++ b/src/components/shared/PanelSettingsList.css @@ -13,6 +13,8 @@ } .panelSettingsListItem { + --internal-border-color: var(--grey-20); + display: flex; align-items: center; padding: 0; @@ -21,5 +23,11 @@ .panelSettingsListItem:not(:first-child) { padding-left: 10px; - border-left: 1px solid var(--grey-20); + border-left: 1px solid var(--internal-border-color); +} + +:root.dark-mode { + .panelSettingsListItem { + --internal-border-color: var(--grey-80); + } } diff --git a/src/components/shared/ProfileMetaInfoSummary.css b/src/components/shared/ProfileMetaInfoSummary.css index dffc90cc34..5ef5342639 100644 --- a/src/components/shared/ProfileMetaInfoSummary.css +++ b/src/components/shared/ProfileMetaInfoSummary.css @@ -5,12 +5,30 @@ /* We use em in this CSS file so that it will adjust with the inherited font-size. */ .profileMetaInfoSummary { + --internal-info-icon: url(../../../res/img/svg/info.svg); + --internal-firefox-icon: url(../../../res/img/svg/brands/firefoxbrowser.svg); + --internal-apple-icon: url(../../../res/img/svg/brands/apple.svg); + --internal-windows-icon: url(../../../res/img/svg/brands/windows.svg); + --internal-linux-icon: url(../../../res/img/svg/brands/linux.svg); + --internal-android-icon: url(../../../res/img/svg/brands/android.svg); + display: flex; overflow: hidden; min-width: 0; align-items: stretch; } +:root.dark-mode { + .profileMetaInfoSummary { + --internal-info-icon: url(../../../res/img/svg/info-light.svg); + --internal-firefox-icon: url(../../../res/img/svg/brands/firefoxbrowser-light.svg); + --internal-apple-icon: url(../../../res/img/svg/brands/apple-light.svg); + --internal-windows-icon: url(../../../res/img/svg/brands/windows-light.svg); + --internal-linux-icon: url(../../../res/img/svg/brands/linux-light.svg); + --internal-android-icon: url(../../../res/img/svg/brands/android-light.svg); + } +} + .profileMetaInfoSummaryProductAndVersion, .profileMetaInfoSummaryPlatform { overflow: hidden; @@ -20,32 +38,32 @@ .profileMetaInfoSummaryProductAndVersion { padding-left: 1.4em; /* 1 (for the background image) + 0.4 */ - background: url(../../../res/img/svg/info.svg) left center / 1em no-repeat; + background: var(--internal-info-icon) left center / 1em no-repeat; } .profileMetaInfoSummaryProductAndVersion[data-product^='Firefox'], .profileMetaInfoSummaryProductAndVersion[data-product='Fennec'] { - background-image: url(../../../res/img/svg/brands/firefoxbrowser.svg); + background-image: var(--internal-firefox-icon); } .profileMetaInfoSummaryPlatform { padding-left: 1.4em; /* 1 (for the background image) + 0.4 */ margin-left: 1em; - background: url(../../../res/img/svg/info.svg) left center / 1em no-repeat; + background: var(--internal-info-icon) left center / 1em no-repeat; } .profileMetaInfoSummaryPlatform[data-toolkit^='macOS'] { - background-image: url(../../../res/img/svg/brands/apple.svg); + background-image: var(--internal-apple-icon); } .profileMetaInfoSummaryPlatform[data-toolkit^='Windows'] { - background-image: url(../../../res/img/svg/brands/windows.svg); + background-image: var(--internal-windows-icon); } .profileMetaInfoSummaryPlatform[data-toolkit^='Linux'] { - background-image: url(../../../res/img/svg/brands/linux.svg); + background-image: var(--internal-linux-icon); } .profileMetaInfoSummaryPlatform[data-toolkit^='Android'] { - background-image: url(../../../res/img/svg/brands/android.svg); + background-image: var(--internal-android-icon); } diff --git a/src/components/shared/SourceView.tsx b/src/components/shared/SourceView.tsx index 7563e42d79..9e722f1f8b 100644 --- a/src/components/shared/SourceView.tsx +++ b/src/components/shared/SourceView.tsx @@ -4,7 +4,6 @@ import * as React from 'react'; import { ensureExists } from 'firefox-profiler/utils/types'; -import { mapGetKeyWithMaxValue } from 'firefox-profiler/utils'; import type { LineTimings } from 'firefox-profiler/types'; import type { SourceViewEditor } from './SourceView-codemirror'; @@ -40,12 +39,10 @@ for understanding where time was actually spent in a program." type SourceViewProps = { readonly timings: LineTimings; readonly sourceCode: string; - readonly disableOverscan: boolean; readonly filePath: string | null; - readonly scrollToHotSpotGeneration: number; + readonly scrollGeneration: number; readonly scrollToLineNumber?: number; - readonly hotSpotTimings: LineTimings; - readonly highlightedLine?: number; + readonly highlightedLine: number | null; }; let editorModulePromise: Promise | null = null; @@ -54,36 +51,6 @@ export class SourceView extends React.PureComponent { _ref = React.createRef(); _editor: SourceViewEditor | null = null; - /** - * Scroll to the line with the most hits, based on the timings in - * timingsForScrolling. - * - * How is timingsForScrolling different from this.props.timings? - * In the current implementation, this.props.timings are always the "global" - * timings, i.e. they show the line hits for all samples in the current view, - * regardless of the selected call node. However, when opening the source - * view from a specific call node, you really want to see the code that's - * relevant to that specific call node, or at least that specific function. - * So timingsForScrolling are the timings that indicate just the line hits - * in the selected call node. This means that the "hotspot" will be somewhere - * in the selected function, and it will even be in the line that's most - * relevant to that specific call node. - * - * Sometimes, timingsForScrolling can be completely empty. This happens, for - * example, when the source view is showing a different file than the - * selected call node's function's file, for example because we just loaded - * from a URL and ended up with an arbitrary selected call node. - * In that case, pick the hotspot from the global line timings. - */ - _scrollToHotSpot(timingsForScrolling: LineTimings) { - const heaviestLine = - mapGetKeyWithMaxValue(timingsForScrolling.totalLineHits) ?? - mapGetKeyWithMaxValue(this.props.timings.totalLineHits); - if (heaviestLine !== undefined) { - this._scrollToLine(heaviestLine - 5); - } - } - _scrollToLine(lineNumber: number) { if (this._editor) { this._editor.scrollToLine(lineNumber); @@ -142,15 +109,13 @@ export class SourceView extends React.PureComponent { this._getSourceCodeOrFallback(), this.props.filePath, this.props.timings, - this.props.highlightedLine ?? null, + this.props.highlightedLine, domParent ); this._editor = editor; // If an explicit line number is provided, scroll to it. Otherwise, scroll to the hotspot. if (this.props.scrollToLineNumber !== undefined) { this._scrollToLine(Math.max(1, this.props.scrollToLineNumber - 5)); - } else { - this._scrollToHotSpot(this.props.hotSpotTimings); } })(); } @@ -179,14 +144,11 @@ export class SourceView extends React.PureComponent { if ( contentsChanged || - this.props.scrollToHotSpotGeneration !== - prevProps.scrollToHotSpotGeneration + this.props.scrollGeneration !== prevProps.scrollGeneration ) { // If an explicit line number is provided, scroll to it. Otherwise, scroll to the hotspot. if (this.props.scrollToLineNumber !== undefined) { this._scrollToLine(Math.max(1, this.props.scrollToLineNumber - 5)); - } else { - this._scrollToHotSpot(this.props.hotSpotTimings); } } @@ -195,7 +157,7 @@ export class SourceView extends React.PureComponent { } if (this.props.highlightedLine !== prevProps.highlightedLine) { - this._editor.setHighlightedLine(this.props.highlightedLine ?? null); + this._editor.setHighlightedLine(this.props.highlightedLine); } } } diff --git a/src/components/shared/StackImplementationSetting.tsx b/src/components/shared/StackImplementationSetting.tsx index 8c71753ab0..aa14cf97ea 100644 --- a/src/components/shared/StackImplementationSetting.tsx +++ b/src/components/shared/StackImplementationSetting.tsx @@ -47,7 +47,7 @@ class StackImplementationSettingImpl extends PureComponent { ) { const htmlId = `implementation-radio-${implementationFilter}`; return ( - <> + ); } diff --git a/src/components/shared/StackSettings.css b/src/components/shared/StackSettings.css index e491e619ef..dba9d1128a 100644 --- a/src/components/shared/StackSettings.css +++ b/src/components/shared/StackSettings.css @@ -9,6 +9,8 @@ flex-flow: row nowrap; align-items: stretch; padding: 0; + background-color: var(--raised-background-color); + color: var(--raised-foreground-color); scrollbar-width: none; white-space: nowrap; } diff --git a/src/components/shared/TabSelectorMenu.css b/src/components/shared/TabSelectorMenu.css index 92c6ad16b3..831cf44fd8 100644 --- a/src/components/shared/TabSelectorMenu.css +++ b/src/components/shared/TabSelectorMenu.css @@ -3,6 +3,9 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ .TabSelectorMenu { + background-color: var(--menu-background-color); + color: var(--menu-foreground-color); + /* Make it scrollable if the tab list is too long. */ overflow-y: auto; } diff --git a/src/components/shared/TabSelectorMenu.tsx b/src/components/shared/TabSelectorMenu.tsx index 59101c23a3..0f493b04d0 100644 --- a/src/components/shared/TabSelectorMenu.tsx +++ b/src/components/shared/TabSelectorMenu.tsx @@ -11,7 +11,7 @@ import explicitConnect from 'firefox-profiler/utils/connect'; import { changeTabFilter } from 'firefox-profiler/actions/receive-profile'; import { getTabFilter } from '../../selectors/url-state'; import { getProfileFilterSortedPageData } from 'firefox-profiler/selectors/profile'; -import { Icon } from 'firefox-profiler/components/shared/Icon'; +import { PageSelectorIcon } from 'firefox-profiler/components/shared/PageSelectorIcon'; import type { TabID, SortedTabPageData } from 'firefox-profiler/types'; import type { ConnectedProps } from 'firefox-profiler/utils/connect'; @@ -73,7 +73,10 @@ class TabSelectorMenuImpl extends React.PureComponent { 'aria-checked': tabFilter === tabID ? 'false' : 'true', }} > - + {pageData.hostname} ))} diff --git a/src/components/shared/TrackSearchField.css b/src/components/shared/TrackSearchField.css index 9f3565a06f..2420627ad2 100644 --- a/src/components/shared/TrackSearchField.css +++ b/src/components/shared/TrackSearchField.css @@ -3,6 +3,9 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ .trackSearchField { + --internal-search-icon: url(../../../res/img/svg/searchfield-icon.svg); + --internal-cancel-icon: url(../../../res/img/svg/searchfield-cancel.svg); + position: relative; /* to properly position the button */ display: inline-flex; @@ -13,21 +16,32 @@ } .photon-input.trackSearchFieldInput { + --internal-border-color: #aaa; + /* the chain class selectors are used here to override the styling for the photon-input class */ width: calc(100% - 10px); height: 25px; padding: 0 18px 0 17px; /* right padding for the reset button, left padding for the search icon */ - border: 0.5px solid #aaa; + border: 0.5px solid var(--internal-border-color); margin: 0 5px; } +:root.dark-mode { + .trackSearchField { + --internal-search-icon: url(../../../res/img/svg/searchfield-icon-light.svg); + } + + .photon-input.trackSearchFieldInput { + --internal-border-color: var(--base-border-color); + } +} + .trackSearchFieldInput { position: relative; flex: 1; margin: 0; - background: url(../../../res/img/svg/searchfield-icon.svg) 3px center - no-repeat white; + background: var(--internal-search-icon) 3px center no-repeat; background-size: 11px 11px; } @@ -41,8 +55,7 @@ height: 11px; padding: 0; border: 0; - background: url(../../../res/img/svg/searchfield-cancel.svg) top left - no-repeat; + background: var(--internal-cancel-icon) top left no-repeat; background-size: contain; color: transparent; -moz-user-focus: ignore; diff --git a/src/components/shared/TransformNavigator.css b/src/components/shared/TransformNavigator.css index 77c91966a2..83ea41b0bc 100644 --- a/src/components/shared/TransformNavigator.css +++ b/src/components/shared/TransformNavigator.css @@ -3,7 +3,19 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ .calltreeTransformNavigator { - border-top: 1px solid var(--grey-30); - background: var(--grey-10); - color: var(--ink-70); + --internal-foreground-color: var(--panel-foreground-color); + --internal-background-color: var(--panel-background-color); + --internal-border-color: var(--panel-border-color); + + border-top: 1px solid var(--internal-border-color); + background: var(--internal-background-color); + color: var(--internal-foreground-color); +} + +:root.dark-mode { + .calltreeTransformNavigator { + --internal-foreground-color: var(--lowered-foreground-color); + --internal-border-color: var(--base-border-color); + --internal-background-color: var(--lowered-background-color); + } } diff --git a/src/components/shared/TreeView.css b/src/components/shared/TreeView.css index c799f3752b..da1e77572d 100644 --- a/src/components/shared/TreeView.css +++ b/src/components/shared/TreeView.css @@ -3,25 +3,45 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ .treeView { - --tree-row-toggle-button-color: #888; - --tree-row-toggle-button-active-color: var(--grey-60); - --selected-tree-row-toggle-button-color: #fff; - --selected-tree-row-toggle-button-active-color: rgb(255 255 255 / 0.7); + --internal-row-right-clicked-background-color: var(--blue-50-a30); + --internal-row-selected-foreground-color: #000; + --internal-row-selected-background-color: var(--grey-30); + --internal-highlighting-foreground-color: #000; + --internal-highlighting-background-color: #cbe8fe; + --internal-highlighting-shadow-color1: rgb(0 0 0 / 0.05); + --internal-highlighting-shadow-color2: rgb(0 0 0 / 0.3); + --internal-row-toggle-button-color: #888; + --internal-row-toggle-button-active-color: var(--grey-60); + --internal-selected-row-toggle-button-color: #fff; + --internal-selected-row-toggle-button-active-color: rgb(255 255 255 / 0.7); display: flex; flex: 1; flex-flow: column nowrap; - border-top: 1px solid var(--grey-30); + border-top: 1px solid var(--base-border-color); cursor: default; user-select: none; } +:root.dark-mode { + .treeView { + --internal-row-right-clicked-background-color: var(--blue-50-a30); + --internal-row-selected-foreground-color: var(--grey-10); + --internal-row-selected-background-color: var(--grey-70); + --internal-highlighting-foreground-color: var(--grey-10); + --internal-highlighting-background-color: #1280b6; + --internal-row-toggle-button-color: var(--grey-50); + --internal-row-toggle-button-active-color: var(--grey-60); + --internal-selected-row-toggle-button-color: var(--grey-10); + } +} + @media (forced-colors: active) { .treeView { - --tree-row-toggle-button-color: ButtonText; - --tree-row-toggle-button-active-color: SelectedItem; - --selected-tree-row-toggle-button-color: SelectedItemText; - --selected-tree-row-toggle-button-active-color: ButtonText; + --internal-row-toggle-button-color: ButtonText !important; + --internal-row-toggle-button-active-color: SelectedItem !important; + --internal-selected-row-toggle-button-color: SelectedItemText !important; + --internal-selected-row-toggle-button-active-color: ButtonText !important; } } @@ -40,8 +60,9 @@ .treeViewHeader { height: 15px; padding: 4px 0; - border-bottom: 1px solid var(--grey-30); - background: white; + border-bottom: 1px solid var(--base-border-color); + background: var(--raised-background-color); + color: var(--raised-foreground-color); } .treeViewHeaderMainColumn { @@ -58,6 +79,7 @@ z-index: 0; overflow: auto; flex: 1; + background-color: var(--base-background-color); line-height: 16px; outline: 0; will-change: scroll-position; @@ -86,7 +108,8 @@ .treeViewBodyInner { overflow: hidden; - background: white; + background: var(--base-background-color); + color: var(--base-foreground-color); } .treeViewHeaderColumn { @@ -116,13 +139,13 @@ } .treeViewColumnDivider::before { - border-right: 1px solid var(--grey-30); + border-right: 1px solid var(--base-border-color); content: ''; } .treeViewColumnDivider.isResizable::before { width: 1px; - border-left: 1px solid var(--grey-30); + border-left: 1px solid var(--base-border-color); } .treeViewHeaderColumn.treeViewFixedColumn { @@ -153,27 +176,32 @@ } .treeViewRow.even { - background-color: #fff; + background-color: var(--base-background-color); } .treeViewRow.odd { - background-color: #f5f5f5; + background-color: var(--row-odd-background-color); } /* Note that `isRightClicked` is present before `isSelected` so that it can be overriden. */ .treeViewRow.isRightClicked { - background-color: var(--blue-50-a30); + background-color: var(--internal-row-right-clicked-background-color); } .treeViewRow.isSelected { - background-color: var(--grey-30); - color: black; + background-color: var(--internal-row-selected-background-color); + color: var(--internal-row-selected-foreground-color); } .treeViewMainColumn.dim { opacity: 0.7; } +/* Override dimming level when selected for better a11y */ +.treeViewBody:focus .treeViewRow.isSelected .treeViewMainColumn.dim { + opacity: 0.9; +} + /* stylelint-disable order/properties-order, declaration-block-no-duplicate-properties, value-keyword-case */ .treeViewBody:focus .treeViewRow.isSelected { /* Fallback for browsers that don't support SelectedItem */ @@ -192,6 +220,11 @@ } /* stylelint-enable order/properties-order, declaration-block-no-duplicate-properties, value-keyword-case */ +/* Override opacity level of appendage column when selected for better a11y */ +.treeViewBody:focus .treeViewRow.isSelected .treeViewAppendageColumn { + opacity: 0.9; +} + .treeViewBody:focus .treeViewRow.isSelected a { color: inherit; } @@ -204,11 +237,10 @@ * covers the underlying background. There's an underlying background when the * line is selected. */ margin: -1px 0; - background: #cbe8fe; - box-shadow: - 0 0 0 0.5px rgb(0 0 0 / 0.05), - 0 1px 1px rgb(0 0 0 / 0.3); - color: #000; + background: var(--internal-highlighting-background-color); + box-shadow: 0 0 0 0.5px var(--internal-highlighting-shadow-color1) 0 1px 1px + var(--internal-highlighting-shadow-color2); + color: var(--internal-highlighting-foreground-color); } .treeRowToggleButton { @@ -219,7 +251,7 @@ border-bottom: 5px solid transparent; border-left: 8px solid; margin-left: 8px; - color: var(--tree-row-toggle-button-color); + color: var(--internal-row-toggle-button-color); /* Opt-out of forced colors so the transparent borders aren't turned into opaque ones */ forced-color-adjust: none; @@ -236,17 +268,17 @@ } .treeRowToggleButton:active:hover { - color: var(--tree-row-toggle-button-active-color); + color: var(--internal-row-toggle-button-active-color); } .treeViewBody:focus .treeViewRow.isSelected > .treeRowToggleButton { - color: var(--selected-tree-row-toggle-button-color); + color: var(--internal-selected-row-toggle-button-color); } .treeViewBody:focus .treeViewRow.isSelected > .treeRowToggleButton:active:hover { - color: var(--selected-tree-row-toggle-button-active-color); + color: var(--internal-selected-row-toggle-button-active-color); } .treeRowToggleButton.leaf { diff --git a/src/components/shared/chart/Canvas.tsx b/src/components/shared/chart/Canvas.tsx index a7b6c86509..c181d59158 100644 --- a/src/components/shared/chart/Canvas.tsx +++ b/src/components/shared/chart/Canvas.tsx @@ -327,6 +327,17 @@ export class ChartCanvas extends React.Component< _onDoubleClick = () => { this.props.onDoubleClickItem(this.state.hoveredItem); + + if (this.props.stickyTooltips) { + // The double click is received as a sequence of click + click + dblclick. + // The each click sets the selectedItem inside _onClick. + // + // Unset the selectedItem here to differentiate the behavior between + // the single click vs the double clicks. + this.setState(() => ({ + selectedItem: null, + })); + } }; _getHoveredItemInfo = (): React.ReactNode => { @@ -378,6 +389,20 @@ export class ChartCanvas extends React.Component< this.setState({ selectedItem: null }); } } + + if ( + this._canvas && + this._canvas.width !== 0 && + this.props.containerWidth === 0 + ) { + // This is a temporary default state triggered by Viewport, + // for the viewportNeedsUpdate condition. + // + // Another setState call with the updated containerWidth/containerHeight + // will be performed and componentDidUpdate will be called again. + // We should ignore this update, in order to avoid an unnecessary flash. + return; + } this._scheduleDraw(); } else if ( !hoveredItemsAreEqual(prevState.hoveredItem, this.state.hoveredItem) diff --git a/src/components/shared/chart/Viewport.css b/src/components/shared/chart/Viewport.css index a7868280c9..0c8bea78c1 100644 --- a/src/components/shared/chart/Viewport.css +++ b/src/components/shared/chart/Viewport.css @@ -7,16 +7,19 @@ overflow: hidden; flex: 1; margin-top: 0; + outline: none; /* hide focus outline */ } .chartViewport.expanded { - border-top: 1px solid var(--grey-30); - border-bottom: 1px solid var(--grey-30); + border-top: 1px solid var(--base-border-color); + border-bottom: 1px solid var(--base-border-color); margin-bottom: 5px; cursor: grab; } .chartViewport.expanded::after { + --internal-shadow-color: rgb(64 115 140 / 0.2); + position: absolute; z-index: 1; top: 0; @@ -25,7 +28,7 @@ height: 100%; /* Slight blue shadow. */ - box-shadow: inset 0 0 20px rgb(64 115 140 / 0.2); + box-shadow: inset 0 0 20px var(--internal-shadow-color); content: ''; pointer-events: none; } @@ -35,16 +38,18 @@ } .chartViewportScroll { + --internal-background-color: rgb(0 0 0 / 0.07); + position: absolute; bottom: 0; left: 0; overflow: hidden; padding: 3px 10px; border-radius: 0 5px 0 0; - background: rgb(0 0 0 / 0.07); + background: var(--internal-background-color); box-shadow: - 0 0 0 0.5px rgb(0 0 0 / 0.1), - 0 2px 4px rgb(0 0 0 / 0.1); + 0 0 0 0.5px var(--base-shadow-color), + 0 2px 4px var(--base-shadow-color); line-height: 20px; opacity: 1; pointer-events: none; @@ -56,6 +61,12 @@ will-change: opacity, transform; } +:root.dark-mode { + .chartViewportScroll { + --internal-background-color: rgb(237 237 240 / 0.07); + } +} + .chartViewportScroll.hidden { opacity: 0; transform: translateY(30px); @@ -66,10 +77,10 @@ top: -1px; display: inline-block; padding: 0 0.5em; - border: 1px solid #ccc; + border: 1px solid var(--kbd-border-color); border-radius: 0.2em; margin: 0 0.2em; - background-color: #f6f6f6; - box-shadow: -0.1em 0.1em 0 #bbb; - color: #000; + background-color: var(--kbd-background-color); + box-shadow: -0.1em 0.1em 0 var(--kbd-shadow-color); + color: var(--kbd-foreground-color); } diff --git a/src/components/shared/thread/ActivityGraphCanvas.tsx b/src/components/shared/thread/ActivityGraphCanvas.tsx index 705045cc82..77a562d43c 100644 --- a/src/components/shared/thread/ActivityGraphCanvas.tsx +++ b/src/components/shared/thread/ActivityGraphCanvas.tsx @@ -83,7 +83,7 @@ export class ActivityGraphCanvas extends React.PureComponent { category: categoryIndex, filteredOutByTransformFillStyle: _createDiagonalStripePattern( ctx, - styles.unselectedFillStyle + styles.getUnselectedFillStyle() ), }; } diff --git a/src/components/shared/thread/ActivityGraphFills.tsx b/src/components/shared/thread/ActivityGraphFills.tsx index 1898bfcde9..a664e8cfe8 100644 --- a/src/components/shared/thread/ActivityGraphFills.tsx +++ b/src/components/shared/thread/ActivityGraphFills.tsx @@ -67,10 +67,13 @@ type CategoryFill = { export type CategoryDrawStyles = ReadonlyArray<{ readonly category: number; readonly gravity: number; - readonly selectedFillStyle: string; - readonly unselectedFillStyle: string; + readonly _selectedFillStyle: string | [string, string]; + readonly _unselectedFillStyle: string | [string, string]; + readonly _selectedTextColor: string | [string, string]; + readonly getSelectedFillStyle: () => string; + readonly getUnselectedFillStyle: () => string; + readonly getSelectedTextColor: () => string; readonly filteredOutByTransformFillStyle: CanvasPattern; - readonly selectedTextColor: string; }>; type SelectedPercentageAtPixelBuffers = { @@ -328,7 +331,7 @@ export class ActivityGraphFillComputer { const categoryDrawStyle = categoryDrawStyles[category]; const percentageBuffers = this.mutablePercentageBuffers[category]; - if (categoryDrawStyle.selectedFillStyle === 'transparent') { + if (categoryDrawStyle.getSelectedFillStyle() === 'transparent') { return; } @@ -796,7 +799,7 @@ function _getCategoryFills( return [ { category: categoryDrawStyle.category, - fillStyle: categoryDrawStyle.unselectedFillStyle, + fillStyle: categoryDrawStyle.getUnselectedFillStyle(), perPixelContribution: buffer.beforeSelectedPercentageAtPixel, accumulatedUpperEdge: new Float32Array( buffer.beforeSelectedPercentageAtPixel.length @@ -804,7 +807,7 @@ function _getCategoryFills( }, { category: categoryDrawStyle.category, - fillStyle: categoryDrawStyle.selectedFillStyle, + fillStyle: categoryDrawStyle.getSelectedFillStyle(), perPixelContribution: buffer.selectedPercentageAtPixel, accumulatedUpperEdge: new Float32Array( buffer.beforeSelectedPercentageAtPixel.length @@ -812,7 +815,7 @@ function _getCategoryFills( }, { category: categoryDrawStyle.category, - fillStyle: categoryDrawStyle.unselectedFillStyle, + fillStyle: categoryDrawStyle.getUnselectedFillStyle(), perPixelContribution: buffer.afterSelectedPercentageAtPixel, accumulatedUpperEdge: new Float32Array( buffer.beforeSelectedPercentageAtPixel.length diff --git a/src/components/shared/thread/SampleGraph.tsx b/src/components/shared/thread/SampleGraph.tsx index 032e0bb35f..85070db2ef 100644 --- a/src/components/shared/thread/SampleGraph.tsx +++ b/src/components/shared/thread/SampleGraph.tsx @@ -9,7 +9,7 @@ import { timeCode } from 'firefox-profiler/utils/time-code'; import { getSampleIndexClosestToCenteredTime } from 'firefox-profiler/profile-logic/profile-data'; import { bisectionRight } from 'firefox-profiler/utils/bisect'; import { withSize } from 'firefox-profiler/components/shared/WithSize'; -import { BLUE_70, BLUE_40 } from 'photon-colors'; +import { BLUE_40, BLUE_50, BLUE_70, BLUE_80 } from 'photon-colors'; import { Tooltip, MOUSE_OFFSET, @@ -30,6 +30,7 @@ import type { } from 'firefox-profiler/types'; import type { SizeProps } from 'firefox-profiler/components/shared/WithSize'; import type { CpuRatioInTimeRange } from './ActivityGraphFills'; +import { lightDark } from 'firefox-profiler/utils/dark-mode'; export type HoveredPixelState = { readonly sample: IndexIntoSamplesTable | null; @@ -216,9 +217,9 @@ class ThreadSampleGraphCanvas extends React.PureComponent { // Draw the samples in multiple passes, separated by color. This reduces the calls // to ctx.fillStyle, which saves on time that's spent parsing color strings. - const lighterBlue = '#c5e1fe'; - drawSamples(regularSamples, BLUE_40); - drawSamples(highlightedSamples, BLUE_70); + const lighterBlue = lightDark('#c5e1fe', BLUE_80); + drawSamples(regularSamples, lightDark(BLUE_40, BLUE_70)); + drawSamples(highlightedSamples, lightDark(BLUE_70, BLUE_50)); drawSamples(idleSamples, lighterBlue); } diff --git a/src/components/sidebar/MarkerSidebar.tsx b/src/components/sidebar/MarkerSidebar.tsx index 0ffbd9db69..91b3a8f278 100644 --- a/src/components/sidebar/MarkerSidebar.tsx +++ b/src/components/sidebar/MarkerSidebar.tsx @@ -13,7 +13,7 @@ import { } from 'firefox-profiler/selectors/url-state'; import { TooltipMarker } from 'firefox-profiler/components/tooltip/Marker'; import { updateBottomBoxContentsAndMaybeOpen } from 'firefox-profiler/actions/profile-view'; -import { getBottomBoxInfoForStackFrame } from 'firefox-profiler/profile-logic/profile-data'; +import { getBottomBoxInfoForStackFrame } from 'firefox-profiler/profile-logic/bottom-box'; import type { ConnectedProps } from 'firefox-profiler/utils/connect'; import type { diff --git a/src/components/sidebar/sidebar.css b/src/components/sidebar/sidebar.css index 1b9f02f0bf..8e3e1c4bfd 100644 --- a/src/components/sidebar/sidebar.css +++ b/src/components/sidebar/sidebar.css @@ -1,7 +1,10 @@ .sidebar { + --internal-dimmed-foreground-color: var(--grey-50); + overflow: auto; - border-top: 1px solid var(--grey-30); - background: var(--grey-10); + border-top: 1px solid var(--panel-border-color); + background: var(--panel-background-color); + color: var(--base-foreground-color); font-size: 11px; --sidebar-indent: 16px; @@ -56,7 +59,7 @@ } .sidebar-title-label > :nth-child(2) { - color: var(--grey-50); + color: var(--internal-dimmed-foreground-color); text-align: end; } @@ -90,7 +93,7 @@ } .sidebar-subtitle { - color: var(--grey-50); + color: var(--internal-dimmed-foreground-color); font-size: inherit; /* reset browser styles */ } @@ -108,7 +111,7 @@ } .sidebar-label { - color: var(--grey-50); + color: var(--internal-dimmed-foreground-color); grid-column-start: 1; margin-inline-start: var(--sidebar-indent); white-space: nowrap; @@ -125,7 +128,11 @@ } .sidebar-histogram-bar { - background-color: var(--grey-30); + --internal-background-color: var(--grey-30); + --internal-transparent-color: var(--grey-50); + --internal-grey-color: var(--grey-50); + + background-color: var(--internal-background-color); grid-column-start: span 3; margin-inline-start: var(--sidebar-indent); } @@ -136,19 +143,22 @@ .sidebar-histogram-bar-color.category-color-transparent { /* A white bar does not show up well here */ - background-color: var(--grey-50); + background-color: var(--internal-transparent-color); } .sidebar-histogram-bar-color.category-color-grey { /* The grey category color is the same our background color, choose a darker grey instead. */ - background-color: var(--grey-50); + background-color: var(--internal-grey-color); } .sidebar-toggle { + --internal-focus-foreground-color: var(--blue-60); + --internal-active-foreground-color: var(--grey-60); + padding: 0; border: 0; background: 0; - color: var(--grey-50); + color: var(--internal-dimmed-foreground-color); font-size: inherit; } @@ -158,7 +168,7 @@ border: 0; border-radius: 2px; box-shadow: none; - color: var(--blue-60); + color: var(--internal-focus-foreground-color); text-decoration: underline; } @@ -183,7 +193,7 @@ } .sidebar-toggle:active:hover { - color: var(--grey-60); + color: var(--internal-active-foreground-color); } .sidebar-toggle.expanded::before { @@ -205,3 +215,19 @@ color: SelectedItem; } } + +:root.dark-mode { + .sidebar { + --internal-dimmed-foreground-color: var(--grey-40); + } + + .sidebar-histogram-bar { + --internal-background-color: var(--grey-70); + --internal-transparent-color: var(--grey-50); + } + + .sidebar-toggle { + --internal-focus-foreground-color: var(--blue-40); + --internal-active-foreground-color: var(--grey-30); + } +} diff --git a/src/components/stack-chart/Canvas.tsx b/src/components/stack-chart/Canvas.tsx index 0c113a3872..81fc837544 100644 --- a/src/components/stack-chart/Canvas.tsx +++ b/src/components/stack-chart/Canvas.tsx @@ -1,7 +1,7 @@ /* 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 { GREY_30 } from 'photon-colors'; +import { GREY_30, GREY_70 } from 'photon-colors'; import * as React from 'react'; import { TIMELINE_MARGIN_RIGHT } from '../../app-logic/constants'; import { withChartViewport, type Viewport } from '../shared/chart/Viewport'; @@ -10,13 +10,14 @@ import { FastFillStyle } from '../../utils'; import TextMeasurement from '../../utils/text-measurement'; import { formatMilliseconds, formatBytes } from '../../utils/format-numbers'; import { bisectionLeft, bisectionRight } from '../../utils/bisect'; -import type { - updatePreviewSelection, - changeMouseTimePosition, -} from '../../actions/profile-view'; +import type { changeMouseTimePosition } from '../../actions/profile-view'; type ChangeMouseTimePosition = typeof changeMouseTimePosition; -import { mapCategoryColorNameToStackChartStyles } from '../../utils/colors'; +import { + mapCategoryColorNameToStackChartStyles, + getForegroundColor, + getBackgroundColor, +} from '../../utils/colors'; import { TooltipCallNode } from '../tooltip/CallNode'; import { TooltipMarker } from '../tooltip/Marker'; @@ -50,7 +51,6 @@ import type { IndexIntoStackTiming, SameWidthsIndexToTimestampMap, } from '../../profile-logic/stack-timing'; -import type { WrapFunctionInDispatch } from '../../utils/connect'; type OwnProps = { readonly thread: Thread; @@ -63,14 +63,12 @@ type OwnProps = { readonly combinedTimingRows: CombinedTimingRows; readonly sameWidthsIndexToTimestampMap: SameWidthsIndexToTimestampMap; readonly stackFrameHeight: CssPixels; - readonly updatePreviewSelection: WrapFunctionInDispatch< - typeof updatePreviewSelection - >; readonly changeMouseTimePosition: ChangeMouseTimePosition; readonly getMarker: (param: MarkerIndex) => Marker; readonly categories: CategoryList; readonly callNodeInfo: CallNodeInfo; readonly selectedCallNodeIndex: IndexIntoCallNodeTable | null; + readonly onDoubleClick: (param: IndexIntoCallNodeTable | null) => void; readonly onSelectionChange: (param: IndexIntoCallNodeTable | null) => void; readonly onRightClick: (param: IndexIntoCallNodeTable | null) => void; readonly shouldDisplayTooltips: () => boolean; @@ -93,6 +91,7 @@ type HoveredStackTiming = { }; import './Canvas.css'; +import { lightDark } from 'firefox-profiler/utils/dark-mode'; const ROW_CSS_PIXELS_HEIGHT = 16; const TEXT_CSS_PIXELS_OFFSET_START = 3; @@ -224,7 +223,7 @@ class StackChartCanvasImpl extends React.PureComponent { const devicePixelsWidth = containerWidth * cssToDeviceScale; const devicePixelsHeight = containerHeight * cssToDeviceScale; - fastFillStyle.set('#ffffff'); + fastFillStyle.set(getBackgroundColor()); ctx.fillRect(0, 0, devicePixelsWidth, devicePixelsHeight); const viewportDevicePixelsTop = viewportTop * cssToDeviceScale; @@ -513,8 +512,8 @@ class StackChartCanvasImpl extends React.PureComponent { // Draw the box. fastFillStyle.set( isHovered || isSelected - ? colorStyles.selectedFillStyle - : colorStyles.unselectedFillStyle + ? colorStyles.getSelectedFillStyle() + : colorStyles.getUnselectedFillStyle() ); ctx.fillRect( intX, @@ -544,8 +543,8 @@ class StackChartCanvasImpl extends React.PureComponent { if (fittedText) { fastFillStyle.set( isHovered || isSelected - ? colorStyles.selectedTextColor - : '#000000' + ? colorStyles.getSelectedTextColor() + : getForegroundColor() ); ctx.fillText(fittedText, textX, intY + textDevicePixelsOffsetTop); } @@ -554,7 +553,7 @@ class StackChartCanvasImpl extends React.PureComponent { } // Draw the borders on the left and right. - fastFillStyle.set(GREY_30); + fastFillStyle.set(lightDark(GREY_30, GREY_70)); ctx.fillRect( pixelAtViewportPosition(0), 0, @@ -643,16 +642,14 @@ class StackChartCanvasImpl extends React.PureComponent { }; _onDoubleClickStack = (hoveredItem: HoveredStackTiming | null) => { - if (hoveredItem === null) { - return; + if (!hoveredItem) return; + + const result = + this._getCallNodeIndexOrMarkerIndexFromHoveredItem(hoveredItem); + + if (result && result.type === 'call-node') { + this.props.onDoubleClick(result.index); } - const { depth, stackTimingIndex } = hoveredItem; - const { combinedTimingRows, updatePreviewSelection } = this.props; - updatePreviewSelection({ - isModifying: false, - selectionStart: combinedTimingRows[depth].start[stackTimingIndex], - selectionEnd: combinedTimingRows[depth].end[stackTimingIndex], - }); }; _getCallNodeIndexOrMarkerIndexFromHoveredItem( diff --git a/src/components/stack-chart/index.css b/src/components/stack-chart/index.css index b6c1d42fd0..f8d8c53505 100644 --- a/src/components/stack-chart/index.css +++ b/src/components/stack-chart/index.css @@ -12,5 +12,5 @@ .stackChartContent { display: flex; flex: 1; - border-top: 1px solid var(--grey-30); + border-top: 1px solid var(--base-border-color); } diff --git a/src/components/stack-chart/index.tsx b/src/components/stack-chart/index.tsx index ad706dd84c..ba98f87a9b 100644 --- a/src/components/stack-chart/index.tsx +++ b/src/components/stack-chart/index.tsx @@ -39,7 +39,7 @@ import { changeMouseTimePosition, } from '../../actions/profile-view'; -import { getBottomBoxInfoForCallNode } from '../../profile-logic/profile-data'; +import { getBottomBoxInfoForCallNode } from '../../profile-logic/bottom-box'; import type { Thread, @@ -170,7 +170,8 @@ class StackChartImpl extends React.PureComponent { const bottomBoxInfo = getBottomBoxInfoForCallNode( nodeIndex, callNodeInfo, - thread + thread, + thread.samples ); updateBottomBoxContentsAndMaybeOpen('stack-chart', bottomBoxInfo); return; @@ -179,6 +180,23 @@ class StackChartImpl extends React.PureComponent { handleCallNodeTransformShortcut(event, threadsKey, nodeIndex); }; + _onDoubleClick = (callNodeIndex: IndexIntoCallNodeTable | null) => { + if (callNodeIndex === null) { + return; + } + + const { thread, callNodeInfo, updateBottomBoxContentsAndMaybeOpen } = + this.props; + + const bottomBoxInfo = getBottomBoxInfoForCallNode( + callNodeIndex, + callNodeInfo, + thread, + thread.samples + ); + updateBottomBoxContentsAndMaybeOpen('stack-chart', bottomBoxInfo); + }; + _onCopy = (event: ClipboardEvent) => { if (document.activeElement === this._viewport) { event.preventDefault(); @@ -213,7 +231,6 @@ class StackChartImpl extends React.PureComponent { timeRange, interval, previewSelection, - updatePreviewSelection, changeMouseTimePosition, callNodeInfo, categories, @@ -270,7 +287,6 @@ class StackChartImpl extends React.PureComponent { combinedTimingRows, sameWidthsIndexToTimestampMap, getMarker, - updatePreviewSelection, changeMouseTimePosition, rangeStart: timeRange.start, rangeEnd: timeRange.end, @@ -278,6 +294,7 @@ class StackChartImpl extends React.PureComponent { callNodeInfo, categories, selectedCallNodeIndex, + onDoubleClick: this._onDoubleClick, onSelectionChange: this._onSelectedCallNodeChange, // TODO: support right clicking user timing markers #2354. onRightClick: this._onRightClickedCallNodeChange, diff --git a/src/components/timeline/EmptyThreadIndicator.css b/src/components/timeline/EmptyThreadIndicator.css index 3289a0da66..2dea1bbfe3 100644 --- a/src/components/timeline/EmptyThreadIndicator.css +++ b/src/components/timeline/EmptyThreadIndicator.css @@ -8,6 +8,7 @@ /* The following color is based off grey-20, but uses opacity instead. */ --shutdown-startup-color: rgb(0 0 50 / 0.07); + --shutdown-startup-shadow-color: rgb(0 0 0 / 0.05); } .timelineEmptyThreadIndicatorBlock { @@ -22,13 +23,13 @@ .timelineEmptyThreadIndicatorStartup { border-right: solid 1px var(--grey-30); background-color: var(--shutdown-startup-color); - box-shadow: inset -4px 0 4px rgb(0 0 0 / 0.05); + box-shadow: inset -4px 0 4px var(--shutdown-startup-shadow-color); } .timelineEmptyThreadIndicatorShutdown { border-left: solid 1px var(--grey-30); background-color: var(--shutdown-startup-color); - box-shadow: inset 4px 0 4px rgb(0 0 0 / 0.05); + box-shadow: inset 4px 0 4px var(--shutdown-startup-shadow-color); } .timelineEmptyThreadIndicatorEmptyBuffer { diff --git a/src/components/timeline/Markers.tsx b/src/components/timeline/Markers.tsx index f75f72db74..f214e6239e 100644 --- a/src/components/timeline/Markers.tsx +++ b/src/components/timeline/Markers.tsx @@ -154,7 +154,7 @@ class TimelineMarkersCanvas extends React.PureComponent { ) : Number.MAX_SAFE_INTEGER; const markerStyle = getMarkerStyle(marker); - ctx.fillStyle = markerStyle.background; + ctx.fillStyle = markerStyle.getBackground(); if (markerStyle.squareCorners) { ctx.fillRect(pos, markerStyle.top, itemWidth, markerStyle.height); } else { @@ -497,6 +497,7 @@ class TimelineMarkers extends React.PureComponent { marker={hoveredMarker} threadsKey={threadsKey} restrictHeightWidth={true} + hideFilterButton={true} /> ) : null} diff --git a/src/components/timeline/OverflowEdgeIndicator.css b/src/components/timeline/OverflowEdgeIndicator.css index e769a24c66..a58fdf6fde 100644 --- a/src/components/timeline/OverflowEdgeIndicator.css +++ b/src/components/timeline/OverflowEdgeIndicator.css @@ -11,6 +11,8 @@ } .overflowEdgeIndicatorEdge { + --internal-shadow-color: rgb(0 0 0 / 0.4); + position: absolute; z-index: 3; opacity: 0; @@ -20,6 +22,12 @@ visibility: hidden; } +:root.dark-mode { + .overflowEdgeIndicatorEdge { + --internal-shadow-color: rgb(0 0 0 / 0.5); + } +} + .overflowEdgeIndicatorEdge.topEdge, .overflowEdgeIndicatorEdge.leftEdge, .overflowEdgeIndicatorEdge.bottomEdge { @@ -57,7 +65,7 @@ .overflowEdgeIndicatorEdge.topEdge { background: radial-gradient( ellipse at center top, - rgb(0 0 0 / 0.4), + var(--internal-shadow-color), transparent 60% ); } @@ -65,7 +73,7 @@ .overflowEdgeIndicatorEdge.rightEdge { background: radial-gradient( ellipse at right center, - rgb(0 0 0 / 0.4), + var(--internal-shadow-color), transparent 60% ); } @@ -73,7 +81,7 @@ .overflowEdgeIndicatorEdge.bottomEdge { background: radial-gradient( ellipse at center bottom, - rgb(0 0 0 / 0.4), + var(--internal-shadow-color), transparent 60% ); } @@ -81,7 +89,7 @@ .overflowEdgeIndicatorEdge.leftEdge { background: radial-gradient( ellipse at left center, - rgb(0 0 0 / 0.4), + var(--internal-shadow-color), transparent 60% ); } diff --git a/src/components/timeline/Ruler.css b/src/components/timeline/Ruler.css index a963c395e5..67c11ec397 100644 --- a/src/components/timeline/Ruler.css +++ b/src/components/timeline/Ruler.css @@ -3,9 +3,21 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ .timelineRuler { + --internal-foreground-color: var(--grey-50); + --internal-notch-color1: var(--grey-30); + --internal-notch-color2: #d7d7db66; /* --grey-30 + 66 */ + height: var(--timeline-ruler-height); } +:root.dark-mode { + .timelineRuler { + --internal-foreground-color: var(--grey-40); + --internal-notch-color1: var(--grey-40); + --internal-notch-color2: #b1b1b366; /* --grey-50 + 66 */ + } +} + .timelineRulerContainer { position: absolute; top: 0; @@ -16,7 +28,7 @@ height: 20px; padding: 0; margin: 0; - color: var(--grey-50); + color: var(--internal-foreground-color); cursor: default; font-size: 9px; line-height: 20px; @@ -37,9 +49,9 @@ margin-left: -1px; background: linear-gradient( transparent, - var(--grey-30) 19px, - var(--grey-30) 20px, - #d7d7db66 0 + var(--internal-notch-color1) 19px, + var(--internal-notch-color1) 20px, + var(--internal-notch-color2) 0 ); text-align: right; white-space: nowrap; diff --git a/src/components/timeline/Selection.css b/src/components/timeline/Selection.css index 48de90f2f4..1f07df3970 100644 --- a/src/components/timeline/Selection.css +++ b/src/components/timeline/Selection.css @@ -13,6 +13,16 @@ --grippy-range-border-color: white; --grippy-range-hover-background-color: #888; --selection-outbound-opacity: 0.1; + --internal-hover-line-color: rgb(0 0 0 / 0.4); + --internal-overlay-time-foreground-color: #fff; + --internal-overlay-time-background-color: rgb(153 153 153); + --internal-dimmed-color: rgb(12 12 13 / var(--selection-outbound-opacity)); + --internal-range-background-color: var(--blue-50); + --internal-zoom-background-color: rgb(255 255 255 / 0.6); + --internal-zoom-border-color: rgb(0 0 0 / 0.2); + --internal-zoom-icon: url(../../../res/img/svg/zoom-icon.svg); + --internal-zoom-hover-background-color: rgb(255 255 255 / 0.9); + --internal-zoom-active-background-color: rgb(160 160 160 / 0.6); position: relative; display: flex; @@ -23,13 +33,32 @@ -moz-user-focus: ignore; } +:root.dark-mode { + .timelineSelection { + --selection-outbound-opacity: 0.4; + --grippy-range-background-color: var(--grey-40); + --grippy-range-border-color: var(--grey-40); + --grippy-range-hover-background-color: var(--grey-60); + --internal-hover-line-color: rgb(237 237 240 / 0.4); + --internal-overlay-time-foreground-color: var(--grey-20); + --internal-overlay-time-background-color: var(--grey-70); + --internal-dimmed-color: rgb(0 0 0 / var(--selection-outbound-opacity)); + --internal-range-background-color: var(--blue-60); + --internal-zoom-background-color: rgb(54 57 89 / 0.6); + --internal-zoom-border-color: rgb(237 237 240 / 0.5); + --internal-zoom-icon: url(../../../res/img/svg/zoom-icon-light.svg); + --internal-zoom-hover-background-color: rgb(54 57 89 / 0.9); + --internal-zoom-active-background-color: rgb(89 94 145 / 0.6); + } +} + .timelineSelectionHoverLine { position: absolute; z-index: 1; top: 0; bottom: 0; width: 1px; - background: rgb(0 0 0 / 0.4); + background: var(--internal-hover-line-color); pointer-events: none; } @@ -39,8 +68,8 @@ padding: 3.25px 8px; border-radius: 0 4px 4px 0; margin-left: 1px; - background-color: rgb(153 153 153); - color: #fff; + background-color: var(--internal-overlay-time-background-color); + color: var(--internal-overlay-time-foreground-color); } .timelineSelectionOverlay { @@ -58,7 +87,7 @@ .timelineSelectionDimmerBefore, .timelineSelectionDimmerAfter { flex-shrink: 0; - background: rgb(12 12 13 / var(--selection-outbound-opacity)); + background: var(--internal-dimmed-color); forced-color-adjust: none; } @@ -130,9 +159,9 @@ top: 20px; padding: 4px 8px; border-radius: 0 0 4px 4px; - background-color: var(--blue-50); - box-shadow: 0 2px 2px rgb(0 0 0 / 0.2); - color: #fff; + background-color: var(--internal-range-background-color); + box-shadow: 0 2px 2px var(--base-shadow-color); + color: var(--base-foreground-color); opacity: 1; pointer-events: none; transition: opacity 200ms; @@ -147,10 +176,10 @@ width: 30px; height: 30px; box-sizing: border-box; - border: 1px solid rgb(0 0 0 / 0.2); + border: 1px solid var(--internal-zoom-border-color); border-radius: 100%; margin: -15px; - background-color: rgb(255 255 255 / 0.6); + background-color: var(--internal-zoom-background-color); opacity: 0.5; pointer-events: auto; transition: opacity 200ms ease-in-out; @@ -160,7 +189,7 @@ .timelineSelectionOverlayZoomButton::before { position: absolute; display: grid; - content: url(../../../res/img/svg/zoom-icon.svg); + content: var(--internal-zoom-icon); inset: 0; place-content: center; } @@ -172,7 +201,7 @@ } .timelineSelectionOverlayZoomButton:hover { - background-color: rgb(255 255 255 / 0.9); + background-color: var(--internal-zoom-hover-background-color); } .timelineSelectionOverlayZoomButton:active, @@ -181,7 +210,7 @@ } .timelineSelectionOverlayZoomButton:active:hover { - background-color: rgb(160 160 160 / 0.6); + background-color: var(--internal-zoom-active-background-color); } @media (forced-colors: active) { @@ -195,9 +224,9 @@ } .timelineSelection { - --grippy-range-background-color: ButtonText; - --grippy-range-border-color: ButtonBorder; - --grippy-range-hover-background-color: SelectedItem; + --grippy-range-background-color: ButtonText !important; + --grippy-range-border-color: ButtonBorder !important; + --grippy-range-hover-background-color: SelectedItem !important; --selection-outbound-opacity: 0.6; } diff --git a/src/components/timeline/Track.css b/src/components/timeline/Track.css index 7bba80b5d7..e58e759daa 100644 --- a/src/components/timeline/Track.css +++ b/src/components/timeline/Track.css @@ -7,10 +7,28 @@ * into one file, as it is mostly shared style. */ .timelineTrack { + --internal-row-selected-border-color: var(--blue-60); + --internal-label-details-foreground-color: var(--grey-90-a60); + --internal-close-icon: url(../../../res/img/svg/close-dark.svg); + --internal-close-hover-background-color: rgb(0 0 0 / 0.15); + --internal-close-active-background-color: rgb(0 0 0 / 0.3); + --internal-local-background-color: var(--grey-20); + padding: 0; margin: 0; } +:root.dark-mode { + .timelineTrack { + --internal-row-selected-border-color: var(--blue-50); + --internal-label-details-foreground-color: var(--grey-40); + --internal-close-icon: url(../../../res/img/svg/close-light.svg); + --internal-close-hover-background-color: rgb(255 255 255 / 0.15); + --internal-close-active-background-color: rgb(255 255 255 / 0.3); + --internal-local-background-color: var(--grey-90); + } +} + .timelineTrackLocal { --local-track-margin: 15px; @@ -25,13 +43,14 @@ display: flex; flex-flow: row nowrap; - background-color: #fff; + background-color: var(--raised-background-color); + color: var(--raised-foreground-color); } .timelineTrackRow.selected { position: relative; padding-left: 0; - background-color: #edf6ff; + background-color: var(--selected-track-background-color); } /* Showing a blue border on the left side of the track row to indicate that @@ -42,13 +61,13 @@ bottom: 0; left: 0; width: var(--selected-left-border-width); - background: var(--blue-60); + background: var(--internal-row-selected-border-color); content: ''; pointer-events: none; } .timelineTrackLocalRow { - border-left: 1px solid var(--grey-30); + border-left: 1px solid var(--base-border-color); } .timelineTrackLocalRow.selected { @@ -76,8 +95,8 @@ box-sizing: border-box; flex-flow: row nowrap; align-items: center; - border-top: 1px solid var(--grey-30); - border-right: 1px solid var(--grey-30); + border-top: 1px solid var(--base-border-color); + border-right: 1px solid var(--base-border-color); cursor: default; } @@ -90,6 +109,7 @@ padding: 0 0 0 10px; border: none; background: none; + color: var(--internal-name-foreground-color); font: inherit; text-align: left; text-overflow: ellipsis; @@ -97,7 +117,7 @@ } .timelineTrackNameButtonAdditionalDetails { - color: var(--grey-90-a60); + color: var(--internal-label-details-foreground-color); font-size: 10px; } @@ -109,7 +129,7 @@ padding: 1px; border: 0; border-radius: 2px; - background: url(../../../res/img/svg/close-dark.svg) no-repeat center; + background: var(--internal-close-icon) no-repeat center; background-origin: content-box; background-size: contain; color: transparent; @@ -118,11 +138,11 @@ } .timelineTrackCloseButton:hover { - background-color: rgb(0 0 0 / 0.15); + background-color: var(--internal-close-hover-background-color); } .timelineTrackCloseButton:active:hover { - background-color: rgb(0 0 0 / 0.3); + background-color: var(--internal-close-active-background-color); } .timelineTrackLabel:not(:hover) > .timelineTrackCloseButton { @@ -134,13 +154,13 @@ display: flex; flex: 1; flex-flow: column nowrap; - border-top: 1px solid var(--grey-30); + border-top: 1px solid var(--base-border-color); } .timelineTrackLocalTracks { position: relative; padding: 0; - background: var(--grey-20); + background: var(--internal-local-background-color); list-style: none; } @@ -157,7 +177,7 @@ left: -15px; width: calc(100% + 15px); height: 100%; - box-shadow: inset 0 1px 5px rgb(0 0 0 / 0.2); + box-shadow: inset 0 1px 5px var(--base-shadow-color); content: ''; pointer-events: none; } diff --git a/src/components/timeline/TrackContextMenu.css b/src/components/timeline/TrackContextMenu.css index e1bf61fad5..221de610d6 100644 --- a/src/components/timeline/TrackContextMenu.css +++ b/src/components/timeline/TrackContextMenu.css @@ -2,6 +2,13 @@ * 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/. */ +.timelineTrackContextMenu { + max-height: 43.5vh; + background-color: var(--menu-background-color); + color: var(--menu-foreground-color); + overflow-y: auto; +} + .timelineTrackContextMenuItem { display: flex; } @@ -16,16 +23,13 @@ } .timelineTrackContextMenuPid { - color: var(--grey-90-a60); -} + --internal-foreground-color: var(--grey-90-a60); -.react-contextmenu-item--selected .timelineTrackContextMenuPid { - color: #fff; + color: var(--internal-foreground-color); } -.timelineTrackContextMenu { - max-height: 43.5vh; - overflow-y: auto; +.react-contextmenu-item--selected .timelineTrackContextMenuPid { + color: var(--menu-foreground-color); } .trackContextMenuSearchFilter { @@ -33,3 +37,9 @@ text-overflow: ellipsis; white-space: nowrap; } + +:root.dark-mode { + .timelineTrackContextMenuPid { + --internal-foreground-color: var(--grey-10-a60); + } +} diff --git a/src/components/timeline/TrackCustomMarkerGraph.tsx b/src/components/timeline/TrackCustomMarkerGraph.tsx index db5d254e00..dcdb40b26d 100644 --- a/src/components/timeline/TrackCustomMarkerGraph.tsx +++ b/src/components/timeline/TrackCustomMarkerGraph.tsx @@ -471,6 +471,7 @@ class TrackCustomMarkerGraphImpl extends React.PureComponent { marker={marker} threadsKey={threadIndex} restrictHeightWidth={true} + hideFilterButton={true} /> ); diff --git a/src/components/timeline/TrackEventDelay.css b/src/components/timeline/TrackEventDelay.css index 498da075a8..5ab407a32b 100644 --- a/src/components/timeline/TrackEventDelay.css +++ b/src/components/timeline/TrackEventDelay.css @@ -15,13 +15,15 @@ } .timelineTrackEventDelayGraphDot { + --internal-background-color: var(--orange-60); + position: absolute; width: 6px; height: 6px; border-radius: 3px; margin-top: -3px; margin-left: -3px; - background-color: var(--orange-60); + background-color: var(--internal-background-color); pointer-events: none; } @@ -32,6 +34,6 @@ .timelineTrackEventDelayTooltipNumber { display: inline-block; min-width: 60px; - color: var(--grey-60); + color: var(--tooltip-number-foreground-color); font-weight: bold; } diff --git a/src/components/timeline/TrackMemory.css b/src/components/timeline/TrackMemory.css index 38fcf20e68..404b625b0c 100644 --- a/src/components/timeline/TrackMemory.css +++ b/src/components/timeline/TrackMemory.css @@ -31,7 +31,7 @@ .timelineTrackMemoryTooltipNumber { display: inline-block; min-width: 60px; - color: var(--grey-60); + color: var(--tooltip-number-foreground-color); font-weight: bold; } diff --git a/src/components/timeline/TrackNetwork.tsx b/src/components/timeline/TrackNetwork.tsx index 04f6ffb664..5740489f7a 100644 --- a/src/components/timeline/TrackNetwork.tsx +++ b/src/components/timeline/TrackNetwork.tsx @@ -409,6 +409,7 @@ class Network extends PureComponent { marker={hoveredMarker} threadsKey={threadIndex} restrictHeightWidth={true} + hideFilterButton={true} /> ) : null} diff --git a/src/components/timeline/TrackProcessCPU.css b/src/components/timeline/TrackProcessCPU.css index f834f41728..76f8c45d52 100644 --- a/src/components/timeline/TrackProcessCPU.css +++ b/src/components/timeline/TrackProcessCPU.css @@ -15,13 +15,15 @@ } .timelineTrackProcessCPUGraphDot { + --internal-background-color: var(--grey-50); + position: absolute; width: 6px; height: 6px; border-radius: 3px; margin-top: -3px; margin-left: -3px; - background-color: var(--grey-50); + background-color: var(--internal-background-color); pointer-events: none; } @@ -32,6 +34,6 @@ .timelineTrackProcessCPUTooltipNumber { display: inline-block; min-width: 60px; - color: var(--grey-60); + color: var(--tooltip-number-foreground-color); font-weight: bold; } diff --git a/src/components/timeline/TrackScreenshots.css b/src/components/timeline/TrackScreenshots.css index 268a918aca..10d9495e0c 100644 --- a/src/components/timeline/TrackScreenshots.css +++ b/src/components/timeline/TrackScreenshots.css @@ -3,9 +3,19 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ .timelineTrackScreenshot { + --internal-backgrond-color: var(--grey-20); + --internal-img-backgrond-color: var(--grey-30); + position: relative; overflow: hidden; - background: var(--grey-20); + background: var(--internal-backgrond-color); +} + +:root.dark-mode { + .timelineTrackScreenshot { + --internal-backgrond-color: var(--grey-90); + --internal-img-backgrond-color: var(--grey-80); + } } .timelineTrackScreenshotImgContainer { @@ -22,7 +32,7 @@ right: 0; width: 1px; height: 100%; - background: var(--grey-30); + background: var(--internal-img-backgrond-color); /* Create a border on the right of the container */ content: ''; @@ -37,7 +47,7 @@ .timelineTrackScreenshotHoverImg { padding: 0.5px; - border: 0.5px solid rgb(0 0 0 / 0.2); + border: 0.5px solid var(--base-shadow-color); border-radius: 5px; - box-shadow: 0 2px 4px rgb(0 0 0 / 0.2); + box-shadow: 0 2px 4px var(--base-shadow-color); } diff --git a/src/components/timeline/TrackThread.css b/src/components/timeline/TrackThread.css index c0e8351728..c25bd2d971 100644 --- a/src/components/timeline/TrackThread.css +++ b/src/components/timeline/TrackThread.css @@ -17,5 +17,5 @@ width: 100%; height: var(--timeline-track-thread-blank-height); padding-left: 1px; - background-color: #fff; + background-color: var(--base-background-color); } diff --git a/src/components/timeline/TrackVisualProgress.css b/src/components/timeline/TrackVisualProgress.css index a666104e06..aa637d7fa6 100644 --- a/src/components/timeline/TrackVisualProgress.css +++ b/src/components/timeline/TrackVisualProgress.css @@ -32,6 +32,6 @@ .timelineTrackVisualProgressTooltipNumber { display: inline-block; min-width: 60px; - color: var(--grey-60); + color: var(--tooltip-number-foreground-color); font-weight: bold; } diff --git a/src/components/timeline/VerticalIndicators.css b/src/components/timeline/VerticalIndicators.css index dae6436e92..2c3ceaf177 100644 --- a/src/components/timeline/VerticalIndicators.css +++ b/src/components/timeline/VerticalIndicators.css @@ -33,24 +33,40 @@ display: inline-block; width: 12px; height: 12px; - border: 1px solid var(--grey-30); + border: 1px solid var(--base-border-color); margin-bottom: -3px; background-color: var(--vertical-indicator-color); } .timelineVerticalIndicatorsDim { + --internal-foreground-color: var(--grey-50); + color: var(--grey-50); } .timelineVerticalIndicatorsTime { + --internal-foreground-color: var(--grey-70); + color: var(--grey-70); font-weight: bold; } .timelineVerticalIndicatorsUrl { + --internal-foreground-color: var(--grey-60); + overflow: hidden; margin-top: 6px; color: var(--grey-60); text-overflow: ellipsis; white-space: nowrap; } + +:root.dark-mode { + .timelineVerticalIndicatorsTime { + --internal-foreground-color: var(--grey-30); + } + + .timelineVerticalIndicatorsUrl { + --internal-foreground-color: var(--grey-40); + } +} diff --git a/src/components/timeline/index.css b/src/components/timeline/index.css index 17a4f3dbc3..e034c80932 100644 --- a/src/components/timeline/index.css +++ b/src/components/timeline/index.css @@ -3,6 +3,9 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ .timelineOverflowEdgeIndicatorScrollbox { + --internal-background-stop1-color: #eee; + --internal-background-stop2-color: var(--grey-30); + position: absolute; /* if the platform scrollbar ends up being larger than --vertical-scrollbar-reserved-width (which is unexpected), make sure there is no horizontal scrollbar */ @@ -14,20 +17,25 @@ margin-left: calc(var(--thread-label-column-width) * -1); background: linear-gradient( to left, - white, - #eee calc(var(--vertical-scrollbar-reserved-width) - 1px), - var(--grey-30) 0, - var(--grey-30) var(--vertical-scrollbar-reserved-width), + var(--base-background-color), + var(--internal-background-stop1-color) + calc(var(--vertical-scrollbar-reserved-width) - 1px), + var(--internal-background-stop2-color) 0, + var(--internal-background-stop2-color) + var(--vertical-scrollbar-reserved-width), transparent 0 ); } .timelineThreadList { + --internal-background-color: var(--grey-20); + --internal-shadow-color: var(--grey-30); + width: calc(100vw - var(--vertical-scrollbar-reserved-width)); padding: 0; margin: 0 0 0 calc(var(--thread-label-column-width) * -1); - background-color: var(--grey-20); - box-shadow: inset 0 1px var(--grey-30); + background-color: var(--internal-background-color); + box-shadow: inset 0 1px var(--internal-shadow-color); list-style: none; } @@ -37,10 +45,14 @@ box-sizing: border-box; /* Separator line between tracks menu button and timeline ruler */ - border-right: 1px solid var(--grey-30); + border-right: 1px solid var(--base-border-color); } .timelineSettingsHiddenTracks { + --internal-hover-background-color: rgb(0 0 0 / 0.1); + --internal-active-background-color: rgb(0 0 0 / 0.2); + --internal-arrow-foreground-color: var(--grey-60); + /* Make sure and override all rules that have to do with button defaults. */ position: relative; @@ -54,11 +66,11 @@ } .timelineSettingsHiddenTracks:hover { - background-color: rgb(0 0 0 / 0.1); + background-color: var(--internal-hover-background-color); } .timelineSettingsHiddenTracks:hover:active { - background-color: rgb(0 0 0 / 0.2); + background-color: var(--internal-active-background-color); } /* This is the dropdown arrow on the left of the button. */ @@ -73,7 +85,7 @@ margin-top: 5px; margin-right: 5px; margin-left: 5px; - color: var(--grey-60); + color: var(--internal-arrow-foreground-color); content: ''; /* Opt-out of forced colors so arrow is visible in High Contrast Mode */ @@ -97,13 +109,14 @@ * creating a padding and a minus margin with the scrollbar width to make * this work. */ padding-right: var(--vertical-scrollbar-reserved-width); - border-bottom: 1px solid var(--grey-30); + border-bottom: 1px solid var(--base-border-color); margin-right: calc(var(--vertical-scrollbar-reserved-width) * -1); margin-left: calc(var(--thread-label-column-width) * -1); /* This div needs to be apaque so that it can properly hide the hidden tracks * container border below */ - background: white; + background: var(--raised-background-color); + color: var(--raised-foreground-color); } .tracksContainer { @@ -136,3 +149,21 @@ color: currentcolor; } } + +:root.dark-mode { + .timelineOverflowEdgeIndicatorScrollbox { + --internal-background-stop1-color: var(--grey-80); + --internal-background-stop2-color: var(--grey-70); + } + + .timelineThreadList { + --internal-background-color: var(--grey-80); + --internal-shadow-color: var(--grey-90); + } + + .timelineSettingsHiddenTracks { + --internal-hover-background-color: rgb(237 237 240 / 0.1); + --internal-active-background-color: rgb(237 237 240 / 0.2); + --internal-arrow-foreground-color: var(--grey-30); + } +} diff --git a/src/components/tooltip/CallNode.css b/src/components/tooltip/CallNode.css index 4363502103..efd1e49744 100644 --- a/src/components/tooltip/CallNode.css +++ b/src/components/tooltip/CallNode.css @@ -2,20 +2,42 @@ * 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/. */ +.tooltipCallNodeDetails { + --internal-category-border-color: var(--grey-40); + --internal-header-foreground-color: var(--grey-50); + --internal-graph-meter-border-color: var(--grey-50); + --internal-graph-meter-background-color: var(--grey-90-a10); + --internal-swatch-border-color: rgb(0 0 0 / 0.1); + --internal-swatch-running-background-color: var(--blue-40); + --internal-swatch-self-background-color: var(--blue-60); +} + +:root.dark-mode { + .tooltipCallNodeDetails { + --internal-category-border-color: var(--grey-40); + --internal-header-foreground-color: var(--grey-20); + --internal-graph-meter-border-color: var(--grey-50); + --internal-graph-meter-background-color: var(--grey-10-a10); + --internal-swatch-border-color: rgb(237 237 240 / 0.1); + --internal-swatch-running-background-color: var(--blue-50); + --internal-swatch-self-background-color: var(--blue-70); + } +} + .tooltipCallNodeCategory { display: grid; align-items: center; padding-bottom: 10px; - border-top: 1px solid var(--grey-40); + border-top: 1px solid var(--internal-category-border-color); gap: 4px; grid-template-columns: repeat(4, min-content); } .tooltipCallNodeHeader { padding: 0 5px; - border-right: 1px solid var(--grey-30); + border-right: 1px solid var(--base-border-color); margin-top: 5px; - color: var(--grey-50); + color: var(--internal-header-foreground-color); white-space: nowrap; } @@ -39,7 +61,7 @@ height: 0; flex: 1; appearance: none; - background: var(--grey-90-a10); + background: var(--internal-graph-meter-background-color); } /* This is the bar inside the meter. */ @@ -49,7 +71,7 @@ /* Some colors don't have enough contrast (yellow for JavaScript, white for * Idle), therefore adding a border improves accessibility. */ - border-inline-end: 1px solid var(--grey-50); + border-inline-end: 1px solid var(--internal-graph-meter-border-color); } .tooltipCallNodeCategory .tooltipCategoryRowHeader { @@ -69,16 +91,16 @@ width: 9px; height: 9px; box-sizing: border-box; - border: 0.5px solid rgb(0 0 0 / 0.1); + border: 0.5px solid var(--internal-swatch-border-color); margin-right: 3px; } .tooltipCallNodeHeaderSwatchRunning { - background-color: var(--blue-40); + background-color: var(--internal-swatch-running-background-color); } .tooltipCallNodeHeaderSwatchSelf { - background-color: var(--blue-60); + background-color: var(--internal-swatch-self-background-color); } .tooltipCallNodeCategory .tooltipCallNodeHeaderSwatchSelf, diff --git a/src/components/tooltip/Marker.css b/src/components/tooltip/Marker.css index 6a3bc29dd7..87b1d9b507 100644 --- a/src/components/tooltip/Marker.css +++ b/src/components/tooltip/Marker.css @@ -18,13 +18,15 @@ } .tooltipTitleFilterButton { + --tooltip-filter-icon: url(../../../res/img/svg/filter.svg); + width: 16px; height: 16px; flex: none; padding: 0; border: none; background-color: transparent; - background-image: url(../../../res/img/svg/filter.svg); + background-image: var(--tooltip-filter-icon); cursor: pointer; margin-inline-start: 6px; opacity: 0.6; @@ -33,3 +35,9 @@ .tooltipTitleFilterButton:hover { opacity: 1; } + +:root.dark-mode { + .tooltipTitleFilterButton { + --tooltip-filter-icon: url(../../../res/img/svg/filter-light.svg); + } +} diff --git a/src/components/tooltip/Marker.tsx b/src/components/tooltip/Marker.tsx index 2b2719e6f7..9d8ea59bf0 100644 --- a/src/components/tooltip/Marker.tsx +++ b/src/components/tooltip/Marker.tsx @@ -85,6 +85,7 @@ type OwnProps = { readonly marker: Marker; readonly threadsKey: ThreadsKey; readonly className?: string; + readonly hideFilterButton?: boolean; // In tooltips it can be awkward for really long and tall things to force // the layout to be huge. This option when set to true will restrict the // height of things like stacks, and the width of long things like URLs. @@ -521,8 +522,13 @@ class MarkerTooltipContents extends React.PureComponent { * a short list of rendering strategies, in the order they appear. */ override render() { - const { className, markerIndex, getMarkerLabel, getMarkerSearchTerm } = - this.props; + const { + className, + markerIndex, + getMarkerLabel, + getMarkerSearchTerm, + hideFilterButton, + } = this.props; const markerLabel = getMarkerLabel(markerIndex); const searchTerm = getMarkerSearchTerm(markerIndex); return ( @@ -532,19 +538,21 @@ class MarkerTooltipContents extends React.PureComponent { {this._maybeRenderMarkerDuration()}
    {markerLabel} - -
diff --git a/src/components/tooltip/NetworkMarker.css b/src/components/tooltip/NetworkMarker.css index 7d5084b4f1..4efb38b794 100644 --- a/src/components/tooltip/NetworkMarker.css +++ b/src/components/tooltip/NetworkMarker.css @@ -7,9 +7,11 @@ } .tooltipNetworkPhases { + --internal-border-color: var(--grey-40); + display: grid; padding-top: 4px; - border-top: 1px solid var(--grey-40); + border-top: 1px solid var(--internal-border-color); margin-top: 4px; gap: 2px 5px; @@ -18,6 +20,12 @@ grid-template-columns: min-content min-content auto; } +:root.dark-mode { + .tooltipNetworkPhases { + --internal-border-color: var(--base-border-color); + } +} + .tooltipNetworkTitle3 { /* Reset most default properties of h3. */ padding: 0; @@ -38,7 +46,7 @@ } .tooltipNetworkPhaseEmpty { - background-color: white; + background-color: var(--base-background-color); box-shadow: 0 0 0 1px inset var(--marker-color); opacity: 0.5; } diff --git a/src/components/tooltip/Tooltip.css b/src/components/tooltip/Tooltip.css index bd119676ca..a2a3c150b2 100644 --- a/src/components/tooltip/Tooltip.css +++ b/src/components/tooltip/Tooltip.css @@ -1,12 +1,22 @@ .tooltip { + --internal-border-color: #ccc; + --internal-shadow-color: rgb(0 0 0 / 0.3); + --internal-timing-foreground-color: #666; + --internal-swatch-foreground-color: #888; + --internal-header-border-color: var(--grey-40); + --internal-dimmed-foreground-color: var(--grey-50); + --internal-backtrace-foreground-color: var(--grey-40); + --internal-label-foreground-color: var(--grey-60); + position: fixed; display: inline-block; overflow: hidden; max-width: 600px; padding: 8px; - border: 1px solid #ccc; - background-color: var(--grey-10); - box-shadow: 0 1px 3px rgb(0 0 0 / 0.3); + border: 1px solid var(--internal-border-color); + background-color: var(--panel-background-color); + box-shadow: 0 1px 3px var(--internal-shadow-color); + color: var(--base-foreground-color); line-height: 1.4; pointer-events: none; text-align: left; @@ -14,6 +24,17 @@ transition: opacity 250ms; } +:root.dark-mode { + .tooltip { + --internal-border-color: var(--grey-60); + --internal-timing-foreground-color: var(--grey-40); + --internal-header-border-color: var(--grey-40); + --internal-dimmed-foreground-color: var(--grey-40); + --internal-backtrace-foreground-color: var(--grey-30); + --internal-label-foreground-color: var(--grey-40); + } +} + .tooltip.clickable { /* Make sure that the pointer events are properly triggered if it's clickable */ pointer-events: auto; @@ -21,7 +42,7 @@ .tooltipTiming { padding-right: 0.5em; - color: #666; + color: var(--internal-timing-foreground-color); font-weight: bold; } @@ -53,7 +74,7 @@ display: inline-block; width: 10px; height: 10px; - border: 1px solid #888; + border: 1px solid var(--internal-swatch-foreground-color); margin-right: 3px; } @@ -71,7 +92,7 @@ .tooltipHeader { padding-bottom: 8px; - border-bottom: 1px solid var(--grey-40); + border-bottom: 1px solid var(--internal-header-border-color); font-size: 12px; } @@ -89,7 +110,7 @@ } .tooltipDetailSeparator { - border-top: 1px solid var(--grey-40); + border-top: 1px solid var(--internal-header-border-color); margin: 4px 0; grid-column: 1 / 3; } @@ -99,39 +120,39 @@ } .tooltipDetailsDim { - color: var(--grey-50); + color: var(--internal-dimmed-foreground-color); } .tooltipLabel { - color: var(--grey-50); + color: var(--internal-dimmed-foreground-color); text-align: right; white-space: nowrap; } .tooltipBackTraceTitle { margin: 0; - color: var(--grey-50); + color: var(--internal-dimmed-foreground-color); font-size: 100%; font-weight: normal; } .tooltipDetails + .tooltipDetailsBackTrace { padding-top: 5px; - border-top: 1px solid var(--grey-40); + border-top: 1px solid var(--internal-backtrace-foreground-color); margin-top: 5px; } .tooltipLib { align-self: flex-end; - color: var(--grey-50); + color: var(--internal-dimmed-foreground-color); white-space: nowrap; } .tooltipScreenshotImg { padding: 0.5px; - border: 0.5px solid rgb(0 0 0 / 0.2); + border: 0.5px solid var(--base-shadow-color); border-radius: 5px; - box-shadow: 0 2px 4px rgb(0 0 0 / 0.2); + box-shadow: 0 2px 4px var(--base-shadow-color); } /** @@ -148,7 +169,7 @@ .sidebar .tooltipLabel { margin-top: 8px; - color: var(--grey-60); + color: var(--internal-label-foreground-color); font-weight: bold; text-align: left; } diff --git a/src/index.tsx b/src/index.tsx index 20a3d4a286..51421b2551 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -24,6 +24,9 @@ import { logDevelopmentTips, } from './utils/window-console'; import { ensureExists } from './utils/types'; +import { initTheme } from './utils/dark-mode'; + +initTheme(); // Mock out Google Analytics for anything that's not production so that we have run-time // code coverage in development and testing. diff --git a/src/profile-logic/address-timings.ts b/src/profile-logic/address-timings.ts index 144dadfeb8..571758abcd 100644 --- a/src/profile-logic/address-timings.ts +++ b/src/profile-logic/address-timings.ts @@ -72,16 +72,12 @@ import type { FuncTable, StackTable, SamplesLikeTable, - IndexIntoCallNodeTable, IndexIntoNativeSymbolTable, StackAddressInfo, AddressTimings, Address, } from 'firefox-profiler/types'; -import { getMatchingAncestorStackForInvertedCallNode } from './profile-data'; -import type { CallNodeInfo, CallNodeInfoInverted } from './call-node-info'; - /** * For each stack in `stackTable`, and one specific native symbol, compute the * sets of addresses for frames belonging to that native symbol that are hit by @@ -180,312 +176,6 @@ export function getStackAddressInfo( }; } -/** - * Gathers the addresses which are hit by a given call node. - * This is different from `getStackAddressInfo`: `getStackAddressInfo` counts - * address hits anywhere in the stack, and this function only counts hits *in - * the given call node*. - * - * This is useful when opening the assembly view from a call node: We can - * directly jump to the place in the assembly where *this particular call node* - * spends its time. - * - * Returns a StackAddressInfo object for the given stackTable and for the library - * which contains the call node's func. - */ -export function getStackAddressInfoForCallNode( - stackTable: StackTable, - frameTable: FrameTable, - callNodeIndex: IndexIntoCallNodeTable, - callNodeInfo: CallNodeInfo, - nativeSymbol: IndexIntoNativeSymbolTable -): StackAddressInfo { - const callNodeInfoInverted = callNodeInfo.asInverted(); - return callNodeInfoInverted !== null - ? getStackAddressInfoForCallNodeInverted( - stackTable, - frameTable, - callNodeIndex, - callNodeInfoInverted, - nativeSymbol - ) - : getStackAddressInfoForCallNodeNonInverted( - stackTable, - frameTable, - callNodeIndex, - callNodeInfo, - nativeSymbol - ); -} - -/** - * This function handles the non-inverted case of getStackAddressInfoForCallNode. - * - * Gathers the addresses which are hit by a given call node in a given native - * symbol. - * - * This is best explained with an example. We first start with a case that does - * not have any inlining, because this is already complicated enough. - * - * Let the call node be the node for the call path [A, B, C]. - * Let the native symbol be C. - * Let every frame have inlineDepth:0. - * Let there be a native symbol for every func, with the same name as the func. - * Let this be the stack tree: - * - * - stack 1, func A - * - stack 2, func B - * - stack 3, func C, address 0x30 - * - stack 4, func C, address 0x40 - * - stack 5, func B - * - stack 6, func C, address 0x60 - * - stack 7, func C, address 0x70 - * - stack 8, func D - * - stack 9, func E - * - stack 10, func F - * - * This maps to the following call tree: - * - * - call node 1, func A - * - call node 2, func B - * - call node 3, func C - * - call node 4, func D - * - call node 5, func E - * - call node 6, func F - * - * The call path [A, B, C] uniquely identifies call node 3. - * The following stacks all "collapse into" ("map to") call node 3: - * stack 3, 4, 6 and 7. - * Stack 8 maps to call node 4, which is a child of call node 3. - * Stacks 1, 2, 5, 9 and 10 are outside the call path [A, B, C]. - * - * In this function, we only compute "address hits" that are contributed to - * the given call node. - * Stacks 3, 4, 6 and 7 all contribute their time both as "self time" - * and as "total time" to call node 3, at the addresses 0x30, 0x40, 0x60, - * and 0x70, respectively. - * Stack 8 also hits call node 3 at address 0x70, but does not contribute to - * call node 3's "self time", it only contributes to its "total time". - * Stacks 1, 2, 5, 9 and 10 don't contribute to call node 3's self or total time. - * - * Now here's an example *with* inlining. - * - * Let the call node be the node for the call path [A, B, C]. - * Let the native symbol be B. - * Let this be the stack tree: - * - * - stack 1, func A, nativeSymbol A - * - stack 2, func B, nativeSymbol B, address 0x40 - * - stack 3, func C, nativeSymbol B, address 0x40, inlineDepth 1 - * - stack 4, func B, nativeSymbol B, address 0x45 - * - stack 5, func C, nativeSymbol B, address 0x45, inlineDepth 1 - * - stack 6, func D, nativeSymbol D - * - stack 7, func E, nativeSymbol E - * - stack 8, func A, nativeSymbol A, address 0x30 - * - stack 9, func B, nativeSymbol A, address 0x30, inlineDepth 1 - * - stack 10, func C, nativeSymbol A, address 0x30, inlineDepth 2 - * - * This maps to the following call tree: - * - * - call node 1, func A - * - call node 2, func B - * - call node 3, func C - * - call node 4, func D - * - call node 5, func E - * - * The funky part here is that call node 3 has frames from two different native - * symbols: Two from native symbol B, and one from native symbol A. That's - * because B is present both as its own native symbol (separate outer function) - * and as an inlined call from A. In other words, C has been inlined both into - * a standalone B and also into another copy of B which was inlined into A. - * - * This means that, if the user double clicks call node 3, there are two - * different symbols for which we may want to display the assembly code. And - * depending on whether the assembly for A or for B is displayed, we want to - * call this function for a different native symbol. - * - * In this example, we call this function for native symbol B. - * - * The call path [A, B, C] uniquely identifies call node 3. - * The following stacks all "collapse into" ("map to") call node 3: - * stack 3, 5 and 10. However, only stacks 3 and 5 belong to native symbol B; - * stack 10 belongs to native symbol A. - * Stack 6 maps to call node 4, which is a child of call node 3. - * Stacks 1, 2, 4, 7, 8 and 9 are outside the call path [A, B, C]. - * - * Stacks 3 and 5 both contribute their time both as "self time" and as "total - * time" to call node 3 and native symbol B, at the addresses 0x40 and 0x45, - * respectively. Stack 10 has the right call node but the wrong native symbol, - * so it contributes to neither self nor total time. - * Stack 6 also hits call node 3 at address 0x45, but does not contribute to - * call node 3's "self time", it only contributes to its "total time". - * Stacks 1, 2, 4, 7, 8 and 9 don't contribute to call node 3's self or total time. - * - * --- - * - * All stacks can contribute no more than one address in the given call node. - * This is different from the getStackAddressInfo function above, where each - * stack can hit many addresses of the same native symbol, because all of the ancestor - * stacks are taken into account, rather than just one of them. Concretely, - * this means that in the returned StackAddressInfo, each stackAddresses[stack] - * set will only contain at most one element. - * - * The returned StackAddressInfo is computed as follows: - * selfAddress[stack]: - * For stacks that map to the given call node and whose nativeSymbol is the - * given native symbol, this is stack.frame.address. - * For all other stacks this is null. - * stackAddresses[stack]: - * For stacks that map to the given call node or one of its descendant - * call nodes, and whose nativeSymbol is the given native symbol, this is a - * set containing one element, which is ancestorStack.frame.address, where - * ancestorStack maps to the given call node. - * For all other stacks, this is null. - */ -export function getStackAddressInfoForCallNodeNonInverted( - stackTable: StackTable, - frameTable: FrameTable, - callNodeIndex: IndexIntoCallNodeTable, - callNodeInfo: CallNodeInfo, - nativeSymbol: IndexIntoNativeSymbolTable -): StackAddressInfo { - const stackIndexToCallNodeIndex = - callNodeInfo.getStackIndexToNonInvertedCallNodeIndex(); - - // "self address" == "the address which a stack's self time is contributed to" - const callNodeSelfAddressForAllStacks = []; - // "total addresses" == "the set of addresses whose total time this stack contributes to" - // Either null or a single-element set. - const callNodeTotalAddressesForAllStacks: Array | null> = []; - - // This loop takes advantage of the fact that the stack table is topologically ordered: - // Prefix stacks are always visited before their descendants. - for (let stackIndex = 0; stackIndex < stackTable.length; stackIndex++) { - let selfAddress: Address | null = null; - let totalAddresses: Set
| null = null; - const frame = stackTable.frame[stackIndex]; - - if ( - stackIndexToCallNodeIndex[stackIndex] === callNodeIndex && - frameTable.nativeSymbol[frame] === nativeSymbol - ) { - // This stack contributes to the call node's self time for the right - // native symbol. We needed to check both, because multiple stacks for the - // same call node can have different native symbols. - selfAddress = frameTable.address[frame]; - if (selfAddress !== -1) { - totalAddresses = new Set([selfAddress]); - } - } else { - // This stack does not map to the given call node or has the wrong native - // symbol. So this stack contributes no self time to the call node for the - // requested native symbol, and we leave selfAddress at null. - // As for totalTime, this stack contributes to the same address's totalTime - // as its parent stack: If it is a descendant of a stack X which maps to - // the given call node, then it contributes to stack X's address's totalTime, - // otherwise it contributes to no address's totalTime. - // In the example above, this is how stack 8 contributes to call node 3's - // totalTime. - const prefixStack = stackTable.prefix[stackIndex]; - totalAddresses = - prefixStack !== null - ? callNodeTotalAddressesForAllStacks[prefixStack] - : null; - } - - callNodeSelfAddressForAllStacks.push(selfAddress); - callNodeTotalAddressesForAllStacks.push(totalAddresses); - } - return { - selfAddress: callNodeSelfAddressForAllStacks, - stackAddresses: callNodeTotalAddressesForAllStacks, - }; -} - -/** - * This handles the inverted case of getStackAddressInfoForCallNode. - * - * The returned StackAddressInfo is computed as follows: - * selfAddress[stack]: - * For (inverted thread) root stack nodes that map to the given call node - * and whose stack.frame.nativeSymbol is the given symbol, this is stack.frame.address. - * For (inverted thread) root stack nodes whose frame with a different symbol, - * or which don't map to the given call node, this is null. - * For (inverted thread) *non-root* stack nodes, this is the same as the selfAddress - * of the stack's prefix. This way, the selfAddress is always inherited from the - * subtree root. - * stackAddresses[stack]: - * For stacks that map to the given call node or one of its (inverted tree) - * descendant call nodes, this is a set containing one element, which is - * ancestorStack.frame.address, where ancestorStack maps to the given call - * node. - * For all other stacks, this is null. - */ -export function getStackAddressInfoForCallNodeInverted( - stackTable: StackTable, - frameTable: FrameTable, - callNodeIndex: IndexIntoCallNodeTable, - callNodeInfo: CallNodeInfoInverted, - nativeSymbol: IndexIntoNativeSymbolTable -): StackAddressInfo { - const depth = callNodeInfo.depthForNode(callNodeIndex); - const [rangeStart, rangeEnd] = - callNodeInfo.getSuffixOrderIndexRangeForCallNode(callNodeIndex); - const callNodeIsRootOfInvertedTree = callNodeInfo.isRoot(callNodeIndex); - const stackIndexToCallNodeIndex = - callNodeInfo.getStackIndexToNonInvertedCallNodeIndex(); - const stackTablePrefixCol = stackTable.prefix; - const suffixOrderIndexes = callNodeInfo.getSuffixOrderIndexes(); - - // "self address" == "the address which a stack's self time is contributed to" - const callNodeSelfAddressForAllStacks = []; - // "total addresses" == "the set of addresses whose total time this stack contributes to" - // Either null or a single-element set. - const callNodeTotalAddressesForAllStacks = []; - - for (let stackIndex = 0; stackIndex < stackTable.length; stackIndex++) { - let selfAddress: Address | null = null; - let totalAddresses: Set
| null = null; - - const stackForCallNode = getMatchingAncestorStackForInvertedCallNode( - stackIndex, - rangeStart, - rangeEnd, - suffixOrderIndexes, - depth, - stackIndexToCallNodeIndex, - stackTablePrefixCol - ); - if (stackForCallNode !== null) { - const frameForCallNode = stackTable.frame[stackForCallNode]; - if (frameTable.nativeSymbol[frameForCallNode] === nativeSymbol) { - // This stack contributes to the call node's total time for the right - // native symbol. We needed to check both, because multiple stacks for the - // same call node can have different native symbols. - const address = frameTable.address[frameForCallNode]; - if (address !== -1) { - totalAddresses = new Set([address]); - if (callNodeIsRootOfInvertedTree) { - // This is a root of the inverted tree, and it is the given - // call node. That means that we have a self address. - selfAddress = address; - } else { - // This is not a root stack node, so no self time is spent - // in the given call node for this stack node. - } - } - } - } - - callNodeSelfAddressForAllStacks.push(selfAddress); - callNodeTotalAddressesForAllStacks.push(totalAddresses); - } - return { - selfAddress: callNodeSelfAddressForAllStacks, - stackAddresses: callNodeTotalAddressesForAllStacks, - }; -} - // An AddressTimings instance without any hits. export const emptyAddressTimings: AddressTimings = { totalAddressHits: new Map(), @@ -530,3 +220,50 @@ export function getAddressTimings( } return { totalAddressHits, selfAddressHits }; } + +// Returns the addresses which are hit within the specified native +// symbol in a specific call node, along with the total of the +// sample weights per address. +// callNodeFramePerStack needs to be a mapping from stackIndex to the +// corresponding frame in the call node of interest. +export function getTotalAddressTimingsForCallNode( + samples: SamplesLikeTable, + callNodeFramePerStack: Int32Array, + frameTable: FrameTable, + nativeSymbol: IndexIntoNativeSymbolTable | null +): Map { + if (nativeSymbol === null) { + return new Map(); + } + + const totalPerAddress = new Map(); + for (let sampleIndex = 0; sampleIndex < samples.length; sampleIndex++) { + const stack = samples.stack[sampleIndex]; + if (stack === null) { + continue; + } + const callNodeFrame = callNodeFramePerStack[stack]; + if (callNodeFrame === -1) { + // This sample does not contribute to the call node's total. Ignore. + continue; + } + + if (frameTable.nativeSymbol[callNodeFrame] !== nativeSymbol) { + continue; + } + + const address = frameTable.address[callNodeFrame]; + if (address === -1) { + continue; + } + + const sampleWeight = + samples.weight !== null ? samples.weight[sampleIndex] : 1; + totalPerAddress.set( + address, + (totalPerAddress.get(address) ?? 0) + sampleWeight + ); + } + + return totalPerAddress; +} diff --git a/src/profile-logic/bottom-box.ts b/src/profile-logic/bottom-box.ts new file mode 100644 index 0000000000..4519ee5cc1 --- /dev/null +++ b/src/profile-logic/bottom-box.ts @@ -0,0 +1,170 @@ +/* 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 { resourceTypes } from './data-structures'; + +import type { + Thread, + IndexIntoStackTable, + IndexIntoCallNodeTable, + BottomBoxInfo, + SamplesLikeTable, +} from 'firefox-profiler/types'; +import type { CallNodeInfo } from './call-node-info'; +import { + getCallNodeFramePerStack, + getNativeSymbolInfo, + getNativeSymbolsForCallNode, +} from './profile-data'; +import { mapGetKeyWithMaxValue } from 'firefox-profiler/utils'; +import { getTotalLineTimingsForCallNode } from './line-timings'; +import { getTotalAddressTimingsForCallNode } from './address-timings'; + +/** + * Calculate the BottomBoxInfo for a call node, i.e. information about which + * things should be shown in the profiler UI's "bottom box" when this call node + * is double-clicked. + * + * We always want to update all panes in the bottom box when a new call node is + * double-clicked, so that we don't show inconsistent information side-by-side. + */ +export function getBottomBoxInfoForCallNode( + callNodeIndex: IndexIntoCallNodeTable, + callNodeInfo: CallNodeInfo, + thread: Thread, + samples: SamplesLikeTable +): BottomBoxInfo { + const { + stackTable, + frameTable, + funcTable, + stringTable, + resourceTable, + nativeSymbols, + } = thread; + + const funcIndex = callNodeInfo.funcForNode(callNodeIndex); + const sourceIndex = funcTable.source[funcIndex]; + const resource = funcTable.resource[funcIndex]; + const libIndex = + resource !== -1 && resourceTable.type[resource] === resourceTypes.library + ? resourceTable.lib[resource] + : null; + const callNodeFramePerStack = getCallNodeFramePerStack( + callNodeIndex, + callNodeInfo, + stackTable + ); + const nativeSymbolsForCallNode = getNativeSymbolsForCallNode( + callNodeFramePerStack, + frameTable + ); + + // If we have at least one native symbol to show assembly for, pick + // the first one arbitrarily. + // TODO: If the we have more than one native symbol, pick the one + // with the highest total sample count. + const initialNativeSymbol = nativeSymbolsForCallNode.length !== 0 ? 0 : null; + + const nativeSymbolInfosForCallNode = nativeSymbolsForCallNode.map( + (nativeSymbolIndex) => + getNativeSymbolInfo( + nativeSymbolIndex, + nativeSymbols, + frameTable, + stringTable + ) + ); + + // Compute the hottest line and instruction address, so we can ask the + // source and assembly view to scroll those into view. + const funcLine = funcTable.lineNumber[funcIndex]; + const lineTimings = getTotalLineTimingsForCallNode( + samples, + callNodeFramePerStack, + frameTable, + funcLine + ); + const hottestLine = mapGetKeyWithMaxValue(lineTimings); + const addressTimings = getTotalAddressTimingsForCallNode( + samples, + callNodeFramePerStack, + frameTable, + initialNativeSymbol !== null + ? nativeSymbolsForCallNode[initialNativeSymbol] + : null + ); + const hottestInstructionAddress = mapGetKeyWithMaxValue(addressTimings); + + return { + libIndex, + sourceIndex, + nativeSymbols: nativeSymbolInfosForCallNode, + initialNativeSymbol, + scrollToLineNumber: hottestLine, + scrollToInstructionAddress: hottestInstructionAddress, + highlightedLineNumber: null, + highlightedInstructionAddress: null, + }; +} + +/** + * Get bottom box info for a stack frame. This is similar to + * getBottomBoxInfoForCallNode but works directly with stack indexes. + */ +export function getBottomBoxInfoForStackFrame( + stackIndex: IndexIntoStackTable, + thread: Thread +): BottomBoxInfo { + const { + stackTable, + frameTable, + funcTable, + resourceTable, + nativeSymbols, + stringTable, + } = thread; + + const frameIndex = stackTable.frame[stackIndex]; + const funcIndex = frameTable.func[frameIndex]; + const sourceIndex = funcTable.source[funcIndex]; + const resource = funcTable.resource[funcIndex]; + const libIndex = + resource !== -1 && resourceTable.type[resource] === resourceTypes.library + ? resourceTable.lib[resource] + : null; + + // Get native symbol for this frame + const nativeSymbol = frameTable.nativeSymbol[frameIndex]; + const nativeSymbolInfos = + nativeSymbol !== null + ? [ + getNativeSymbolInfo( + nativeSymbol, + nativeSymbols, + frameTable, + stringTable + ), + ] + : []; + + const instructionAddress = + nativeSymbol !== null ? frameTable.address[frameIndex] : -1; + + // Extract line number from the frame + const lineNumber = frameTable.line[frameIndex]; + + return { + libIndex, + sourceIndex, + nativeSymbols: nativeSymbolInfos, + initialNativeSymbol: 0, + scrollToLineNumber: lineNumber ?? undefined, + highlightedLineNumber: lineNumber, + scrollToInstructionAddress: + instructionAddress !== -1 ? instructionAddress : undefined, + highlightedInstructionAddress: + instructionAddress !== -1 ? instructionAddress : null, + }; +} diff --git a/src/profile-logic/call-tree.ts b/src/profile-logic/call-tree.ts index 5802354430..71ab7b3f7a 100644 --- a/src/profile-logic/call-tree.ts +++ b/src/profile-logic/call-tree.ts @@ -6,7 +6,6 @@ import { timeCode } from '../utils/time-code'; import { getOriginAnnotationForFunc, getCategoryPairLabel, - getBottomBoxInfoForCallNode, } from './profile-data'; import { resourceTypes } from './data-structures'; import { getFunctionName } from './function-info'; @@ -37,6 +36,7 @@ import { checkBit } from '../utils/bitset'; import * as ProfileData from './profile-data'; import type { CallTreeSummaryStrategy } from '../types/actions'; import type { CallNodeInfo, CallNodeInfoInverted } from './call-node-info'; +import { getBottomBoxInfoForCallNode } from './bottom-box'; type CallNodeChildren = IndexIntoCallNodeTable[]; @@ -359,6 +359,7 @@ export class CallTree { _internal: CallTreeInternal; _callNodeInfo: CallNodeInfo; _thread: Thread; + _previewFilteredCtssSamples: SamplesLikeTable; _rootTotalSummary: number; _displayDataByIndex: Map; // _children is indexed by IndexIntoCallNodeTable. Since they are @@ -372,6 +373,7 @@ export class CallTree { thread: Thread, categories: CategoryList, callNodeInfo: CallNodeInfo, + previewFilteredCtssSamples: SamplesLikeTable, internal: CallTreeInternal, rootTotalSummary: number, isHighPrecision: boolean, @@ -381,6 +383,7 @@ export class CallTree { this._internal = internal; this._callNodeInfo = callNodeInfo; this._thread = thread; + this._previewFilteredCtssSamples = previewFilteredCtssSamples; this._rootTotalSummary = rootTotalSummary; this._displayDataByIndex = new Map(); this._children = []; @@ -616,7 +619,8 @@ export class CallTree { return getBottomBoxInfoForCallNode( callNodeIndex, this._callNodeInfo, - this._thread + this._thread, + this._previewFilteredCtssSamples ); } @@ -1050,6 +1054,7 @@ export function getCallTree( thread: Thread, callNodeInfo: CallNodeInfo, categories: CategoryList, + previewFilteredCtssSamples: SamplesLikeTable, callTreeTimings: CallTreeTimings, weightType: WeightType ): CallTree { @@ -1061,6 +1066,7 @@ export function getCallTree( thread, categories, callNodeInfo, + previewFilteredCtssSamples, new CallTreeInternalNonInverted(callNodeInfo, timings), timings.rootTotalSummary, Boolean(thread.isJsTracer), @@ -1073,6 +1079,7 @@ export function getCallTree( thread, categories, callNodeInfo, + previewFilteredCtssSamples, new CallTreeInternalFunctionList(timings), timings.rootTotalSummary, Boolean(thread.isJsTracer), @@ -1085,6 +1092,7 @@ export function getCallTree( thread, categories, callNodeInfo, + previewFilteredCtssSamples, new CallTreeInternalInverted( ensureExists(callNodeInfo.asInverted()), timings diff --git a/src/profile-logic/graph-color.ts b/src/profile-logic/graph-color.ts index 216afe78e1..84d2380aab 100644 --- a/src/profile-logic/graph-color.ts +++ b/src/profile-logic/graph-color.ts @@ -95,7 +95,7 @@ export function getTextColor(color: GraphColor): string { // values. For other values, mapCategoryColorNameToStyles defaults to gray. // This is good enough for now. const colorStyles = mapCategoryColorNameToStyles(color); - return colorStyles.selectedTextColor; + return colorStyles.getSelectedTextColor(); } /** diff --git a/src/profile-logic/import/chrome.ts b/src/profile-logic/import/chrome.ts index 9026a6fd20..436ccd50f6 100644 --- a/src/profile-logic/import/chrome.ts +++ b/src/profile-logic/import/chrome.ts @@ -922,6 +922,32 @@ function extractMarkers( throw new Error('No "Other" category in empty profile category list'); } + // Map to track category names to their indices. + const categoryNameToIndex = new Map(); + const categories = ensureExists(profile.meta.categories); + for (let i = 0; i < categories.length; i++) { + categoryNameToIndex.set(categories[i].name, i); + } + + // Helper function to get or create a category index for a given category name. + function getOrCreateCategoryIndex(categoryName: string): number { + const existing = categoryNameToIndex.get(categoryName); + if (existing !== undefined) { + return existing; + } + + // Create a new category with a default color. The colors are not important + // since we don't visualize the marker colors yet. + const newIndex = categories.length; + categories.push({ + name: categoryName, + color: 'grey', + subcategories: ['Other'], + }); + categoryNameToIndex.set(categoryName, newIndex); + return newIndex; + } + profile.meta.markerSchema = [ { name: 'EventDispatch', @@ -941,11 +967,6 @@ function extractMarkers( }, ]; - // Map to store begin event detail field for pairing with end events. - // For async events (b/e), key is "pid:tid:id:name" - // For duration events (B/E), key is "pid:tid:name" - const beginEventDetail: Map = new Map(); - // Track whether we've added the EventWithDetail schema let hasEventWithDetailSchema = false; @@ -1012,22 +1033,7 @@ function extractMarkers( } } - // For end events (E/e), try to use the detail from the corresponding begin event - if ((event.ph === 'E' || event.ph === 'e') && !argData) { - // Generate key for looking up the begin event detail - // For async events (b/e), use id; for duration events (B/E), use name only - const key = - event.ph === 'e' && 'id' in event - ? `${event.pid}:${event.tid}:${event.id}:${name}` - : `${event.pid}:${event.tid}:${name}`; - const detail = beginEventDetail.get(key); - if (detail) { - argData = { detail }; - } - } - markers.name.push(stringTable.indexForString(name)); - markers.category.push(otherCategoryIndex); if (argData && 'type' in argData) { argData.type2 = argData.type; @@ -1059,11 +1065,18 @@ function extractMarkers( hasEventWithDetailSchema = true; } - const newData = { - ...argData, - type: argData?.detail ? 'EventWithDetail' : name, - category: event.cat, - }; + const newData = argData + ? { + ...argData, + type: argData?.detail ? 'EventWithDetail' : name, + } + : null; + + // Store the category in the markers.category array. + const categoryIndex = event.cat + ? getOrCreateCategoryIndex(event.cat) + : otherCategoryIndex; + markers.category.push(categoryIndex); // @ts-expect-error Opt out of type checking for this one. markers.data.push(newData); @@ -1082,15 +1095,6 @@ function extractMarkers( markers.startTime.push(time); markers.endTime.push(null); markers.phase.push(INTERVAL_START); - - // Store the detail field from begin event so it can be used for the corresponding end event - if (argData?.detail) { - const key = - event.ph === 'b' && 'id' in event - ? `${event.pid}:${event.tid}:${event.id}:${name}` - : `${event.pid}:${event.tid}:${name}`; - beginEventDetail.set(key, argData.detail); - } } else if (event.ph === 'E' || event.ph === 'e') { // Duration or Async Event End // https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview#heading=h.nso4gcezn7n1 @@ -1098,13 +1102,6 @@ function extractMarkers( markers.startTime.push(null); markers.endTime.push(time); markers.phase.push(INTERVAL_END); - - // Clean up the stored begin event detail - const key = - event.ph === 'e' && 'id' in event - ? `${event.pid}:${event.tid}:${event.id}:${name}` - : `${event.pid}:${event.tid}:${name}`; - beginEventDetail.delete(key); } else { // Instant Event // https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview#heading=h.lenwiilchoxp diff --git a/src/profile-logic/line-timings.ts b/src/profile-logic/line-timings.ts index 68c0a679b5..fc9e09b13c 100644 --- a/src/profile-logic/line-timings.ts +++ b/src/profile-logic/line-timings.ts @@ -7,16 +7,12 @@ import type { FuncTable, StackTable, SamplesLikeTable, - IndexIntoCallNodeTable, StackLineInfo, LineTimings, LineNumber, IndexIntoSourceTable, } from 'firefox-profiler/types'; -import { getMatchingAncestorStackForInvertedCallNode } from './profile-data'; -import type { CallNodeInfo, CallNodeInfoInverted } from './call-node-info'; - /** * For each stack in `stackTable`, and one specific source file, compute the * sets of line numbers in file that are hit by the stack. @@ -110,256 +106,6 @@ export function getStackLineInfo( }; } -/** - * Gathers the line numbers which are hit by a given call node. - * This is different from `getStackLineInfo`: `getStackLineInfo` counts line hits - * anywhere in the stack, and this function only counts hits *in the given call node*. - * - * This is useful when opening a file from a call node: We can directly jump to the - * place in the file where *this particular call node* spends its time. - * - * Returns a StackLineInfo object for the given stackTable and for the source file - * which contains the call node's func. - */ -export function getStackLineInfoForCallNode( - stackTable: StackTable, - frameTable: FrameTable, - funcTable: FuncTable, - callNodeIndex: IndexIntoCallNodeTable, - callNodeInfo: CallNodeInfo -): StackLineInfo { - const callNodeInfoInverted = callNodeInfo.asInverted(); - return callNodeInfoInverted !== null - ? getStackLineInfoForCallNodeInverted( - stackTable, - frameTable, - funcTable, - callNodeIndex, - callNodeInfoInverted - ) - : getStackLineInfoForCallNodeNonInverted( - stackTable, - frameTable, - funcTable, - callNodeIndex, - callNodeInfo - ); -} - -/** - * This function handles the non-inverted case of getStackLineInfoForCallNode. - * - * Gathers the line numbers which are hit by a given call node. - * These line numbers are in the source file that contains that call node's func. - * - * This is best explained with an example. - * Let the call node be the node for the call path [A, B, C]. - * Let this be the stack tree: - * - * - stack 1, func A - * - stack 2, func B - * - stack 3, func C, line 30 - * - stack 4, func C, line 40 - * - stack 5, func B - * - stack 6, func C, line 60 - * - stack 7, func C, line 70 - * - stack 8, func D - * - stack 9, func E - * - stack 10, func F - * - * This maps to the following call tree: - * - * - call node 1, func A - * - call node 2, func B - * - call node 3, func C - * - call node 4, func D - * - call node 5, func E - * - call node 6, func F - * - * The call path [A, B, C] uniquely identifies call node 3. - * The following stacks all "collapse into" ("map to") call node 3: - * stack 3, 4, 6 and 7. - * Stack 8 maps to call node 4, which is a child of call node 3. - * Stacks 1, 2, 5, 9 and 10 are outside the call path [A, B, C]. - * - * In this function, we only compute "line hits" that are contributed to - * the given call node. - * Stacks 3, 4, 6 and 7 all contribute their time both as "self time" - * and as "total time" to call node 3, at the line numbers 30, 40, 60, - * and 70, respectively. - * Stack 8 also hits call node 3 at line 70, but does not contribute to - * call node 3's "self time", it only contributes to its "total time". - * Stacks 1, 2, 5, 9 and 10 don't contribute to call node 3's self or total time. - * - * All stacks can contribute no more than one line in the given call node. - * This is different from the getStackLineInfo function above, where each - * stack can hit many lines in the same file, because all of the ancestor - * stacks are taken into account, rather than just one of them. Concretely, - * this means that in the returned StackLineInfo, each stackLines[stack] - * set will only contain at most one element. - * - * The returned StackLineInfo is computed as follows: - * selfLine[stack]: - * For stacks that map to the given call node, this is stack.frame.line. - * For all other stacks this is null. - * stackLines[stack]: - * For stacks that map to the given call node or one of its descendant - * call nodes, this is a set containing one element, which is - * ancestorStack.frame.line, where ancestorStack maps to the given call - * node. - * For all other stacks, this is null. - */ -export function getStackLineInfoForCallNodeNonInverted( - stackTable: StackTable, - frameTable: FrameTable, - funcTable: FuncTable, - callNodeIndex: IndexIntoCallNodeTable, - callNodeInfo: CallNodeInfo -): StackLineInfo { - const stackIndexToCallNodeIndex = - callNodeInfo.getStackIndexToNonInvertedCallNodeIndex(); - - // "self line" == "the line which a stack's self time is contributed to" - const callNodeSelfLineForAllStacks = []; - // "total lines" == "the set of lines whose total time this stack contributes to" - // Either null or a single-element set. - const callNodeTotalLinesForAllStacks: Array | null> = []; - - // This loop takes advantage of the fact that the stack table is topologically ordered: - // Prefix stacks are always visited before their descendants. - for (let stackIndex = 0; stackIndex < stackTable.length; stackIndex++) { - let selfLine: LineNumber | null = null; - let totalLines: Set | null = null; - - if (stackIndexToCallNodeIndex[stackIndex] === callNodeIndex) { - // This stack contributes to the call node's self time. - // We don't need to check the stack's func or file because it'll be - // the same as the given call node's func and file. - const frame = stackTable.frame[stackIndex]; - selfLine = frameTable.line[frame]; - // Fallback to func line info if frame line info is not available - if (selfLine === null) { - const func = frameTable.func[frame]; - selfLine = funcTable.lineNumber[func]; - } - if (selfLine !== null) { - totalLines = new Set([selfLine]); - } - } else { - // This stack does not map to the given call node. - // So this stack contributes no self time to the call node, and we - // leave selfLine at null. - // As for totalTime, this stack contributes to the same line's totalTime - // as its parent stack: If it is a descendant of a stack X which maps to - // the given call node, then it contributes to stack X's line's totalTime, - // otherwise it contributes to no line's totalTime. - // In the example above, this is how stack 8 contributes to call node 3's - // totalTime. - const prefixStack = stackTable.prefix[stackIndex]; - totalLines = - prefixStack !== null - ? callNodeTotalLinesForAllStacks[prefixStack] - : null; - } - - callNodeSelfLineForAllStacks.push(selfLine); - callNodeTotalLinesForAllStacks.push(totalLines); - } - return { - selfLine: callNodeSelfLineForAllStacks, - stackLines: callNodeTotalLinesForAllStacks, - }; -} - -/** - * This handles the inverted case of getStackLineInfoForCallNode. - * - * The returned StackLineInfo is computed as follows: - * selfLine[stack]: - * For (inverted thread) root stack nodes that map to the given call node - * and whose stack.frame.func.file is the given file, this is stack.frame.line. - * For (inverted thread) root stack nodes whose frame is in a different file, - * or which don't map to the given call node, this is null. - * For (inverted thread) *non-root* stack nodes, this is the same as the selfLine - * of the stack's prefix. This way, the selfLine is always inherited from the - * subtree root. - * stackLines[stack]: - * For stacks that map to the given call node or one of its (inverted tree) - * descendant call nodes, this is a set containing one element, which is - * ancestorStack.frame.line, where ancestorStack maps to the given call - * node. - * For all other stacks, this is null. - */ -export function getStackLineInfoForCallNodeInverted( - stackTable: StackTable, - frameTable: FrameTable, - funcTable: FuncTable, - callNodeIndex: IndexIntoCallNodeTable, - callNodeInfo: CallNodeInfoInverted -): StackLineInfo { - const depth = callNodeInfo.depthForNode(callNodeIndex); - const [rangeStart, rangeEnd] = - callNodeInfo.getSuffixOrderIndexRangeForCallNode(callNodeIndex); - const callNodeIsRootOfInvertedTree = callNodeInfo.isRoot(callNodeIndex); - const stackIndexToCallNodeIndex = - callNodeInfo.getStackIndexToNonInvertedCallNodeIndex(); - const stackTablePrefixCol = stackTable.prefix; - const suffixOrderIndexes = callNodeInfo.getSuffixOrderIndexes(); - - // "self line" == "the line which a stack's self time is contributed to" - const callNodeSelfLineForAllStacks = []; - // "total lines" == "the set of lines whose total time this stack contributes to" - // Either null or a single-element set. - const callNodeTotalLinesForAllStacks = []; - - for (let stackIndex = 0; stackIndex < stackTable.length; stackIndex++) { - let selfLine: LineNumber | null = null; - let totalLines: Set | null = null; - - const stackForCallNode = getMatchingAncestorStackForInvertedCallNode( - stackIndex, - rangeStart, - rangeEnd, - suffixOrderIndexes, - depth, - stackIndexToCallNodeIndex, - stackTablePrefixCol - ); - if (stackForCallNode !== null) { - const frameForCallNode = stackTable.frame[stackForCallNode]; - // assert(frameTable.func[frameForCallNode] === suffixPath[0]); - - // This stack contributes to the call node's total time. - // We don't need to check the stack's func or file because it'll be - // the same as the given call node's func and file. - let line = frameTable.line[frameForCallNode]; - // Fallback to func line info if frame line info is not available - if (line === null) { - const func = frameTable.func[frameForCallNode]; - line = funcTable.lineNumber[func]; - } - if (line !== null) { - totalLines = new Set([line]); - if (callNodeIsRootOfInvertedTree) { - // This is a root of the inverted tree, and it is the given - // call node. That means that we have a self address. - selfLine = line; - } else { - // This is not a root stack node, so no self time is spent - // in the given call node for this stack node. - } - } - } - - callNodeSelfLineForAllStacks.push(selfLine); - callNodeTotalLinesForAllStacks.push(totalLines); - } - return { - selfLine: callNodeSelfLineForAllStacks, - stackLines: callNodeTotalLinesForAllStacks, - }; -} - // A LineTimings instance without any hits. export const emptyLineTimings: LineTimings = { totalLineHits: new Map(), @@ -404,3 +150,39 @@ export function getLineTimings( } return { totalLineHits, selfLineHits }; } + +// Returns the line numbers which are hit in a specific call node, +// along with the total of the sample weights per line. +// callNodeFramePerStack needs to be a mapping from stackIndex to the +// corresponding frame in the call node of interest. +export function getTotalLineTimingsForCallNode( + samples: SamplesLikeTable, + callNodeFramePerStack: Int32Array, + frameTable: FrameTable, + funcLine: LineNumber | null +): Map { + const totalPerLine = new Map(); + for (let sampleIndex = 0; sampleIndex < samples.length; sampleIndex++) { + const stack = samples.stack[sampleIndex]; + if (stack === null) { + continue; + } + const callNodeFrame = callNodeFramePerStack[stack]; + if (callNodeFrame === -1) { + // This sample does not contribute to the call node's total. Ignore. + continue; + } + + const frameLine = frameTable.line[callNodeFrame]; + const line = frameLine !== null ? frameLine : funcLine; + if (line === null) { + continue; + } + + const sampleWeight = + samples.weight !== null ? samples.weight[sampleIndex] : 1; + totalPerLine.set(line, (totalPerLine.get(line) ?? 0) + sampleWeight); + } + + return totalPerLine; +} diff --git a/src/profile-logic/marker-styles.ts b/src/profile-logic/marker-styles.ts index 5b71b2a061..23b797f5b8 100644 --- a/src/profile-logic/marker-styles.ts +++ b/src/profile-logic/marker-styles.ts @@ -6,19 +6,25 @@ import * as colors from 'photon-colors'; import type { CssPixels, Marker } from 'firefox-profiler/types'; +import { maybeLightDark } from '../utils/dark-mode'; + type MarkerStyle = { readonly top: CssPixels; readonly height: CssPixels; - readonly background: string; + readonly _background: string | [string, string]; + readonly getBackground: () => string; readonly squareCorners: boolean; readonly borderLeft: null | string; readonly borderRight: null | string; }; -const defaultStyle = { +const defaultStyle: MarkerStyle = { top: 0, height: 6, - background: 'black', + _background: ['black', colors.GREY_40], + getBackground: function () { + return maybeLightDark(this._background); + }, squareCorners: false, borderLeft: null, borderRight: null, @@ -27,13 +33,13 @@ const defaultStyle = { const gcStyle = { ...defaultStyle, top: 6, - background: colors.ORANGE_50, + _background: colors.ORANGE_50, }; const ccStyle = { ...gcStyle, // This is a paler orange to distinguish CC from GC. - background: '#ffc600', + _background: '#ffc600', }; /** @@ -55,13 +61,13 @@ const markerStyles: { readonly [styleName: string]: MarkerStyle } = { default: defaultStyle, RefreshDriverTick: { ...defaultStyle, - background: 'hsla(0,0%,0%,0.05)', + _background: 'rgba(237, 237, 240, 0.05)', height: 18, squareCorners: true, }, RD: { ...defaultStyle, - background: 'hsla(0,0%,0%,0.05)', + _background: 'rgba(237, 237, 240, 0.05)', height: 18, squareCorners: true, }, @@ -69,86 +75,86 @@ const markerStyles: { readonly [styleName: string]: MarkerStyle } = { // here for backwards compatibility. Scripts: { ...defaultStyle, - background: colors.ORANGE_70, + _background: colors.ORANGE_70, top: 6, }, 'requestAnimationFrame callbacks': { ...defaultStyle, - background: colors.ORANGE_70, + _background: colors.ORANGE_70, top: 6, }, Styles: { ...defaultStyle, - background: colors.TEAL_50, + _background: [colors.TEAL_50, colors.TEAL_60], top: 7, }, FireScrollEvent: { ...defaultStyle, - background: colors.ORANGE_70, + _background: colors.ORANGE_70, top: 7, }, Reflow: { ...defaultStyle, - background: colors.BLUE_50, + _background: colors.BLUE_50, top: 7, }, DispatchSynthMouseMove: { ...defaultStyle, - background: colors.ORANGE_70, + _background: colors.ORANGE_70, top: 8, }, DisplayList: { ...defaultStyle, - background: colors.PURPLE_50, + _background: colors.PURPLE_50, top: 9, }, LayerBuilding: { ...defaultStyle, - background: colors.ORANGE_50, + _background: colors.ORANGE_50, top: 9, }, Rasterize: { ...defaultStyle, - background: colors.GREEN_50, + _background: [colors.GREEN_50, colors.GREEN_60], top: 10, }, ForwardTransaction: { ...defaultStyle, - background: colors.RED_70, + _background: colors.RED_70, top: 11, }, NotifyDidPaint: { ...defaultStyle, - background: colors.GREY_40, + _background: colors.GREY_40, top: 12, }, LayerTransaction: { ...defaultStyle, - background: colors.RED_70, + _background: colors.RED_70, }, Composite: { ...defaultStyle, - background: colors.BLUE_50, + _background: colors.BLUE_50, }, Vsync: { ...defaultStyle, - background: 'rgb(255, 128, 0)', + _background: 'rgb(255, 128, 0)', }, LayerContentGPU: { ...defaultStyle, - background: 'rgba(0,200,0,0.5)', + _background: 'rgba(0,200,0,0.5)', }, LayerCompositorGPU: { ...defaultStyle, - background: 'rgba(0,200,0,0.5)', + _background: 'rgba(0,200,0,0.5)', }, LayerOther: { ...defaultStyle, - background: 'rgb(200,0,0)', + _background: 'rgb(200,0,0)', }, Jank: { ...defaultStyle, - background: 'hsl(347, 100%, 60%)', + _background: ['hsl(347, 100%, 60%)', 'hsl(347, 75%, 50%)'], borderLeft: colors.RED_50, borderRight: colors.RED_50, squareCorners: true, @@ -157,7 +163,7 @@ const markerStyles: { readonly [styleName: string]: MarkerStyle } = { // unavailable. Let's style them like Jank markers. 'BHR-detected hang': { ...defaultStyle, - background: 'hsl(347, 100%, 60%)', + _background: ['hsl(347, 100%, 60%)', 'hsl(347, 75%, 50%)'], borderLeft: colors.RED_50, borderRight: colors.RED_50, squareCorners: true, @@ -186,27 +192,27 @@ const markerStyles: { readonly [styleName: string]: MarkerStyle } = { // IO: FileIO: { ...defaultStyle, - background: colors.BLUE_50, + _background: colors.BLUE_50, }, IPCOut: { ...defaultStyle, - background: colors.BLUE_50, + _background: colors.BLUE_50, top: 2, }, SyncIPCOut: { ...defaultStyle, - background: colors.BLUE_70, + _background: colors.BLUE_70, top: 6, }, IPCIn: { ...defaultStyle, - background: colors.PURPLE_40, + _background: colors.PURPLE_40, top: 13, }, SyncIPCIn: { ...defaultStyle, - background: colors.PURPLE_70, + _background: colors.PURPLE_70, top: 17, }, }; diff --git a/src/profile-logic/process-profile.ts b/src/profile-logic/process-profile.ts index 2339e28824..a1049c0cab 100644 --- a/src/profile-logic/process-profile.ts +++ b/src/profile-logic/process-profile.ts @@ -1449,6 +1449,7 @@ function _convertGeckoMarkerSchema( display, data, graphs, + colorField, isStackBased, } = markerSchema; @@ -1494,6 +1495,7 @@ function _convertGeckoMarkerSchema( fields, description, graphs, + colorField, isStackBased, }; } diff --git a/src/profile-logic/profile-data.ts b/src/profile-logic/profile-data.ts index 21af66fcd8..3be6925778 100644 --- a/src/profile-logic/profile-data.ts +++ b/src/profile-logic/profile-data.ts @@ -35,8 +35,6 @@ import { numberSeriesFromDeltas, numberSeriesToDeltas, } from 'firefox-profiler/utils/number-series'; -import ExtensionFavicon from '../../res/img/svg/extension-outline.svg'; -import DefaultLinkFavicon from '../../res/img/svg/globe.svg'; import type { StringTable } from 'firefox-profiler/utils/string-table'; import type { @@ -91,7 +89,6 @@ import type { AddressProof, TimelineType, NativeSymbolInfo, - BottomBoxInfo, Bytes, ThreadWithReservedFunctions, TabID, @@ -730,6 +727,159 @@ export function getNthPrefixStack( return s; } +/** + * Given a call node `callNodeIndex`, answer, for each stack S: + * - Does a sample with stack S contribute to `callNodeIndex`'s total time? + * - If so, which of `callNodeIndex`'s frames does such a sample contribute its + * total time to? + * + * If the answer to the first question is "no", we put frame index -1 into the + * returned array for that stack index. + */ +export function getCallNodeFramePerStack( + callNodeIndex: IndexIntoCallNodeTable, + callNodeInfo: CallNodeInfo, + stackTable: StackTable +): Int32Array { + const callNodeInfoInverted = callNodeInfo.asInverted(); + return callNodeInfoInverted !== null + ? getCallNodeFramePerStackInverted( + callNodeIndex, + callNodeInfoInverted, + stackTable + ) + : getCallNodeFramePerStackNonInverted( + callNodeIndex, + callNodeInfo, + stackTable + ); +} + +/** + * This function handles the non-inverted case of getCallNodeFramePerStack. + * + * Gathers the frames which are hit in a given call node by each stack, + * or -1 if the stack isn't in the call node's subtree. + * + * This is best explained with an example. + * Let the call node be the node for the call path [A, B, C]. + * Let this be the stack tree: + * + * - stack 0, func A, frame 100 + * - stack 1, func B, frame 110 + * - stack 2, func C, frame 120 + * - stack 3, func C, frame 130 + * - stack 4, func B, frame 140 + * - stack 5, func C, frame 150 + * - stack 6, func C, frame 160 + * - stack 7, func D, frame 170 + * - stack 8, func E, frame 180 + * - stack 9, func F, frame 190 + * + * This maps to the following call tree: + * + * - call node 0, func A + * - call node 1, func B + * - call node 2, func C + * - call node 3, func D + * - call node 4, func E + * - call node 5, func F + * + * The call path [A, B, C] uniquely identifies call node 2. + * The following stacks all "collapse into" ("map to") call node 2: + * stack 2, 3, 5 and 6. + * Stack 7 maps to call node 3, which is a child of call node 2. + * Stacks 0, 1, 4, 8 and 9 are outside the call path [A, B, C]. + * + * Stacks 2, 3, 4 and 5 all make a "total time" contribution to call + * node 2, to the frames 120, 130, 150, and 160, respectively. + * Stack 7 also contributes total time to call node 2, to frame 160. + * Stacks 0, 1, 4, 8 and 9 don't contribute to call node 2's total time. + * + * So this function returns the following array in the example: + * new Int32Array([-1, -1, 120, 130, -1, 150, 160, 160, -1, -1]) + * // for stacks 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 + */ +export function getCallNodeFramePerStackNonInverted( + callNodeIndex: IndexIntoCallNodeTable, + callNodeInfo: CallNodeInfo, + stackTable: StackTable +): Int32Array { + const stackIndexToCallNodeIndex = + callNodeInfo.getStackIndexToNonInvertedCallNodeIndex(); + + const { frame: frameCol, prefix: prefixCol, length: stackCount } = stackTable; + + const callNodeFramePerStack = new Int32Array(stackCount); + + // This loop takes advantage of the stack table's ordering: + // Prefix stacks are always visited before their descendants. + for (let stackIndex = 0; stackIndex < stackCount; stackIndex++) { + let frame = -1; + const callNodeForThisStack = stackIndexToCallNodeIndex[stackIndex]; + if (callNodeForThisStack === callNodeIndex) { + frame = frameCol[stackIndex]; + } else { + // We're either already in the call node's subtree, or we are + // outside the subtree. Either way, we can just inherit the frame + // that our prefix stack hits in this call node. + const prefix = prefixCol[stackIndex]; + if (prefix !== null) { + frame = callNodeFramePerStack[prefix]; + } + } + + callNodeFramePerStack[stackIndex] = frame; + } + return callNodeFramePerStack; +} + +/** + * This handles the inverted case of getCallNodeFramePerStack. + */ +export function getCallNodeFramePerStackInverted( + callNodeIndex: IndexIntoCallNodeTable, + callNodeInfo: CallNodeInfoInverted, + stackTable: StackTable +): Int32Array { + const depth = callNodeInfo.depthForNode(callNodeIndex); + const [rangeStart, rangeEnd] = + callNodeInfo.getSuffixOrderIndexRangeForCallNode(callNodeIndex); + const stackIndexToCallNodeIndex = + callNodeInfo.getStackIndexToNonInvertedCallNodeIndex(); + const stackTablePrefixCol = stackTable.prefix; + const suffixOrderIndexes = callNodeInfo.getSuffixOrderIndexes(); + + const callNodeFramePerStack = new Int32Array(stackTable.length); + + for (let stackIndex = 0; stackIndex < stackTable.length; stackIndex++) { + let callNodeFrame = -1; + + // Get the non-inverted call tree node for the (non-inverted) stack. + // For example, if the stack has the call path A -> B -> C, + // this will give us the node A -> B -> C in the non-inverted tree. + const thisStackCallNode = stackIndexToCallNodeIndex[stackIndex]; + const thisStackSuffixOrderIndex = suffixOrderIndexes[thisStackCallNode]; + + if ( + thisStackSuffixOrderIndex >= rangeStart && + thisStackSuffixOrderIndex < rangeEnd + ) { + const stackForCallNode = getNthPrefixStack( + stackIndex, + depth, + stackTablePrefixCol + ); + if (stackForCallNode !== null) { + callNodeFrame = stackTable.frame[stackForCallNode]; + } + } + + callNodeFramePerStack[stackIndex] = callNodeFrame; + } + return callNodeFramePerStack; +} + /** * Take a samples table, and return an array that contain indexes that point to the * leaf most call node, or null. @@ -3307,7 +3457,7 @@ export function extractProfileFilterPageData( pageDataByTabID.set(tabID, { origin: pageUrl, hostname: pageUrl, - favicon: DefaultLinkFavicon, + favicon: null, }); continue; } @@ -3321,12 +3471,11 @@ export function extractProfileFilterPageData( // moz-extension:// protocol on platforms outside of Firefox. Only Firefox // can parse it properly. Chrome and node will output a URL with no `origin`. const isExtension = pageUrl.startsWith('moz-extension://'); - const defaultFavicon = isExtension ? ExtensionFavicon : DefaultLinkFavicon; const pageData: ProfileFilterPageData = { // These will be used as a fallback if the urls have been sanitized. origin: pageUrl, hostname: pageUrl, - favicon: currentPage.favicon ?? defaultFavicon, + favicon: currentPage.favicon ?? null, }; try { @@ -3858,75 +4007,18 @@ export function calculateFunctionSizeLowerBound( * functions. */ export function getNativeSymbolsForCallNode( - callNodeIndex: IndexIntoCallNodeTable, - callNodeInfo: CallNodeInfo, - stackTable: StackTable, - frameTable: FrameTable -): IndexIntoNativeSymbolTable[] { - const callNodeInfoInverted = callNodeInfo.asInverted(); - return callNodeInfoInverted !== null - ? getNativeSymbolsForCallNodeInverted( - callNodeIndex, - callNodeInfoInverted, - stackTable, - frameTable - ) - : getNativeSymbolsForCallNodeNonInverted( - callNodeIndex, - callNodeInfo, - stackTable, - frameTable - ); -} - -export function getNativeSymbolsForCallNodeNonInverted( - callNodeIndex: IndexIntoCallNodeTable, - callNodeInfo: CallNodeInfo, - stackTable: StackTable, - frameTable: FrameTable -): IndexIntoNativeSymbolTable[] { - const stackIndexToCallNodeIndex = - callNodeInfo.getStackIndexToNonInvertedCallNodeIndex(); - const set: Set = new Set(); - for (let stackIndex = 0; stackIndex < stackTable.length; stackIndex++) { - if (stackIndexToCallNodeIndex[stackIndex] === callNodeIndex) { - const frame = stackTable.frame[stackIndex]; - const nativeSymbol = frameTable.nativeSymbol[frame]; - if (nativeSymbol !== null) { - set.add(nativeSymbol); - } - } - } - return [...set]; -} - -export function getNativeSymbolsForCallNodeInverted( - callNodeIndex: IndexIntoCallNodeTable, - callNodeInfo: CallNodeInfoInverted, - stackTable: StackTable, + callNodeFramePerStack: Int32Array, frameTable: FrameTable ): IndexIntoNativeSymbolTable[] { - const depth = callNodeInfo.depthForNode(callNodeIndex); - const [rangeStart, rangeEnd] = - callNodeInfo.getSuffixOrderIndexRangeForCallNode(callNodeIndex); - const stackTablePrefixCol = stackTable.prefix; - const stackIndexToCallNodeIndex = - callNodeInfo.getStackIndexToNonInvertedCallNodeIndex(); - const suffixOrderIndexes = callNodeInfo.getSuffixOrderIndexes(); const set: Set = new Set(); - for (let stackIndex = 0; stackIndex < stackTable.length; stackIndex++) { - const stackForNode = getMatchingAncestorStackForInvertedCallNode( - stackIndex, - rangeStart, - rangeEnd, - suffixOrderIndexes, - depth, - stackIndexToCallNodeIndex, - stackTablePrefixCol - ); - if (stackForNode !== null) { - const frame = stackTable.frame[stackForNode]; - const nativeSymbol = frameTable.nativeSymbol[frame]; + for ( + let stackIndex = 0; + stackIndex < callNodeFramePerStack.length; + stackIndex++ + ) { + const callNodeFrame = callNodeFramePerStack[stackIndex]; + if (callNodeFrame !== -1) { + const nativeSymbol = frameTable.nativeSymbol[callNodeFrame]; if (nativeSymbol !== null) { set.add(nativeSymbol); } @@ -3962,109 +4054,6 @@ export function getNativeSymbolInfo( }; } -/** - * Calculate the BottomBoxInfo for a call node, i.e. information about which - * things should be shown in the profiler UI's "bottom box" when this call node - * is double-clicked. - * - * We always want to update all panes in the bottom box when a new call node is - * double-clicked, so that we don't show inconsistent information side-by-side. - */ -export function getBottomBoxInfoForCallNode( - callNodeIndex: IndexIntoCallNodeTable, - callNodeInfo: CallNodeInfo, - thread: Thread -): BottomBoxInfo { - const { - stackTable, - frameTable, - funcTable, - stringTable, - resourceTable, - nativeSymbols, - } = thread; - - const funcIndex = callNodeInfo.funcForNode(callNodeIndex); - const sourceIndex = funcTable.source[funcIndex]; - const resource = funcTable.resource[funcIndex]; - const libIndex = - resource !== -1 && resourceTable.type[resource] === resourceTypes.library - ? resourceTable.lib[resource] - : null; - const nativeSymbolsForCallNode = getNativeSymbolsForCallNode( - callNodeIndex, - callNodeInfo, - stackTable, - frameTable - ); - const nativeSymbolInfosForCallNode = nativeSymbolsForCallNode.map( - (nativeSymbolIndex) => - getNativeSymbolInfo( - nativeSymbolIndex, - nativeSymbols, - frameTable, - stringTable - ) - ); - - return { - libIndex, - sourceIndex, - nativeSymbols: nativeSymbolInfosForCallNode, - }; -} - -/** - * Get bottom box info for a stack frame. This is similar to - * getBottomBoxInfoForCallNode but works directly with stack indexes. - */ -export function getBottomBoxInfoForStackFrame( - stackIndex: IndexIntoStackTable, - thread: Thread -): BottomBoxInfo { - const { - stackTable, - frameTable, - funcTable, - resourceTable, - nativeSymbols, - stringTable, - } = thread; - - const frameIndex = stackTable.frame[stackIndex]; - const funcIndex = frameTable.func[frameIndex]; - const sourceIndex = funcTable.source[funcIndex]; - const resource = funcTable.resource[funcIndex]; - const libIndex = - resource !== -1 && resourceTable.type[resource] === resourceTypes.library - ? resourceTable.lib[resource] - : null; - - // Get native symbol for this frame - const nativeSymbol = frameTable.nativeSymbol[frameIndex]; - const nativeSymbolInfos = - nativeSymbol !== null - ? [ - getNativeSymbolInfo( - nativeSymbol, - nativeSymbols, - frameTable, - stringTable - ), - ] - : []; - - // Extract line number from the frame - const lineNumber = frameTable.line[frameIndex] ?? undefined; - - return { - libIndex, - sourceIndex, - nativeSymbols: nativeSymbolInfos, - lineNumber, - }; -} - /** * Determines the timeline type by looking at the profile data. * diff --git a/src/profile-logic/symbol-store-db.ts b/src/profile-logic/symbol-store-db.ts index 835db72568..dd12b8ee03 100644 --- a/src/profile-logic/symbol-store-db.ts +++ b/src/profile-logic/symbol-store-db.ts @@ -2,8 +2,6 @@ * 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 { SymbolsNotFoundError } from './errors'; - // Contains a symbol table, which can be used to map addresses to strings. // Symbol tables of this format are created within Firefox's implementation of // the geckoProfiler WebExtension API, at @@ -182,16 +180,16 @@ export default class SymbolStoreDB { /** * Retrieve the symbol table for the given library. - * @param {string} The debugName of the library. - * @param {string} The breakpadId of the library. - * @return A promise that resolves with the symbol table (in - * SymbolTableAsTuple format), or fails if we couldn't - * find a symbol table for the requested library. + * @param {string} debugName The debugName of the library. + * @param {string} breakpadId The breakpadId of the library. + * @return A promise that resolves with the symbol table (in + * SymbolTableAsTuple format), with null if we couldn't + * find a symbol table for the requested library. */ getSymbolTable( debugName: string, breakpadId: string - ): Promise { + ): Promise { return this._getDB().then((db) => { return new Promise((resolve, reject) => { const transaction = db.transaction('symbol-tables', 'readwrite'); @@ -207,12 +205,7 @@ export default class SymbolStoreDB { const { addrs, index, buffer } = value; updateDateReq.onsuccess = () => resolve([addrs, index, buffer]); } else { - reject( - new SymbolsNotFoundError( - 'The requested library does not exist in the database.', - { debugName, breakpadId } - ) - ); + resolve(null); } }; }); diff --git a/src/profile-logic/symbol-store.ts b/src/profile-logic/symbol-store.ts index 55def604d7..df39c76a30 100644 --- a/src/profile-logic/symbol-store.ts +++ b/src/profile-logic/symbol-store.ts @@ -2,10 +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 SymbolStoreDB from './symbol-store-db'; import { SymbolsNotFoundError } from './errors'; -import type { RequestedLib, ISymbolStoreDB } from 'firefox-profiler/types'; +import type { RequestedLib } from 'firefox-profiler/types'; import type { SymbolTableAsTuple } from './symbol-store-db'; import { ensureExists } from '../utils/types'; @@ -72,7 +71,10 @@ export interface SymbolProvider { // Expensive, should be called if the other two options were unsuccessful. // Does not carry file + line + inlines information. - requestSymbolTableFromBrowser(lib: RequestedLib): Promise; + requestSymbolsViaSymbolTableFromBrowser( + request: LibSymbolicationRequest, + ignoreCache: boolean + ): Promise>; } export interface AbstractSymbolStore { @@ -193,31 +195,6 @@ function _partitionIntoChunksOfMaxValue( return chunks.map(({ elements }) => elements); } -type DemangleFunction = (name: string) => string; - -/** - * This function returns a function that can demangle function name using a - * WebAssembly module, but falls back on the identity function if the - * WebAssembly module isn't available for some reason. - */ -async function _getDemangleCallback(): Promise { - try { - // When this module imports some WebAssembly module, Webpack's mechanism - // invokes the WebAssembly object which might be absent in some browsers, - // therefore `import` can throw. Also some browsers might refuse to load a - // wasm module because of our CSP. - // See webpack bug https://github.com/webpack/webpack/issues/8517 - const { demangle_any } = await import('gecko-profiler-demangle'); - return demangle_any; - } catch (error) { - // Module loading can fail (for example in browsers without WebAssembly - // support, or due to bad server configuration), so we will fall back - // to a pass-through function if that happens. - console.error('Demangling module could not be imported.', error); - return (mangledString) => mangledString; - } -} - /** * The SymbolStore implements efficient lookup of symbols for a set of addresses. * It consults multiple sources of symbol information and caches some results. @@ -226,44 +203,9 @@ async function _getDemangleCallback(): Promise { */ export class SymbolStore { _symbolProvider: SymbolProvider; - _db: ISymbolStoreDB; - constructor( - dbNamePrefixOrDB: string | ISymbolStoreDB, - symbolProvider: SymbolProvider - ) { + constructor(symbolProvider: SymbolProvider) { this._symbolProvider = symbolProvider; - if (typeof dbNamePrefixOrDB === 'string') { - this._db = new SymbolStoreDB(`${dbNamePrefixOrDB}-symbol-tables`); - } else { - this._db = dbNamePrefixOrDB; - } - } - - async closeDb() { - await this._db.close(); - } - - // Store a symbol table in the database. This is only used for symbol tables - // and not for partial symbol results. Symbol tables are obtained from the - // browser via the geckoProfiler object which is defined in a frame script: - // https://searchfox.org/mozilla-central/rev/a9db89754fb507254cb8422e5a00af7c10d98264/devtools/client/performance-new/frame-script.js#67-81 - // - // We do not store results from the Mozilla symbolication API, because those - // only contain the symbols we requested and not all the symbols of a given - // library. - _storeSymbolTableInDB( - lib: RequestedLib, - symbolTable: SymbolTableAsTuple - ): Promise { - return this._db - .storeSymbolTable(lib.debugName, lib.breakpadId, symbolTable) - .catch((error) => { - console.log( - `Failed to store the symbol table for ${lib.debugName} in the database:`, - error - ); - }); } /** @@ -281,10 +223,13 @@ export class SymbolStore { ): Promise { // For each library, we have three options to obtain symbol information for // it. We try all options in order, advancing to the next option on failure. - // Option 1: Symbol tables cached in the database, this._db. - // Option 2: Obtain symbols from the symbol server. - // Option 3: Obtain symbols from the browser, using the symbolication API. - // Option 4: Obtain symbols from the browser, via individual symbol tables. + // Option 1: Obtain symbols from the symbol server, using the symbolication API. + // Option 2: Obtain symbols from the browser, using the symbolication API. + // Option 3: Obtain symbols from the browser, via individual symbol tables. + // + // Symbol tables provide less information than the symbolication API (no inlines + // and no file / line information) so we try the API before we check for cached + // symbol tables. // Check requests for validity first. requests = requests.filter((request) => { @@ -303,48 +248,8 @@ export class SymbolStore { return true; }); - // First, try option 1 for all libraries and partition them by whether it - // was successful. - const requestsForNonCachedLibs: LibSymbolicationRequest[] = []; - const resultsForCachedLibs: Array<{ - lib: RequestedLib; - addresses: Set; - symbolTable: SymbolTableAsTuple; - }> = []; - if (ignoreCache) { - requestsForNonCachedLibs.push(...requests); - } else { - await Promise.all( - requests.map(async (request) => { - const { lib, addresses } = request; - const { debugName, breakpadId } = lib; - try { - // Try to get the symbol table from the database. - // This call will throw if the symbol table is not present. - const symbolTable = await this._db.getSymbolTable( - debugName, - breakpadId - ); - - // Did not throw, option 1 was successful! - resultsForCachedLibs.push({ - lib, - addresses, - symbolTable, - }); - } catch (e) { - if (!(e instanceof SymbolsNotFoundError)) { - // rethrow JavaScript programming error - throw e; - } - requestsForNonCachedLibs.push(request); - } - }) - ); - } - - // First phase of option 2: - // Try to service requestsForNonCachedLibs using the symbolication API, + // Option 1: + // Try to service requests using the symbolication API, // requesting chunks of max 10000 addresses each. In reality, this usually // means that all addresses for libxul get processed in one chunk, and the // addresses from all other libraries get processed in a second chunk. @@ -354,7 +259,7 @@ export class SymbolStore { // latency also suffers because of per-request overhead and pipelining limits. // We also limit each chunk to at most MAX_JOB_COUNT_PER_CHUNK libraries. const chunks = _partitionIntoChunksOfMaxValue( - requestsForNonCachedLibs, + requests, 10000, MAX_JOB_COUNT_PER_CHUNK, ({ addresses }) => addresses.size @@ -369,73 +274,96 @@ export class SymbolStore { this._symbolProvider.requestSymbolsFromServer(requests), ]); - // Finalize requests for which option 1 was successful: - // Now that the requests to the server have been kicked off, process - // symbolication for the libraries for which we found symbol tables in the - // database. This is delayed until after the request has been kicked off - // because it can take some time. - - // We also need a demangling function for this, which is in an async module. - const demangleCallback = await _getDemangleCallback(); - - for (const { lib, addresses, symbolTable } of resultsForCachedLibs) { - successCb( - lib, - readSymbolsFromSymbolTable(addresses, symbolTable, demangleCallback) - ); - } - // Process the results from the symbolication API request, as they arrive. // For unsuccessful requests, fall back to _getSymbolsFromBrowser. await Promise.all( symbolicationApiRequestsAndResponsesPerChunk.map( - async ([option2Requests, option2ResponsePromise]) => { + async ([chunkRequests, chunkResponsePromise]) => { // Store any errors encountered along the way in this map. // We will report them if all avenues fail. const errorMap: Map = new Map( requests.map((r): [LibSymbolicationRequest, Error[]] => [r, []]) ); - // Process the results of option 2: The response from the Mozilla symbolication APi. - const option3Requests = + // Process the results of option 1: The response from the Mozilla symbolication APi. + const requestsRemainingAfterServerAPI = await this._processSuccessfulResponsesAndReturnRemaining( - option2Requests, - option2ResponsePromise, + chunkRequests, + chunkResponsePromise, errorMap, successCb ); - if (option3Requests.length === 0) { + if (requestsRemainingAfterServerAPI.length === 0) { + // Done! + return; + } + + // Some libraries are still remaining, try option 2 for them. + // Option 2 is symbolProvider.requestSymbolsFromBrowser. + const browserAPIResponsePromise = + this._symbolProvider.requestSymbolsFromBrowser( + requestsRemainingAfterServerAPI + ); + const requestsRemainingAfterBrowserAPI = + await this._processSuccessfulResponsesAndReturnRemaining( + requestsRemainingAfterServerAPI, + browserAPIResponsePromise, + errorMap, + successCb + ); + + if (requestsRemainingAfterBrowserAPI.length === 0) { // Done! return; } // Some libraries are still remaining, try option 3 for them. - // Option 3 is symbolProvider.requestSymbolsFromBrowser. - const option3ResponsePromise = - this._symbolProvider.requestSymbolsFromBrowser(option3Requests); - const option4Requests = + // Option 3: Request a symbol table from the browser. + // Now we've reached the stage where we check for symbol tables. + const responsesPromise: Promise = + Promise.all( + requestsRemainingAfterBrowserAPI.map(async (request) => { + try { + const { lib } = request; + const results = + await this._symbolProvider.requestSymbolsViaSymbolTableFromBrowser( + request, + ignoreCache + ); + return { type: 'SUCCESS', lib, results }; + } catch (error) { + return { type: 'ERROR', request, error }; + } + }) + ); + + const failedRequests = await this._processSuccessfulResponsesAndReturnRemaining( - option3Requests, - option3ResponsePromise, + requestsRemainingAfterBrowserAPI, + responsesPromise, errorMap, successCb ); - if (option4Requests.length === 0) { + if (failedRequests.length === 0) { // Done! return; } - // Some libraries are still remaining, try option 4 for them. - // Option 4 is symbolProvider.requestSymbolTableFromBrowser. - await this._getSymbolTablesFromBrowser( - option4Requests, - errorMap, - demangleCallback, - successCb, - errorCb - ); + for (const request of failedRequests) { + const { lib } = request; + // None of the symbolication methods were successful. + // Call the error callback with all accumulated errors. + errorCb( + request, + new SymbolsNotFoundError( + `Could not obtain symbols for ${lib.debugName}/${lib.breakpadId}.`, + lib, + ...(errorMap.get(request) ?? []) + ) + ); + } } ) ); @@ -480,51 +408,4 @@ export class SymbolStore { return requests; } } - - // Try to get individual symbol tables from the browser, for any libraries - // which couldn't be symbolicated with the symbolication API. - // This is needed for two cases: - // 1. Firefox 95 and earlier, which didn't have a querySymbolicationApi - // WebChannel access point, and only supports symbol tables. - // 2. Android system libraries, even in modern versions of Firefox. We don't - // support querySymbolicationApi for them yet, see - // https://bugzilla.mozilla.org/show_bug.cgi?id=1735897 - async _getSymbolTablesFromBrowser( - requests: LibSymbolicationRequest[], - errorMap: Map, - demangleCallback: DemangleFunction, - successCb: (lib: RequestedLib, results: Map) => void, - errorCb: (request: LibSymbolicationRequest, error: Error) => void - ): Promise { - for (const request of requests) { - const { lib, addresses } = request; - try { - // Option 4: Request a symbol table from the browser. - // This call will throw if the browser cannot obtain the symbol table. - const symbolTable = - await this._symbolProvider.requestSymbolTableFromBrowser(lib); - - // Did not throw, option 4 was successful! - successCb( - lib, - readSymbolsFromSymbolTable(addresses, symbolTable, demangleCallback) - ); - - // Store the symbol table in the database. - await this._storeSymbolTableInDB(lib, symbolTable); - } catch (lastError) { - // None of the symbolication methods were successful. - // Call the error callback with all accumulated errors. - errorCb( - request, - new SymbolsNotFoundError( - `Could not obtain symbols for ${lib.debugName}/${lib.breakpadId}.`, - lib, - ...(errorMap.get(request) ?? []), - lastError - ) - ); - } - } - } } diff --git a/src/profile-logic/transforms.ts b/src/profile-logic/transforms.ts index 6de0e218a7..590cebbd2a 100644 --- a/src/profile-logic/transforms.ts +++ b/src/profile-logic/transforms.ts @@ -58,6 +58,7 @@ import type { StringTable } from 'firefox-profiler/utils/string-table'; const TRANSFORM_OBJ: { [key in TransformType]: true } = { 'focus-subtree': true, 'focus-function': true, + 'focus-self': true, 'merge-call-node': true, 'merge-function': true, 'drop-function': true, @@ -86,6 +87,9 @@ ALL_TRANSFORM_TYPES.forEach((transform: TransformType) => { case 'focus-function': shortKey = 'ff'; break; + case 'focus-self': + shortKey = 'ffs'; + break; case 'focus-category': shortKey = 'fg'; break; @@ -239,6 +243,20 @@ export function parseTransforms(transformString: string): TransformStack { } break; } + case 'focus-self': { + // e.g. "ffs-js-325" + const [, implementation, funcIndexRaw] = tuple; + const funcIndex = parseInt(funcIndexRaw, 10); + if (isNaN(funcIndex) || funcIndex < 0) { + break; + } + transforms.push({ + type: 'focus-self', + funcIndex, + implementation: toValidImplementationFilter(implementation), + }); + break; + } case 'focus-category': { // e.g. "fg-3" const [, categoryRaw] = tuple; @@ -359,6 +377,7 @@ export function stringifyTransforms(transformStack: TransformStack): string { case 'collapse-recursion': return `${shortKey}-${transform.funcIndex}`; case 'collapse-direct-recursion': + case 'focus-self': return `${shortKey}-${transform.implementation}-${transform.funcIndex}`; case 'focus-subtree': case 'merge-call-node': { @@ -444,6 +463,7 @@ export function getTransformLabelL10nIds( funcIndex = transform.callNodePath[transform.callNodePath.length - 1]; break; case 'focus-function': + case 'focus-self': case 'merge-function': case 'drop-function': case 'collapse-direct-recursion': @@ -462,6 +482,8 @@ export function getTransformLabelL10nIds( return { l10nId: 'TransformNavigator--focus-subtree', item: funcName }; case 'focus-function': return { l10nId: 'TransformNavigator--focus-function', item: funcName }; + case 'focus-self': + return { l10nId: 'TransformNavigator--focus-self', item: funcName }; case 'merge-call-node': return { l10nId: 'TransformNavigator--merge-call-node', @@ -511,6 +533,11 @@ export function applyTransformToCallNodePath( ); case 'focus-function': return _startCallNodePathWithFunction(transform.funcIndex, callNodePath); + case 'focus-self': + return _focusFunctionSelfInCallNodePath( + transform.funcIndex, + callNodePath + ); case 'focus-category': return _removeOtherCategoryFunctionsInNodePathWithFunction( transform.category, @@ -576,6 +603,14 @@ function _startCallNodePathWithFunction( return startIndex === -1 ? [] : callNodePath.slice(startIndex); } +function _focusFunctionSelfInCallNodePath( + funcIndex: IndexIntoFuncTable, + callNodePath: CallNodePath +): CallNodePath { + const containsFunc = callNodePath.indexOf(funcIndex) !== -1; + return containsFunc ? [funcIndex] : []; +} + function _mergeNodeInCallNodePath( prefixPath: CallNodePath, callNodePath: CallNodePath @@ -1443,6 +1478,53 @@ export function focusFunction( }); } +export function focusSelf( + thread: Thread, + funcIndexToFocus: IndexIntoFuncTable, + implementation: ImplementationFilter +): Thread { + return timeCode('focusSelf', () => { + const { stackTable, frameTable } = thread; + + const funcMatchesImplementation = FUNC_MATCHES[implementation]; + + const shouldKeepStack = new Uint8Array(stackTable.length); + + const newPrefixCol = stackTable.prefix.slice(); + + for (let stackIndex = 0; stackIndex < stackTable.length; stackIndex++) { + const frameIndex = stackTable.frame[stackIndex]; + const funcIndex = frameTable.func[frameIndex]; + + if (funcIndex === funcIndexToFocus) { + shouldKeepStack[stackIndex] = 1; + newPrefixCol[stackIndex] = null; + } else { + const prefix = newPrefixCol[stackIndex]; + if ( + prefix !== null && + shouldKeepStack[prefix] === 1 && + !funcMatchesImplementation(thread, funcIndex) + ) { + shouldKeepStack[stackIndex] = 1; + } + } + } + + const newStackTable = { + ...stackTable, + prefix: newPrefixCol, + }; + + return updateThreadStacks(thread, newStackTable, (oldStack) => { + if (oldStack === null || shouldKeepStack[oldStack] === 0) { + return null; + } + return oldStack; + }); + }); +} + export function focusCategory(thread: Thread, category: IndexIntoCategoryList) { return timeCode('focusCategory', () => { const { stackTable } = thread; @@ -1859,6 +1941,8 @@ export function applyTransform( return dropFunction(thread, transform.funcIndex); case 'focus-function': return focusFunction(thread, transform.funcIndex); + case 'focus-self': + return focusSelf(thread, transform.funcIndex, transform.implementation); case 'focus-category': return focusCategory(thread, transform.category); case 'collapse-resource': diff --git a/src/reducers/url-state.ts b/src/reducers/url-state.ts index bebe69468d..8903ea34b2 100644 --- a/src/reducers/url-state.ts +++ b/src/reducers/url-state.ts @@ -565,16 +565,25 @@ const profileName: Reducer = (state = null, action) => { }; const sourceView: Reducer = ( - state = { scrollGeneration: 0, libIndex: null, sourceIndex: null }, + state = { + scrollGeneration: 0, + libIndex: null, + sourceIndex: null, + highlightedLine: null, + }, action ) => { switch (action.type) { case 'UPDATE_BOTTOM_BOX': { + const shouldScroll = action.scrollToLineNumber !== undefined; return { - scrollGeneration: state.scrollGeneration + 1, + scrollGeneration: shouldScroll + ? state.scrollGeneration + 1 + : state.scrollGeneration, libIndex: action.libIndex, sourceIndex: action.sourceIndex, - lineNumber: action.lineNumber, + scrollToLineNumber: action.scrollToLineNumber, + highlightedLine: action.highlightedLineNumber, }; } default: @@ -584,21 +593,28 @@ const sourceView: Reducer = ( const assemblyView: Reducer = ( state = { - scrollGeneration: 0, - nativeSymbol: null, - allNativeSymbolsForInitiatingCallNode: [], isOpen: false, + scrollGeneration: 0, + highlightedInstruction: null, + nativeSymbols: [], + currentNativeSymbol: null, }, action ) => { switch (action.type) { case 'UPDATE_BOTTOM_BOX': { + const { nativeSymbols, currentNativeSymbol, shouldOpenAssemblyView } = + action; + const shouldScroll = action.scrollToInstructionAddress !== undefined; return { - scrollGeneration: state.scrollGeneration + 1, - nativeSymbol: action.nativeSymbol, - allNativeSymbolsForInitiatingCallNode: - action.allNativeSymbolsForInitiatingCallNode, - isOpen: state.isOpen || action.shouldOpenAssemblyView, + scrollGeneration: shouldScroll + ? state.scrollGeneration + 1 + : state.scrollGeneration, + nativeSymbols, + currentNativeSymbol, + isOpen: state.isOpen || shouldOpenAssemblyView, + scrollToInstructionAddress: action.scrollToInstructionAddress, + highlightedInstruction: action.highlightedInstructionAddress, }; } case 'OPEN_ASSEMBLY_VIEW': { @@ -607,6 +623,12 @@ const assemblyView: Reducer = ( isOpen: true, }; } + case 'CHANGE_ASSEMBLY_VIEW_NATIVE_SYMBOL_ENTRY_INDEX': { + return { + ...state, + currentNativeSymbol: action.entryIndex, + }; + } case 'CLOSE_ASSEMBLY_VIEW': { return { ...state, diff --git a/src/selectors/per-thread/index.ts b/src/selectors/per-thread/index.ts index df364eccb4..130c55a0a0 100644 --- a/src/selectors/per-thread/index.ts +++ b/src/selectors/per-thread/index.ts @@ -23,26 +23,13 @@ import { getComposedSelectorsPerThread, type ComposedSelectorsPerThread, } from './composed'; -import { - getStackLineInfoForCallNode, - getLineTimings, -} from '../../profile-logic/line-timings'; -import { - getStackAddressInfoForCallNode, - getAddressTimings, -} from '../../profile-logic/address-timings'; import * as ProfileSelectors from '../profile'; import { ensureExists, getFirstItemFromSet } from '../../utils/types'; import type { - Thread, ThreadIndex, Selector, ThreadsKey, - StackLineInfo, - LineTimings, - StackAddressInfo, - AddressTimings, State, } from 'firefox-profiler/types'; @@ -205,10 +192,6 @@ export type NodeSelectors = { readonly getIsJS: Selector; readonly getLib: Selector; readonly getTimingsForSidebar: Selector; - readonly getSourceViewStackLineInfo: Selector; - readonly getSourceViewLineTimings: Selector; - readonly getAssemblyViewStackAddressInfo: Selector; - readonly getAssemblyViewAddressTimings: Selector; }; export const selectedNodeSelectors: NodeSelectors = (() => { @@ -268,85 +251,10 @@ export const selectedNodeSelectors: NodeSelectors = (() => { ProfileData.getTimingsForPath ); - const getSourceViewStackLineInfo: Selector = - createSelector( - selectedThreadSelectors.getFilteredThread, - UrlState.getSourceViewSourceIndex, - selectedThreadSelectors.getCallNodeInfo, - selectedThreadSelectors.getSelectedCallNodeIndex, - ( - { stackTable, frameTable, funcTable }: Thread, - sourceViewSourceIndex, - callNodeInfo, - selectedCallNodeIndex - ): StackLineInfo | null => { - if (sourceViewSourceIndex === null || selectedCallNodeIndex === null) { - return null; - } - const selectedFunc = callNodeInfo.funcForNode(selectedCallNodeIndex); - const selectedSourceIndex = funcTable.source[selectedFunc]; - if ( - selectedSourceIndex === null || - selectedSourceIndex !== sourceViewSourceIndex - ) { - return null; - } - return getStackLineInfoForCallNode( - stackTable, - frameTable, - funcTable, - selectedCallNodeIndex, - callNodeInfo - ); - } - ); - - const getSourceViewLineTimings: Selector = createSelector( - getSourceViewStackLineInfo, - selectedThreadSelectors.getPreviewFilteredCtssSamples, - getLineTimings - ); - - const getAssemblyViewStackAddressInfo: Selector = - createSelector( - selectedThreadSelectors.getFilteredThread, - selectedThreadSelectors.getAssemblyViewNativeSymbolIndex, - selectedThreadSelectors.getCallNodeInfo, - selectedThreadSelectors.getSelectedCallNodeIndex, - ( - { stackTable, frameTable }: Thread, - nativeSymbolIndex, - callNodeInfo, - selectedCallNodeIndex - ): StackAddressInfo | null => { - if (nativeSymbolIndex === null || selectedCallNodeIndex === null) { - return null; - } - return getStackAddressInfoForCallNode( - stackTable, - frameTable, - selectedCallNodeIndex, - callNodeInfo, - nativeSymbolIndex - ); - } - ); - - const getAssemblyViewAddressTimings: Selector = - createSelector( - getAssemblyViewStackAddressInfo, - selectedThreadSelectors.getPreviewFilteredCtssSamples, - getAddressTimings - ); - return { getName, getIsJS, getLib, getTimingsForSidebar, - getSourceViewStackLineInfo, - getSourceViewLineTimings, - getAssemblyViewStackAddressInfo, - getAssemblyViewAddressTimings, }; })(); diff --git a/src/selectors/per-thread/stack-sample.ts b/src/selectors/per-thread/stack-sample.ts index 5b8b44b079..fa9944b9e3 100644 --- a/src/selectors/per-thread/stack-sample.ts +++ b/src/selectors/per-thread/stack-sample.ts @@ -340,6 +340,7 @@ export function getStackAndSampleSelectorsPerThread( threadSelectors.getFilteredThread, getCallNodeInfo, ProfileSelectors.getCategories, + threadSelectors.getPreviewFilteredCtssSamples, getCallTreeTimings, getWeightTypeForCallTree, ProfileSelectors.getSourceTable, @@ -350,12 +351,14 @@ export function getStackAndSampleSelectorsPerThread( threadSelectors.getFilteredThread, _getInvertedCallNodeInfo, ProfileSelectors.getCategories, + threadSelectors.getPreviewFilteredCtssSamples, getFunctionListTimings, getWeightTypeForCallTree, ( thread, callNodeInfoInverted, categories, + previewFilteredCtssSamples, functionListTimings, weightType ) => @@ -363,6 +366,7 @@ export function getStackAndSampleSelectorsPerThread( thread, callNodeInfoInverted, categories, + previewFilteredCtssSamples, { type: 'FUNCTION_LIST', timings: functionListTimings }, weightType ) diff --git a/src/selectors/url-state.ts b/src/selectors/url-state.ts index 48b7e17146..3045882da5 100644 --- a/src/selectors/url-state.ts +++ b/src/selectors/url-state.ts @@ -75,15 +75,37 @@ export const getSourceViewSourceIndex: Selector = ( ) => getProfileSpecificState(state).sourceView.sourceIndex; export const getSourceViewScrollGeneration: Selector = (state) => getProfileSpecificState(state).sourceView.scrollGeneration; -export const getSourceViewLineNumber: Selector = (state) => - getProfileSpecificState(state).sourceView.lineNumber; +export const getSourceViewScrollToLineNumber: Selector = ( + state +) => getProfileSpecificState(state).sourceView.scrollToLineNumber; +export const getSourceViewHighlightedLine: Selector = (state) => + getProfileSpecificState(state).sourceView.highlightedLine; export const getAssemblyViewIsOpen: Selector = (state) => getProfileSpecificState(state).assemblyView.isOpen; export const getAssemblyViewNativeSymbol: Selector = ( state -) => getProfileSpecificState(state).assemblyView.nativeSymbol; +) => { + const { nativeSymbols, currentNativeSymbol } = + getProfileSpecificState(state).assemblyView; + return currentNativeSymbol !== null + ? nativeSymbols[currentNativeSymbol] + : null; +}; +export const getAssemblyViewCurrentNativeSymbolEntryIndex: Selector< + number | null +> = (state) => getProfileSpecificState(state).assemblyView.currentNativeSymbol; +export const getAssemblyViewNativeSymbolEntryCount: Selector = ( + state +) => getProfileSpecificState(state).assemblyView.nativeSymbols.length; export const getAssemblyViewScrollGeneration: Selector = (state) => getProfileSpecificState(state).assemblyView.scrollGeneration; +export const getAssemblyViewScrollToInstructionAddress: Selector< + number | undefined +> = (state) => + getProfileSpecificState(state).assemblyView.scrollToInstructionAddress; +export const getAssemblyViewHighlightedInstruction: Selector = ( + state +) => getProfileSpecificState(state).assemblyView.highlightedInstruction; export const getShowJsTracerSummary: Selector = (state) => getProfileSpecificState(state).showJsTracerSummary; diff --git a/src/symbolicator-cli/index.ts b/src/symbolicator-cli/index.ts index 2e917c7a52..797c4674b8 100644 --- a/src/symbolicator-cli/index.ts +++ b/src/symbolicator-cli/index.ts @@ -23,54 +23,9 @@ import { applySymbolicationSteps, } from '../profile-logic/symbolication'; import type { SymbolicationStepInfo } from '../profile-logic/symbolication'; -import type { SymbolTableAsTuple } from '../profile-logic/symbol-store-db'; import * as MozillaSymbolicationAPI from '../profile-logic/mozilla-symbolication-api'; -import { SymbolsNotFoundError } from '../profile-logic/errors'; import type { ThreadIndex } from '../types'; -/** - * Simple 'in-memory' symbol DB that conforms to the same interface as SymbolStoreDB but - * just stores everything in a simple dictionary instead of IndexedDB. The composite key - * [debugName, breakpadId] is flattened to a string "debugName:breakpadId" to use as the - * map key. - */ -export class InMemorySymbolDB { - _store: Map; - - constructor() { - this._store = new Map(); - } - - _makeKey(debugName: string, breakpadId: string): string { - return `${debugName}:${breakpadId}`; - } - - async storeSymbolTable( - debugName: string, - breakpadId: string, - symbolTable: SymbolTableAsTuple - ): Promise { - this._store.set(this._makeKey(debugName, breakpadId), symbolTable); - } - - async getSymbolTable( - debugName: string, - breakpadId: string - ): Promise { - const key = this._makeKey(debugName, breakpadId); - const value = this._store.get(key); - if (typeof value !== 'undefined') { - return value; - } - throw new SymbolsNotFoundError( - 'The requested library does not exist in the database.', - { debugName, breakpadId } - ); - } - - async close(): Promise {} -} - export interface CliOptions { input: string; output: string; @@ -91,14 +46,12 @@ export async function run(options: CliOptions) { throw new Error('Unable to parse the profile.'); } - const symbolStoreDB = new InMemorySymbolDB(); - /** * SymbolStore implementation which just forwards everything to the symbol server in * MozillaSymbolicationAPI format. No support for getting symbols from 'the browser' as * there is no browser in this context. */ - const symbolStore = new SymbolStore(symbolStoreDB, { + const symbolStore = new SymbolStore({ requestSymbolsFromServer: async (requests) => { for (const { lib } of requests) { console.log(` Loading symbols for ${lib.debugName}`); @@ -126,7 +79,7 @@ export async function run(options: CliOptions) { return []; }, - requestSymbolTableFromBrowser: async () => { + requestSymbolsViaSymbolTableFromBrowser: async () => { throw new Error('Not supported in this context'); }, }); diff --git a/src/symbolicator-cli/webpack.config.js b/src/symbolicator-cli/webpack.config.js index 6e43c9b4f8..944b25a84b 100644 --- a/src/symbolicator-cli/webpack.config.js +++ b/src/symbolicator-cli/webpack.config.js @@ -12,6 +12,7 @@ module.exports = { output: { path: path.resolve(projectRoot, 'dist'), filename: 'symbolicator-cli.js', + asyncChunks: false, }, entry: './src/symbolicator-cli/index.ts', module: { diff --git a/src/test/components/BottomBox.test.tsx b/src/test/components/BottomBox.test.tsx index 41a04d023b..47e7d23e83 100644 --- a/src/test/components/BottomBox.test.tsx +++ b/src/test/components/BottomBox.test.tsx @@ -85,7 +85,7 @@ describe('BottomBox', () => { ); const { profile } = getProfileFromTextSamples(` - A[file:hg:hg.mozilla.org/mozilla-central:${filepath}:${revision}][line:4][address:30][sym:Asym:20:1a][lib:libA.so] + A[file:hg:hg.mozilla.org/mozilla-central:${filepath}:${revision}][line:4][address:30][sym:Asym:20:1a][lib:libA.so] A[file:hg:hg.mozilla.org/mozilla-central:${filepath}:${revision}][line:4][address:70][sym:A2sym:60:1a][lib:libA.so] B[file:git:github.com/rust-lang/rust:library/std/src/sys/unix/thread.rs:53cb7b09b00cbea8754ffb78e7e3cb521cb8af4b] C[lib:libC.so][file:s3:gecko-generated-sources:a5d3747707d6877b0e5cb0a364e3cb9fea8aa4feb6ead138952c2ba46d41045297286385f0e0470146f49403e46bd266e654dfca986de48c230f3a71c2aafed4/ipc/ipdl/PBackgroundChild.cpp:] D[lib:libD.so] @@ -180,4 +180,61 @@ describe('BottomBox', () => { expect(assemblyView()).not.toBeInTheDocument(); }); + + it('should navigate between symbols using prev/next buttons', async () => { + const { sourceView, assemblyView } = setup(); + + const frameElement = screen.getByRole('treeitem', { name: /^A/ }); + + fireFullClick(frameElement); + fireFullClick(frameElement, { detail: 2 }); + expect(sourceView()).toBeInTheDocument(); + + const asmViewShowButton = ensureExists( + document.querySelector('.bottom-assembly-button') + ) as HTMLElement; + fireFullClick(asmViewShowButton); + + expect(assemblyView()).toBeInTheDocument(); + + // Verify we're showing "1 of 2" + const titleTrailer = await screen.findByText( + '\u20681\u2069 of \u20682\u2069' + ); + expect(titleTrailer).toBeInTheDocument(); + + // Find the prev and next buttons + const prevButton = ensureExists( + document.querySelector('.bottom-prev-button') + ) as HTMLButtonElement; + const nextButton = ensureExists( + document.querySelector('.bottom-next-button') + ) as HTMLButtonElement; + + // Initially, prev should be disabled and next should be enabled + expect(prevButton).toBeDisabled(); + expect(nextButton).toBeEnabled(); + + // Click next to go to symbol 2 + fireFullClick(nextButton); + + // Now we should see "2 of 2" + expect( + await screen.findByText('\u20682\u2069 of \u20682\u2069') + ).toBeInTheDocument(); + + // Now prev should be enabled and next should be disabled + expect(prevButton).toBeEnabled(); + expect(nextButton).toBeDisabled(); + + // Click prev to go back to symbol 1 + fireFullClick(prevButton); + + // We should be back to "1 of 2" + expect( + await screen.findByText('\u20681\u2069 of \u20682\u2069') + ).toBeInTheDocument(); + expect(prevButton).toBeDisabled(); + expect(nextButton).toBeEnabled(); + }); }); diff --git a/src/test/components/FilterNavigatorBar.test.tsx b/src/test/components/FilterNavigatorBar.test.tsx index b572bddc12..22fe8cadea 100644 --- a/src/test/components/FilterNavigatorBar.test.tsx +++ b/src/test/components/FilterNavigatorBar.test.tsx @@ -128,4 +128,21 @@ describe('app/ProfileFilterNavigator', () => { const { getByText } = setup(); expect(getByText(/Full Range/)).toBeInTheDocument(); }); + + it('opens a menu when the first item is clicked', () => { + const { getByText } = setup(); + + const item = getByText(/Full Range/).closest('.filterNavigatorBarItem'); + + const listener = jest.fn(); + window.addEventListener('REACT_CONTEXTMENU_SHOW', listener); + fireEvent.click(item!); + window.removeEventListener('REACT_CONTEXTMENU_SHOW', listener); + + expect(listener).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'REACT_CONTEXTMENU_SHOW', + }) + ); + }); }); diff --git a/src/test/components/MarkerChart.test.tsx b/src/test/components/MarkerChart.test.tsx index c72a4e5951..d789a8f5e6 100644 --- a/src/test/components/MarkerChart.test.tsx +++ b/src/test/components/MarkerChart.test.tsx @@ -319,7 +319,7 @@ describe('MarkerChart', function () { // No tooltip displayed yet expect(document.querySelector('.tooltip')).toBeFalsy(); - function leftClick(pos: { x: CssPixels; y: CssPixels }) { + function leftClick(pos: { x: CssPixels; y: CssPixels }, dblClick = false) { const positioningOptions = { offsetX: pos.x, offsetY: pos.y, @@ -333,7 +333,7 @@ describe('MarkerChart', function () { // Because different components listen to different events, we trigger // all the right events, to be as close as possible to the real stuff. fireMouseEvent('mousemove', positioningOptions); - fireFullClick(canvas, positioningOptions); + fireFullClick(canvas, positioningOptions, dblClick); flushRafCalls(); } @@ -365,6 +365,20 @@ describe('MarkerChart', function () { // Now the tooltip should not be displayed. expect(document.querySelector('.tooltip')).toBeFalsy(); + + // The tooltip should be displayed also on double click. + leftClick(position, true); + + // Move the mouse outside of the marker. + fireMouseEvent('mousemove', { + offsetX: 0, + offsetY: 0, + pageX: 0, + pageY: 0, + }); + + // The double click shouldn't make the tooltip persisted. + expect(document.querySelector('.tooltip')).toBeFalsy(); }); it('only renders a single row when hovering', () => { diff --git a/src/test/components/MarkerTable.test.tsx b/src/test/components/MarkerTable.test.tsx index 2d381ff82a..9f9f16b6df 100644 --- a/src/test/components/MarkerTable.test.tsx +++ b/src/test/components/MarkerTable.test.tsx @@ -34,6 +34,7 @@ import { addMarkersToThreadWithCorrespondingSamples, addIPCMarkerPairToThreads, getNetworkTrackProfile, + getProfileWithMarkers, } from '../fixtures/profiles/processed-profile'; import { fireFullClick, fireFullContextMenu } from '../fixtures/utils'; import { autoMockElementSize } from '../fixtures/mocks/element-size'; @@ -515,6 +516,106 @@ describe('MarkerTable', function () { expect(firstColumn).toHaveStyle({ width: '90px' }); }); }); + + it('can copy the table as plain text', () => { + const { container } = setup(); + + const button = ensureExists( + container.querySelector('.copyTableButton') + ) as HTMLElement; + fireFullClick(button); + + const menu = ensureExists( + container.querySelector('.markerCopyTableContextMenu') + ) as HTMLElement; + + const items = menu.querySelectorAll('[role="menuitem"]'); + expect(items.length).toBe(2); + + fireFullClick(items[0] as HTMLElement); + + const pattern = new RegExp( + '^ +Start +Duration +Name +Details\\n +0s +0s +UserTiming +foobar\\n' + ); + expect(copy).toHaveBeenLastCalledWith(expect.stringMatching(pattern)); + }); + + it('can copy the table as markdown', () => { + const { container } = setup(); + + const button = ensureExists( + container.querySelector('.copyTableButton') + ) as HTMLElement; + fireFullClick(button); + + const menu = ensureExists( + container.querySelector('.markerCopyTableContextMenu') + ) as HTMLElement; + + const items = menu.querySelectorAll('[role="menuitem"]'); + expect(items.length).toBe(2); + + fireFullClick(items[1] as HTMLElement); + + const pattern = new RegExp( + '^\\| +Start +\\| +Duration +\\| +Name +\\| +Details +\\|\\n' + + '\\|-+:\\|-+:\\|-+:\\|-+\\|\\n' + + '\\| +0s +\\| +0s +\\| +UserTiming +\\| +foobar +\\|\\n' + ); + expect(copy).toHaveBeenLastCalledWith(expect.stringMatching(pattern)); + }); + + it('shows warning when copying 10001+ rows', () => { + jest.useFakeTimers(); + + const markers: TestDefinedMarker[] = []; + for (let i = 1; i < 10010; i++) { + markers.push([ + 'UserTiming', + i, + i, + { + type: 'UserTiming', + name: 'foobar', + entryType: 'mark', + }, + ]); + } + const profile = getProfileWithMarkers(markers); + const { container } = setup(profile); + + const button = ensureExists( + container.querySelector('.copyTableButton') + ) as HTMLElement; + fireFullClick(button); + + const menu = ensureExists( + container.querySelector('.markerCopyTableContextMenu') + ) as HTMLElement; + + const items = menu.querySelectorAll('[role="menuitem"]'); + expect(items.length).toBe(2); + + fireFullClick(items[0] as HTMLElement); + + const pattern = new RegExp( + '^ +Start +Duration +Name +Details\\n +0s +0s +UserTiming +foobar\\n.+9\\.999s +0s +UserTiming +foobar$', + 's' + ); + expect(copy).toHaveBeenLastCalledWith(expect.stringMatching(pattern)); + + const warning = screen.getByText( + 'The number of rows exceeds the limit: ⁨10,009⁩ > ⁨10,000⁩. Only the first ⁨10,000⁩ rows will be copied.' + ); + expect(warning).toBeInTheDocument(); + + act(() => jest.runAllTimers()); + + const warning2 = screen.queryByText( + 'The number of rows exceeds the limit: ⁨10,009⁩ > ⁨10,000⁩. Only the first ⁨10,000⁩ rows will be copied.' + ); + expect(warning2).not.toBeInTheDocument(); + }); }); function getReflowMarker( diff --git a/src/test/components/MenuButtons.test.tsx b/src/test/components/MenuButtons.test.tsx index 9d38bc26ca..18b0d562ca 100644 --- a/src/test/components/MenuButtons.test.tsx +++ b/src/test/components/MenuButtons.test.tsx @@ -81,6 +81,7 @@ beforeEach(() => { import { shortenUrl } from '../../utils/shorten-url'; jest.mock('../../utils/shorten-url'); +import { compress } from 'firefox-profiler/utils/gz'; import { symbolicateProfile } from 'firefox-profiler/profile-logic/symbolication'; jest.mock('firefox-profiler/profile-logic/symbolication'); @@ -161,6 +162,12 @@ describe('app/MenuButtons', function () { } describe('', function () { + const FIXED_GZIP_BYTES = 1580; + + beforeEach(() => { + (compress as any).mockResolvedValue(new Uint8Array(FIXED_GZIP_BYTES)); + }); + function mockUpload() { // Create a promise with the resolve function outside of it. // const { promise, resolve: resolveUpload, reject: rejectUpload } = Promise.withResolvers(); diff --git a/src/test/components/TooltipMarker.test.tsx b/src/test/components/TooltipMarker.test.tsx index 2bb92521f0..938a5bc361 100644 --- a/src/test/components/TooltipMarker.test.tsx +++ b/src/test/components/TooltipMarker.test.tsx @@ -1333,6 +1333,49 @@ describe('TooltipMarker', function () { }); describe('filter button', () => { + function setupFilterButton(hideFilterButton: boolean) { + const profile = getProfileWithMarkers([ + ['Reflow', 1, 2, { type: 'tracing', category: 'Paint' }], + ]); + const store = storeWithProfile(profile); + const state = store.getState(); + const getMarker = selectedThreadSelectors.getMarkerGetter(state); + const markerIndexes = + selectedThreadSelectors.getFullMarkerListIndexes(state); + + render( + + + + ); + } + + it('hides the filter button when hideFilterButton is true', () => { + setupFilterButton(true); + + expect( + screen.queryByRole('button', { + name: /only show markers matching:/i, + }) + ).not.toBeInTheDocument(); + }); + + it('shows the filter button when hideFilterButton is false', () => { + setupFilterButton(false); + + expect( + screen.getByRole('button', { + name: /only show markers matching:/i, + }) + ).toBeInTheDocument(); + }); + it('shows the filter button for markers without spaces in the label', () => { // Tooltip label: "Reflow" const profile = getProfileWithMarkers([ diff --git a/src/test/components/Viewport.test.tsx b/src/test/components/Viewport.test.tsx index 6bbf9a9c32..b32f27be94 100644 --- a/src/test/components/Viewport.test.tsx +++ b/src/test/components/Viewport.test.tsx @@ -2,11 +2,16 @@ * 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 { Provider } from 'react-redux'; import { fireEvent } from '@testing-library/react'; import { render, act } from 'firefox-profiler/test/fixtures/testing-library'; -import { withChartViewport } from '../../components/shared/chart/Viewport'; +import { + withChartViewport, + type Viewport, +} from '../../components/shared/chart/Viewport'; +import { ChartCanvas } from '../..//components/shared/chart/Canvas'; import { getCommittedRange, getPreviewSelection, @@ -27,6 +32,7 @@ import { storeWithProfile } from '../fixtures/stores'; import { getProfileFromTextSamples } from '../fixtures/profiles/processed-profile'; import type { + MarkerIndex, Milliseconds, MixedObject, PreviewSelection, @@ -614,6 +620,24 @@ describe('Viewport', function () { viewportBottom: BOUNDING_BOX_HEIGHT, }); }); + + it('does not change the canvas size unnecessarily', () => { + const { container, performRerender, flushRafCalls } = + setupWithSimpleCanvas(); + + const canvas = container.querySelector('canvas'); + expect(canvas!.width).toBe(BOUNDING_BOX_WIDTH); + + // Just re-rendering shouldn't change it. + performRerender(); + expect(canvas!.width).toBe(BOUNDING_BOX_WIDTH); + + // The size re-calculation done by Viewport happens asynchronously, + // with containerWidth temporary being 0. + // This shouldn't change the canvas size. + flushRafCalls(); + expect(canvas!.width).toBe(BOUNDING_BOX_WIDTH); + }); }); function setup(profileOverrides: MixedObject = {}) { @@ -759,3 +783,118 @@ function setup(profileOverrides: MixedObject = {}) { dispatch: store.dispatch, }; } + +// A variant with simple ChartCanvas wrapper, used for testing the +// viewportNeedsUpdate handling. +function setupWithSimpleCanvas() { + const realFlushRafCalls = mockRaf(); + const flushRafCalls = (options?: { timestamps: number[]; once?: boolean }) => + act(() => realFlushRafCalls(options)); + + type SimpleCanvasOwnProps = {}; + type SimpleCanvasProps = SimpleCanvasOwnProps & { + readonly viewport: Viewport; + }; + + class SimpleChartCanvasImpl extends React.PureComponent { + _onDoubleClickMarker = () => {}; + _getHoveredMarkerInfo = (): React.ReactNode => { + return null; + }; + _drawCanvas = () => {}; + _hitTest = (): MarkerIndex | null => { + return null; + }; + override render() { + const { containerWidth, containerHeight, isDragging } = + this.props.viewport; + + return ( + + ); + } + } + + const ChartWithViewport = withChartViewport( + SimpleChartCanvasImpl + ); + + type OwnProps = { + charGeneration: number; + }; + + type StateProps = { + previewSelection: PreviewSelection | null; + timeRange: StartEndRange; + }; + + type Props = OwnProps & StateProps; + + let charGeneration = 0; + + // The viewport component started out as an unconnected component, but then it + // started subscribing to the store an dispatching its own actions. This migration + // wasn't completely done, so it still has a few pieces of state passed in through + // its OwnProps. In order to ensure that the component is consistent, make it + // a connected component. + const ConnectedChartWithViewport = explicitConnect({ + mapStateToProps: (state: State) => ({ + timeRange: getCommittedRange(state), + previewSelection: getPreviewSelection(state), + }), + mapDispatchToProps: {}, + component: (props: Props) => ( + true, + marginLeft: 0, + marginRight: 0, + maximumZoom: MAXIMUM_ZOOM, + }} + chartProps={{}} + /> + ), + }); + + const store = storeWithProfile(getProfileFromTextSamples('A').profile); + + const renderResult = render( + + + + ); + const { rerender } = renderResult; + + const performRerender = () => { + charGeneration++; + rerender( + + + + ); + }; + + // WithSize uses requestAnimationFrame. + flushRafCalls(); + + return { + ...renderResult, + performRerender, + flushRafCalls, + dispatch: store.dispatch, + }; +} diff --git a/src/test/components/__snapshots__/BottomBox.test.tsx.snap b/src/test/components/__snapshots__/BottomBox.test.tsx.snap index 4111dd9a97..a4bc6089ff 100644 --- a/src/test/components/__snapshots__/BottomBox.test.tsx.snap +++ b/src/test/components/__snapshots__/BottomBox.test.tsx.snap @@ -50,7 +50,7 @@ exports[`BottomBox should show the assembly view when pressing the toggle button mov ebx, edi
and ebx, 0x1400
@@ -120,7 +120,7 @@ exports[`BottomBox should show the source view when a line in the tree view is d
+
- - - + + `; diff --git a/src/test/components/__snapshots__/FlameGraph.test.tsx.snap b/src/test/components/__snapshots__/FlameGraph.test.tsx.snap index 7070e5dfa6..ee201e3c8d 100644 --- a/src/test/components/__snapshots__/FlameGraph.test.tsx.snap +++ b/src/test/components/__snapshots__/FlameGraph.test.tsx.snap @@ -415,48 +415,57 @@ exports[`FlameGraph matches the snapshot 1`] = `
  • - - -
  • @@ -588,7 +597,7 @@ Array [ ], Array [ "set fillStyle", - "#000", + "#000000", ], Array [ "fillText", @@ -613,7 +622,7 @@ Array [ ], Array [ "set fillStyle", - "#000", + "#000000", ], Array [ "fillText", @@ -638,7 +647,7 @@ Array [ ], Array [ "set fillStyle", - "#000", + "#000000", ], Array [ "fillText", @@ -663,7 +672,7 @@ Array [ ], Array [ "set fillStyle", - "#000", + "#000000", ], Array [ "fillText", @@ -688,7 +697,7 @@ Array [ ], Array [ "set fillStyle", - "#000", + "#000000", ], Array [ "fillText", @@ -713,7 +722,7 @@ Array [ ], Array [ "set fillStyle", - "#000", + "#000000", ], Array [ "fillText", @@ -738,7 +747,7 @@ Array [ ], Array [ "set fillStyle", - "#000", + "#000000", ], Array [ "fillText", @@ -763,7 +772,7 @@ Array [ ], Array [ "set fillStyle", - "#000", + "#000000", ], Array [ "fillText", @@ -788,7 +797,7 @@ Array [ ], Array [ "set fillStyle", - "#000", + "#000000", ], Array [ "fillText", @@ -813,7 +822,7 @@ Array [ ], Array [ "set fillStyle", - "#000", + "#000000", ], Array [ "fillText", diff --git a/src/test/components/__snapshots__/FooterLinks.test.tsx.snap b/src/test/components/__snapshots__/FooterLinks.test.tsx.snap index e60188a158..3e349cdd5f 100644 --- a/src/test/components/__snapshots__/FooterLinks.test.tsx.snap +++ b/src/test/components/__snapshots__/FooterLinks.test.tsx.snap @@ -122,6 +122,11 @@ exports[`correctly renders the FooterLinks component 1`] = ` > Svenska +
    +
    + +
    Filter stacks: - - - +
    +
    + +
    matches the snapshot for the opened panel for class="menuButtonsDownloadSize" > ( - 1.56 kB + 1.58 kB ) diff --git a/src/test/components/__snapshots__/ProfileCallTreeView.test.tsx.snap b/src/test/components/__snapshots__/ProfileCallTreeView.test.tsx.snap index 0789545904..937030aa27 100644 --- a/src/test/components/__snapshots__/ProfileCallTreeView.test.tsx.snap +++ b/src/test/components/__snapshots__/ProfileCallTreeView.test.tsx.snap @@ -16,48 +16,57 @@ exports[`ProfileCallTreeView with JS Allocations matches the snapshot for JS all
  • - - -
  • - - -
  • - - -
  • - - -
  • - - -
  • - - -
  • - - -
  • - - -
  • - - -
  • - - -
  • - - -
  • - - -
  • - - -
  • - - -
  • - - -
  • - - -
  • - - -
  • - - -
  • - - -
  • - - -
  • - - -
  • - - -
  • - - -
  • Marker -
  • diff --git a/src/test/components/__snapshots__/TrackNetwork.test.tsx.snap b/src/test/components/__snapshots__/TrackNetwork.test.tsx.snap index 80f9a8118f..654cfcd788 100644 --- a/src/test/components/__snapshots__/TrackNetwork.test.tsx.snap +++ b/src/test/components/__snapshots__/TrackNetwork.test.tsx.snap @@ -61,12 +61,6 @@ exports[`timeline/TrackNetwork draws differently a request and displays a toolti > Load 0: https://mozilla.org -