diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 3656a3a659..0000000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,132 +0,0 @@ -version: 2.1 - -executors: - node: - docker: - - image: cimg/node:22.14 - base: - docker: - - image: cimg/base:stable - -orbs: - shellcheck: circleci/shellcheck@2.0.0 - -commands: - checkout-and-dependencies: - description: 'Checkout and install dependencies, managing a cache' - steps: - - checkout - - restore_cache: - keys: - - v2-dependencies-{{ checksum "package.json" }}-{{ checksum "yarn.lock" }} - - v2-dependencies-{{ checksum "package.json" }} - - v2-dependencies- - # With --frozen-lockfile, the installation will fail if the lockfile is - # outdated compared to package.json. - - run: yarn install --frozen-lockfile - - save_cache: - paths: - - node_modules - key: v2-dependencies-{{ checksum "package.json" }}-{{ checksum "yarn.lock" }} - -# Important: if you add a job here, don't forget to add it to the workflow below too. -jobs: - lint: - executor: node - resource_class: large - steps: - - checkout-and-dependencies - - run: yarn lint - - tests: - executor: node - resource_class: large - steps: - - checkout-and-dependencies - # We use workerIdleMemoryLimit to work around a memory issue with node. - # See https://github.com/facebook/jest/issues/11956 - - run: yarn test --coverage --logHeapUsage -w=4 --workerIdleMemoryLimit=1.5G - - run: yarn codecov - - build-prod: - executor: node - resource_class: large - steps: - - checkout-and-dependencies - - run: yarn build-prod:quiet - - run: yarn build-symbolicator-cli:quiet - - licence-check: - executor: node - steps: - - checkout-and-dependencies - - run: yarn license-check - - typecheck: - executor: node - resource_class: large - steps: - - checkout-and-dependencies - - run: yarn ts - - alex: - executor: node - steps: - - checkout-and-dependencies - - run: yarn test-alex - - yarn_lock: - executor: node - steps: - - checkout-and-dependencies - - run: yarn test-lockfile - - # This is implemented as a separate job instead of using the orb's predefined - # job so that we can have a more descriptive name when reported to github. - # See also https://github.com/CircleCI-Public/shellcheck-orb/issues/29 - shellcheck: - executor: base - steps: - - checkout - - shellcheck/install - - shellcheck/check: - dir: ./bin - - l10n-sync: - executor: node - steps: - - add_ssh_keys: - fingerprints: - - '92:e1:5d:84:70:96:c5:19:76:55:1c:b1:7a:12:9e:53' - - checkout - - run: git config user.email "perf-html@mozilla.com" - - run: git config user.name "Firefox Profiler [bot]" - - run: node ./bin/l10n-sync.js -y - -workflows: - version: 2 - main: - jobs: - - tests - - lint - - build-prod - - typecheck - - licence-check - - alex - - yarn_lock - - shellcheck - - l10n-sync: - triggers: - - schedule: - # CircleCI is using UTC timezone. So, this will be triggered at 8 AM UTC every day. - cron: '0 8 * * *' - filters: - branches: - only: - - main - jobs: - - tests - - l10n-sync: - requires: - - tests diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000000..5257f37474 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,31 @@ +{ + "name": "Firefox Profiler", + "image": "mcr.microsoft.com/devcontainers/javascript-node:22", + "features": { + "ghcr.io/devcontainers/features/node:1": { + "version": "22" + } + }, + "forwardPorts": [4242], + "portsAttributes": { + "4242": { + "label": "Firefox Profiler", + "onAutoForward": "openBrowser" + } + }, + "postCreateCommand": "yarn install", + "postStartCommand": "FX_PROFILER_HOST=\"0.0.0.0\" yarn start", + "customizations": { + "vscode": { + "extensions": [ + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "stylelint.vscode-stylelint" + ], + "settings": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode" + } + } + } +} diff --git a/.github/actions/setup-node-and-install/action.yml b/.github/actions/setup-node-and-install/action.yml new file mode 100644 index 0000000000..85a3a8ea8e --- /dev/null +++ b/.github/actions/setup-node-and-install/action.yml @@ -0,0 +1,14 @@ +name: 'Setup Node.js and Install Dependencies' +description: 'Setup Node.js with caching and install dependencies' +runs: + using: 'composite' + steps: + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: '22.14' + cache: 'yarn' + + - name: Install dependencies + shell: bash + run: yarn install --frozen-lockfile diff --git a/.github/fluent/linter-config.yml b/.github/fluent/linter-config.yml new file mode 100644 index 0000000000..6ef41ea839 --- /dev/null +++ b/.github/fluent/linter-config.yml @@ -0,0 +1,24 @@ +# 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/. + +# See https://github.com/mozilla-l10n/moz-fluent-linter/blob/main/src/fluent_linter/config.yml +# for details + +--- +# Check brand names +CO01: + enabled: true + brands: + - Firefox + - Mozilla + - Profiler + exclusions: + files: [] + messages: [] +# Enforce variable comments +VC: + disabled: false +# Enforce placeholder style, e.g. { $variable } +PS01: + disabled: false diff --git a/.github/fluent/requirements.txt b/.github/fluent/requirements.txt new file mode 100644 index 0000000000..4b9da3204d --- /dev/null +++ b/.github/fluent/requirements.txt @@ -0,0 +1 @@ +moz-fluent-linter~=0.4.9 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000000..7a91be5dc9 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,114 @@ +name: CI + +on: + push: + branches: + - main + - production + pull_request: + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Setup Node.js and install dependencies + uses: ./.github/actions/setup-node-and-install + + - name: Run lint + run: yarn lint + + tests: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Setup Node.js and install dependencies + uses: ./.github/actions/setup-node-and-install + + - name: Run tests + # We use workerIdleMemoryLimit to work around a memory issue with node. + # See https://github.com/facebook/jest/issues/11956 + run: yarn test --coverage --logHeapUsage -w=4 --workerIdleMemoryLimit=1.5G + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 + with: + fail_ci_if_error: false + + build-prod: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Setup Node.js and install dependencies + uses: ./.github/actions/setup-node-and-install + + - name: Build production + run: yarn build-prod:quiet + + - name: Build symbolicator CLI + run: yarn build-symbolicator-cli:quiet + + licence-check: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Setup Node.js and install dependencies + uses: ./.github/actions/setup-node-and-install + + - name: Run license check + run: yarn license-check + + typecheck: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Setup Node.js and install dependencies + uses: ./.github/actions/setup-node-and-install + + - name: Run TypeScript check + run: yarn ts + + alex: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Setup Node.js and install dependencies + uses: ./.github/actions/setup-node-and-install + + - name: Run alex + run: yarn test-alex + + yarn-lock: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Setup Node.js and install dependencies + uses: ./.github/actions/setup-node-and-install + + - name: Check yarn.lock + run: yarn test-lockfile + + shellcheck: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Run ShellCheck + uses: ludeeus/action-shellcheck@master + with: + scandir: './bin' diff --git a/.github/workflows/fluent-linter.yml b/.github/workflows/fluent-linter.yml new file mode 100644 index 0000000000..e9c53fc9d6 --- /dev/null +++ b/.github/workflows/fluent-linter.yml @@ -0,0 +1,35 @@ +name: Lint Reference Files +on: + push: + paths: + - 'locales/en-US/*.ftl' + - '.github/workflows/fluent-linter.yml' + - '.github/fluent/*' + branches: + - main + pull_request: + paths: + - 'locales/en-US/*.ftl' + - '.github/workflows/fluent_linter.yml' + - '.github/fluent/*' + branches: + - main + workflow_dispatch: +jobs: + linter: + runs-on: ubuntu-latest + steps: + - name: Clone repository + uses: actions/checkout@v5 + - name: Set up Python 3 + uses: actions/setup-python@v6 + with: + python-version: '3.11' + cache: 'pip' + cache-dependency-path: '.github/fluent/requirements.txt' + - name: Install Python dependencies + run: | + pip install -r .github/fluent/requirements.txt + - name: Lint reference + run: | + moz-fluent-lint ./locales/en-US --config .github/fluent/linter-config.yml diff --git a/.github/workflows/l10n-sync.yml b/.github/workflows/l10n-sync.yml new file mode 100644 index 0000000000..b38183aea0 --- /dev/null +++ b/.github/workflows/l10n-sync.yml @@ -0,0 +1,47 @@ +name: L10n Sync + +on: + schedule: + # Runs at 8 AM UTC every day + - cron: '0 8 * * *' + workflow_dispatch: + +jobs: + tests: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Setup Node.js and install dependencies + uses: ./.github/actions/setup-node-and-install + + - name: Run tests + run: yarn test --logHeapUsage -w=4 + + l10n-sync: + runs-on: ubuntu-latest + needs: tests + if: github.ref == 'refs/heads/main' + steps: + - name: Checkout code + uses: actions/checkout@v5 + with: + ssh-key: ${{ secrets.L10N_SYNC_SSH_KEY }} + # Fetch the full git history since we are going to need it to do some + # git operations between the main and l10n branches. + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: '22.14' + cache: 'yarn' + + - name: Configure git + run: | + git config user.email "perf-html@mozilla.com" + git config user.name "Firefox Profiler [bot]" + + - name: Run l10n sync + run: node ./bin/l10n-sync.js -y diff --git a/.gitpod.yml b/.gitpod.yml deleted file mode 100644 index 732a10cc35..0000000000 --- a/.gitpod.yml +++ /dev/null @@ -1,8 +0,0 @@ -ports: - - port: 4242 -tasks: - - before: nvm install 22 - init: yarn install - command: FX_PROFILER_HOST="0.0.0.0" yarn start - - openMode: split-right - command: printf "\nFirefox Profiler ❤ Gitpod\nWelcome to this virtual environment.\nYou can type your commands in this terminal.\n\n" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c4f2fadf68..b7389c64cc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -52,13 +52,11 @@ module.exports = function (config, serverConfig) { This project uses [TypeScript](https://www.typescriptlang.org/). -## Using Gitpod +## Using GitHub Codespaces -Alternatively, you can also develop the Firefox Profiler online in a pre-configured development environment: +Alternatively, you can also develop the Firefox Profiler online in a pre-configured development environment using [GitHub Codespaces](https://github.com/features/codespaces). -[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/firefox-devtools/profiler) - -Gitpod will automatically install all dependencies; start the webpack server for you; and open the web app in a new browser tab. Please look at our [gitpod documentation](./docs-user/gitpod.md) for more information. +GitHub Codespaces will automatically install all dependencies, start the webpack server for you, and forward port 4242 so you can access the web app. Please look at our [GitHub Codespaces documentation](./docs-developer/codespaces.md) for more information. ## Loading in profiles for development @@ -93,7 +91,7 @@ When working on a new feature and code changes, it's important that things work - We have [husky](https://www.npmjs.com/package/husky) installed to run automated checks when committing and pushing. - Run git commands with `--no-verify` to skip this step. This is useful for submitting broken PRs for feedback. - Continuous integration for pull requests - - We use CircleCI to run our tests for every PR that is submitted. This gives reviewers a great way to know if things are still working as expected. + - We use GitHub Actions to run our tests for every PR that is submitted. This gives reviewers a great way to know if things are still working as expected. ### Updating snapshots diff --git a/README.md b/README.md index 8aabced865..1b425401a2 100644 --- a/README.md +++ b/README.md @@ -43,22 +43,20 @@ yarn start This project uses [TypeScript](https://www.typescriptlang.org/). -You can also develop the Firefox Profiler online in a pre-configured development environment. +You can also develop the Firefox Profiler online in a pre-configured development environment using [GitHub Codespaces](https://github.com/features/codespaces). -[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/firefox-devtools/profiler) - -Please look at our [gitpod documentation](./docs-user/gitpod.md) for more information. +Please look at our [GitHub Codespaces documentation](./docs-developer/codespaces.md) for more information. For more detailed information on getting started contributing. We have plenty of docs available to get you started. -| | | -| -------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | -| [Contributing](./CONTRIBUTING.md) | Find out in detail how to get started and get your local development environment configured. | -| [Code of Conduct](./CODE_OF_CONDUCT.md) | We want to create an open and inclusive community, we have a few guidelines to help us out. | -| [Developer Documentation](./docs-developer) | Want to know how this whole thing works? Get started here. | -| [Source Files](./src) | Dive into the inner workings of the code. Most folders have a `README.md` providing more information. | -| [End-User Documentation](https://profiler.firefox.com/docs/#/) | These docs are customized for actual users of the profiler, not just folks contributing. | -| [Gitpod documentatation](./docs-user/gitpod.md) | Start here if you want to set up a work space on gitpod. | +| | | +| ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | +| [Contributing](./CONTRIBUTING.md) | Find out in detail how to get started and get your local development environment configured. | +| [Code of Conduct](./CODE_OF_CONDUCT.md) | We want to create an open and inclusive community, we have a few guidelines to help us out. | +| [Developer Documentation](./docs-developer) | Want to know how this whole thing works? Get started here. | +| [Source Files](./src) | Dive into the inner workings of the code. Most folders have a `README.md` providing more information. | +| [End-User Documentation](https://profiler.firefox.com/docs/#/) | These docs are customized for actual users of the profiler, not just folks contributing. | +| [GitHub Codespaces documentation](./docs-developer/codespaces.md) | Start here if you want to set up a workspace on GitHub Codespaces. | ### Discussion @@ -68,7 +66,7 @@ Say hello on Matrix in the [_Firefox Profiler_ channel (_#profiler:mozilla.org_) [MPL v2](./LICENSE) is designed to encourage contributors to share modifications they make to the code, while still allowing them to combine the code with code under other licenses (open or proprietary) with minimal restrictions. -We are very grateful to the the **zlib compression library (Jean-loup Gailly, Mark Adler and team)** for their contribution to the project. +We are very grateful to the **zlib compression library (Jean-loup Gailly, Mark Adler and team)** for their contribution to the project. [matrix]: https://chat.mozilla.org/#/room/#profiler:mozilla.org diff --git a/bin/l10n-sync.js b/bin/l10n-sync.js index 58d59d2c57..3f88f5baeb 100644 --- a/bin/l10n-sync.js +++ b/bin/l10n-sync.js @@ -12,19 +12,16 @@ const cp = require('child_process'); const readline = require('readline'); const { promisify } = require('util'); -/*:: - type ExecFilePromiseResult = {| - stdout: string | Buffer, - stderr: string | Buffer - |}; - - type ExecFile = ( - command: string, - args?: string[] - ) => Promise; -*/ +/** + * @typeef {Object} ExecFilePromiseResult + * @property {string | Buffer} stdout + * @property {string | Buffer} stderr + */ -const execFile /*: ExecFile */ = promisify(cp.execFile); +/** + * @type {(command: string, args?: string[]) => Promise} + */ +const execFile = promisify(cp.execFile); const DATE_FORMAT = new Intl.DateTimeFormat('en-US', { month: 'long', @@ -39,12 +36,12 @@ const MERGE_COMMIT_MESSAGE = '🔃 Daily sync: main -> l10n'; * Logs the command to be executed first, and spawns a shell then executes the * command. Returns the stdout of the executed command. * + * @param {string} executable + * @param {...string} args + * @returns {Promise} * @throws Will throw an error if executed command fails. */ -async function logAndExec( - executable /*: string */, - ...args /*: string[] */ -) /*: Promise */ { +async function logAndExec(executable, ...args) { console.log('[exec]', executable, args.join(' ')); const result = await execFile(executable, args); @@ -64,9 +61,11 @@ async function logAndExec( * and pipes the stdout of them to the next one. In the end, returns the stdout * of the last piped command. * + * @param {...string[]} commands + * @returns {string} * @throws Will throw an error if one of the executed commands fails. */ -function logAndPipeExec(...commands /*: string[][] */) /*: string */ { +function logAndPipeExec(...commands) { console.log( '[exec]', commands.map((command) => command.join(' ')).join(' | ') @@ -89,11 +88,12 @@ function logAndPipeExec(...commands /*: string[][] */) /*: string */ { /** * Pause with a message and wait for the enter as a confirmation. * The prompt will not be displayed if the `-y` argument is given to the script. - * This is mainly used by the CircleCI automation. + * This is mainly used by the GitHub Actions automation. + * + * @param {string} [message=''] + * @returns {Promise} */ -async function pauseWithMessageIfNecessary( - message /*: string */ = '' -) /*: Promise */ { +async function pauseWithMessageIfNecessary(message = '') { if (SKIP_PROMPTS) { return; } @@ -115,9 +115,10 @@ async function pauseWithMessageIfNecessary( /** * Check if Git workspace is clean. * + * @returns {Promise} * @throws Will throw an error if workspace is not clean. */ -async function checkIfWorkspaceClean() /*: Promise */ { +async function checkIfWorkspaceClean() { console.log('>>> Checking if the workspace is clean for the operations.'); // git status --porcelain --ignore-submodules -unormal const statusResult = await logAndExec( @@ -140,9 +141,10 @@ async function checkIfWorkspaceClean() /*: Promise */ { /** * Finds the Git upstream remote and returns it. * + * @returns {Promise} * @throws Will throw an error if it can't find an upstream remote. */ -async function findUpstream() /*: Promise */ { +async function findUpstream() { console.log('>>> Finding the upstream remote.'); try { const gitRemoteResult = await logAndExec('git', 'remote', '-v'); @@ -178,19 +180,21 @@ async function findUpstream() /*: Promise */ { * Fails if the `compareBranch` has changes from the files that doesn't match * the `allowedRegexp`. * + * @param {Object} options + * @param {string} options.upstream + * @param {string} options.compareBranch + * @param {string} options.baseBranch + * @param {RegExp} options.allowedRegexp + * @returns {Promise} * @throws Will throw an error if `compareBranch` has changes from the files * that doesn't match the `allowedRegexp`. */ -async function checkAllowedPaths( - { upstream, compareBranch, baseBranch, allowedRegexp } /*: - {| - upstream: string, - compareBranch: string, - baseBranch: string , - allowedRegexp: RegExp - |} - */ -) { +async function checkAllowedPaths({ + upstream, + compareBranch, + baseBranch, + allowedRegexp, +}) { console.log( `>>> Checking if ${compareBranch} branch has changes from the files that are not allowed.` ); @@ -224,8 +228,11 @@ async function checkAllowedPaths( * It's a pretty simple hack and would be good to have a more sophisticated * (localized?) API function. But it's not really worth for a deployment only * script. + * + * @param {number} count + * @returns {string} */ -function fewTimes(count /*: number */) /*: string */ { +function fewTimes(count) { switch (count) { case 1: return 'once'; @@ -239,9 +246,11 @@ function fewTimes(count /*: number */) /*: string */ { /** * Tries to sync the l10n branch and retries for 3 times if it fails to sync. * + * @param {string} upstream + * @returns {Promise} * @throws Will throw an error if it fails to sync for more than 3 times. */ -async function tryToSync(upstream /*: string */) /*: Promise */ { +async function tryToSync(upstream) { console.log('>>> Syncing the l10n branch with main.'); // RegExp for matching only the vendored locales. // It matches the files in `locales` directory but excludes `en-US` which is the @@ -268,7 +277,8 @@ async function tryToSync(upstream /*: string */) /*: Promise */ { // changes and try again. Nevertheless, we should have a hard cap on the try // count for safety. const totalTryCount = 3; - let error /*: Error | null */ = null; + /** @type {Error | null} */ + let error = null; let tryCount = 0; // Try to sync and retry for `totalTryCount` times if it fails. @@ -341,9 +351,10 @@ async function tryToSync(upstream /*: string */) /*: Promise */ { /** * Main function to be executed in the global scope. * + * @returns {Promise} * @throws Will throw an error if any of the functions it calls throw. */ -async function main() /*: Promise */ { +async function main() { const args = process.argv.slice(2); if (args.includes('-y')) { @@ -363,8 +374,13 @@ async function main() /*: Promise */ { console.log('>>> Done!'); } -main().catch((error /*: Error */) => { - // Print the error to the console and exit if an error is caught. - console.error(error); - process.exitCode = 1; -}); +main().catch( + /** + * @param {Error} error + */ + (error) => { + // Print the error to the console and exit if an error is caught. + console.error(error); + process.exitCode = 1; + } +); diff --git a/bin/pre-install.js b/bin/pre-install.js index 2395c2a303..10cf9c7920 100644 --- a/bin/pre-install.js +++ b/bin/pre-install.js @@ -98,15 +98,18 @@ function checkYarn(agents /*: AgentsVersion */) { } function parseExpectedNodeVersion() { - // Let's fetch our minimal version from circleci's file + // Let's fetch our minimal version from GitHub Actions composite action file const fs = require('fs'); - const circleConfig = fs.readFileSync('.circleci/config.yml', { - encoding: 'utf8', - }); - const expectedNodeVersion = /image: cimg\/node:([\d.]+)/.exec(circleConfig); + const actionConfig = fs.readFileSync( + '.github/actions/setup-node-and-install/action.yml', + { + encoding: 'utf8', + } + ); + const expectedNodeVersion = /node-version:\s*'([\d.]+)'/.exec(actionConfig); if (!expectedNodeVersion) { throw new Error( - `Couldn't extract the node version from .circleci/config.yml.` + `Couldn't extract the node version from .github/actions/setup-node-and-install/action.yml.` ); } return expectedNodeVersion[1]; diff --git a/docs-developer/codespaces.md b/docs-developer/codespaces.md new file mode 100644 index 0000000000..79d87c4159 --- /dev/null +++ b/docs-developer/codespaces.md @@ -0,0 +1,64 @@ +# Setting up profiler on GitHub Codespaces + +Instead of configuring a local setup, you can also use [GitHub Codespaces](https://github.com/features/codespaces), a cloud-based development environment with minimal setup. + +## Getting Started + +You can create a new Codespace directly from the repository: + +1. Navigate to the [Firefox Profiler repository](https://github.com/firefox-devtools/profiler) +2. Click the green `Code` button +3. Click on the `Codespaces` tab +4. Click `Create codespace on main` (or on your branch) + +GitHub will automatically: + +- Create a new development environment +- Install all dependencies using `yarn install` +- Start the development server on port 4242 + +## Open the profiler UI in your web browser + +Once the Codespace is ready, GitHub will automatically forward port 4242. You'll see a notification that a service is available on this port. + +- Click `Open in Browser` to open the profiler UI in a new tab +- Alternatively, you can access forwarded ports from the `PORTS` tab at the bottom of the VS Code interface + +## Load custom profiles + +If you want to load profiles for development, you can follow the steps described in [Loading in profiles for development](../CONTRIBUTING.md#loading-in-profiles-for-development) section. + +## Advanced usage + +### Opening a specific branch or pull request + +You can create a Codespace for any branch or pull request: + +1. Navigate to the branch or pull request you want to work on +2. Click the `Code` button +3. Select the `Codespaces` tab +4. Click `Create codespace on [branch-name]` + +### Using the GitHub CLI + +You can also create and manage Codespaces using the [GitHub CLI](https://cli.github.com/): + +```bash +# Create a new codespace +gh codespace create --repo firefox-devtools/profiler + +# List your codespaces +gh codespace list + +# Connect to a codespace +gh codespace code +``` + +### Configuration + +The Codespace is configured using the `.devcontainer/devcontainer.json` file in the repository. This includes: + +- Node.js environment +- Automatic port forwarding for port 4242 +- Pre-installed VS Code extensions (ESLint, Prettier, Stylelint) +- Automatic dependency installation and server startup diff --git a/docs-user/gitpod.md b/docs-user/gitpod.md deleted file mode 100644 index a4ffb9fcd6..0000000000 --- a/docs-user/gitpod.md +++ /dev/null @@ -1,34 +0,0 @@ -# Setting up profiler on gitpod - -Instead of configuring a local setup, you can also use [gitpod](https://www.gitpod.io/), an online continuous development environment with minimum setup. -Click the link below. An automatic workspace will be created. - -[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/firefox-devtools/profiler) - -## Authorize with github - -If you are using gitpod for the first time, you will have to authorize access to your GitHub account.This is necessary so you can access your data from within Gitpod. - -To authorize access, click your avatar/profile picture at the top right hand section of the setup. A dropdown menu will be displayed. In the dropdown menu, click `Open Access Control`. It will open Access Control page from where you can check the checkboxes and click update. - -## Open the profiler UI in your web browser - -A popup box, `Open Ports View`, also appears at the bottom right hand section of the setup with the message `A service is available on port 4242`. You can click `Open Browser` which will open profiler UI, [profiler.firefox.com](https://profiler.firefox.com/), for your setup in a separate tab. If you have closed `Open Ports View`, you can display it by clicking `PORTS` at the right hand section of the bottom bar. - -## Load custom profiles - -If you want to load profiles for development, you can follow the steps described in [Loading in profiles for development](../CONTRIBUTING.md#loading-in-profiles-for-development) section. - -## Advanced usage - -As an alternative to following the link above, you can also login using your gitHub account and then follow the sets below. - -- Change the URL of your browser to the respository, pull request or issue you want to open on gitpod e.g. for the [profiler project](https://github.com/firefox-devtools/profiler), URL of the upstream repository is `https://github.com/firefox-devtools/profiler`. You can also use the forked repository if you wish to start contributing to the project. -- Prefix the URL in the address bar of your browser with `gitpod.io/#` e.g. `https://github.com/firefox-devtools/profiler` becomes `https://gitpod.io/#https://github.com/firefox-devtools/profiler`. -- Gitpod will then launch a workspace for you and clone the repository, branch, commit or pull request depending on the URL in the first step. - -## Using the gitpod browser extension (optional) - -You can also install the [gitpod browser extension](https://addons.mozilla.org/en-GB/firefox/addon/gitpod/) if you wish instead of prefixing the URL of your browser with `gitpod.io/#` as described in the first step of **Advanced usage** section. - -The browser extension, if you choose to install, will add a button on each repository on Github. Clicking the button will trigger creation of an automatic gitpod setup. diff --git a/locales/be/app.ftl b/locales/be/app.ftl index 4b47948d6c..d5700daa2c 100644 --- a/locales/be/app.ftl +++ b/locales/be/app.ftl @@ -42,6 +42,14 @@ AppViewRouter--error-from-localhost-url-safari = AppViewRouter--route-not-found--home = .specialMessage = URL-адрас, да якога вы намагаецеся атрымаць доступ, не распазнаны. +## 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 = (убудаваны) + .title = { $function } была ўбудавана ў месца выкліку кампілятарам. + ## CallNodeContextMenu ## This is used as a context menu for the Call Tree, Flame Graph and Stack Chart ## panels. @@ -421,6 +429,16 @@ MarkerTable--duration = Працягласць MarkerTable--name = Назва MarkerTable--details = Падрабязнасці +## 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 = Паказваць толькі маркёры, якія адпавядаюць: “{ $filter }” + .aria-label = Паказваць толькі маркёры, якія адпавядаюць: “{ $filter }” + ## MenuButtons ## These strings are used for the buttons at the top of the profile viewer. @@ -1123,6 +1141,13 @@ SourceView--not-in-archive-error-when-obtaining-source = Файл { $pathInArchi # $url (String) - The URL from which the "archive" file was downloaded. # $parsingErrorMessage (String) - The raw internal error message during parsing, not localized SourceView--archive-parsing-error-when-obtaining-source = Не ўдалося прааналізаваць архіў па адрасе { $url }: { $parsingErrorMessage } +# Displayed below SourceView--cannot-obtain-source, if a JS file could not be found in +# the browser. +# Variables: +# $url (String) - The URL of the JS source file. +# $sourceUuid (number) - The UUID of the JS source file. +# $errorMessage (String) - The raw internal error message, not localized +SourceView--not-in-browser-error-when-obtaining-js-source = Браўзеру не ўдалося атрымаць зыходны файл для { $url } з sourceUuid { $sourceUuid }: { $errorMessage }. ## Toggle buttons in the top right corner of the bottom box diff --git a/locales/de/app.ftl b/locales/de/app.ftl index a7733a46a0..6861b370e6 100644 --- a/locales/de/app.ftl +++ b/locales/de/app.ftl @@ -427,6 +427,16 @@ MarkerTable--duration = Dauer MarkerTable--name = Name MarkerTable--details = Details +## 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 = Nur passende Markierungen anzeigen: „{ $filter }“ + .aria-label = Nur passende Markierungen anzeigen: „{ $filter }“ + ## MenuButtons ## These strings are used for the buttons at the top of the profile viewer. @@ -503,6 +513,8 @@ MenuButtons--metaInfo--profiling-started = Aufzeichnungsbeginn: MenuButtons--metaInfo--profiling-session = Aufzeichnungslänge: MenuButtons--metaInfo--main-process-started = Hauptprozess gestartet: MenuButtons--metaInfo--main-process-ended = Hauptprozess beendet: +MenuButtons--metaInfo--file-name = Dateiname: +MenuButtons--metaInfo--file-size = Dateigröße: MenuButtons--metaInfo--interval = Intervall: MenuButtons--metaInfo--buffer-capacity = Pufferkapazität: MenuButtons--metaInfo--buffer-duration = Pufferdauer: diff --git a/locales/el/app.ftl b/locales/el/app.ftl index 78a6a1cf4c..f493e6c5d0 100644 --- a/locales/el/app.ftl +++ b/locales/el/app.ftl @@ -42,6 +42,14 @@ AppViewRouter--error-from-localhost-url-safari = AppViewRouter--route-not-found--home = .specialMessage = Δεν αναγνωρίστηκε το URL που προσπαθήσατε να μεταβείτε. +## 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 = (ενσωματωμένη) + .title = Η συνάρτηση «{ $function }» ενσωματώθηκε στο καλούν στοιχείο από τον μεταγλωττιστή. + ## CallNodeContextMenu ## This is used as a context menu for the Call Tree, Flame Graph and Stack Chart ## panels. @@ -438,6 +446,16 @@ MarkerTable--duration = Διάρκεια MarkerTable--name = Όνομα MarkerTable--details = Λεπτομέρειες +## 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 = Εμφάνιση μόνο των αντίστοιχων δεικτών: «{ $filter }» + .aria-label = Εμφάνιση μόνο των αντίστοιχων δεικτών: «{ $filter }» + ## MenuButtons ## These strings are used for the buttons at the top of the profile viewer. @@ -514,6 +532,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 = Χωρητικότητα buffer: MenuButtons--metaInfo--buffer-duration = Διάρκεια buffer: diff --git a/locales/en-GB/app.ftl b/locales/en-GB/app.ftl index 0be816d28d..931cae3497 100644 --- a/locales/en-GB/app.ftl +++ b/locales/en-GB/app.ftl @@ -451,6 +451,16 @@ MarkerTable--duration = Duration MarkerTable--name = Name MarkerTable--details = Details +## 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 = Only show markers matching: “{ $filter }” + .aria-label = Only show markers matching: “{ $filter }” + ## MenuButtons ## These strings are used for the buttons at the top of the profile viewer. @@ -527,6 +537,8 @@ MenuButtons--metaInfo--profiling-started = Recording started: MenuButtons--metaInfo--profiling-session = Recording length: MenuButtons--metaInfo--main-process-started = Main process started: MenuButtons--metaInfo--main-process-ended = Main process ended: +MenuButtons--metaInfo--file-name = File name: +MenuButtons--metaInfo--file-size = File size: MenuButtons--metaInfo--interval = Interval: MenuButtons--metaInfo--buffer-capacity = Buffer Capacity: MenuButtons--metaInfo--buffer-duration = Buffer Duration: diff --git a/locales/en-US/app.ftl b/locales/en-US/app.ftl index 336411c89d..9eb89ea769 100644 --- a/locales/en-US/app.ftl +++ b/locales/en-US/app.ftl @@ -581,6 +581,8 @@ MenuButtons--metaInfo--profiling-started = Recording started: MenuButtons--metaInfo--profiling-session = Recording length: MenuButtons--metaInfo--main-process-started = Main process started: MenuButtons--metaInfo--main-process-ended = Main process ended: +MenuButtons--metaInfo--file-name = File name: +MenuButtons--metaInfo--file-size = File size: MenuButtons--metaInfo--interval = Interval: MenuButtons--metaInfo--buffer-capacity = Buffer capacity: MenuButtons--metaInfo--buffer-duration = Buffer duration: diff --git a/locales/es-CL/app.ftl b/locales/es-CL/app.ftl index b1ecf4ba4d..4c2f7f4543 100644 --- a/locales/es-CL/app.ftl +++ b/locales/es-CL/app.ftl @@ -39,6 +39,14 @@ AppViewRouter--error-from-localhost-url-safari = Debido a una limitación esp AppViewRouter--route-not-found--home = .specialMessage = La URL a la que intentaste acceder no fue reconocida. +## 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 = (incorporadas) + .title = { $function } fue incorporada en la función que lo llamó por el compilador. + ## CallNodeContextMenu ## This is used as a context menu for the Call Tree, Flame Graph and Stack Chart ## panels. @@ -374,6 +382,16 @@ MarkerTable--duration = Duración MarkerTable--name = Nombre MarkerTable--details = Detalles +## 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 = Mostrar solo marcadores que coincidan con: “{ $filter }” + .aria-label = Mostrar solo marcadores que coincidan con: “{ $filter }” + ## MenuButtons ## These strings are used for the buttons at the top of the profile viewer. diff --git a/locales/fr/app.ftl b/locales/fr/app.ftl index 84068f8ab7..9aca11515e 100644 --- a/locales/fr/app.ftl +++ b/locales/fr/app.ftl @@ -368,6 +368,16 @@ MarkerTable--duration = Durée MarkerTable--name = Nom MarkerTable--details = Détails +## 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 = Afficher uniquement les marqueurs correspondant à « { $filter } » + .aria-label = Afficher uniquement les marqueurs correspondant à « { $filter } » + ## MenuButtons ## These strings are used for the buttons at the top of the profile viewer. @@ -444,6 +454,8 @@ MenuButtons--metaInfo--profiling-started = Enregistrement commencé : MenuButtons--metaInfo--profiling-session = Durée d’enregistrement : MenuButtons--metaInfo--main-process-started = Processus principal démarré : MenuButtons--metaInfo--main-process-ended = Processus principal terminé : +MenuButtons--metaInfo--file-name = Nom du fichier : +MenuButtons--metaInfo--file-size = Taille du fichier : MenuButtons--metaInfo--interval = Intervalle : MenuButtons--metaInfo--buffer-capacity = Capacité de la mémoire tampon : MenuButtons--metaInfo--buffer-duration = Durée de la mémoire tampon : diff --git a/locales/fy-NL/app.ftl b/locales/fy-NL/app.ftl index f0a484c98f..c76008ff97 100644 --- a/locales/fy-NL/app.ftl +++ b/locales/fy-NL/app.ftl @@ -451,6 +451,16 @@ MarkerTable--duration = Doer MarkerTable--name = Namme MarkerTable--details = Details +## 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 = Allinnich markearringen toane dy’t oerienkomme mei: ‘{ $filter }’ + .aria-label = Allinnich markearringen toane dy’t oerienkomme mei: ‘{ $filter }’ + ## MenuButtons ## These strings are used for the buttons at the top of the profile viewer. @@ -527,6 +537,8 @@ MenuButtons--metaInfo--profiling-started = Opname start: MenuButtons--metaInfo--profiling-session = Opnamedoer: MenuButtons--metaInfo--main-process-started = Haadproses start: MenuButtons--metaInfo--main-process-ended = Haadproses stoppe: +MenuButtons--metaInfo--file-name = Bestânsnamme: +MenuButtons--metaInfo--file-size = Bestânsgrutte: MenuButtons--metaInfo--interval = Ynterfal: MenuButtons--metaInfo--buffer-capacity = Bufferkapasiteit: MenuButtons--metaInfo--buffer-duration = Bufferdoer: diff --git a/locales/ia/app.ftl b/locales/ia/app.ftl index fe7749914d..99a8617407 100644 --- a/locales/ia/app.ftl +++ b/locales/ia/app.ftl @@ -436,6 +436,16 @@ MarkerTable--duration = Duration MarkerTable--name = Nomine MarkerTable--details = Detalios +## 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 = Monstrar solo marcatores concordante: “{ $filter }” + .aria-label = Monstrar solo marcatores concordante: “{ $filter }” + ## MenuButtons ## These strings are used for the buttons at the top of the profile viewer. @@ -512,6 +522,8 @@ MenuButtons--metaInfo--profiling-started = Registration comenciate: MenuButtons--metaInfo--profiling-session = Durata de registration MenuButtons--metaInfo--main-process-started = Processo principal initiate: MenuButtons--metaInfo--main-process-ended = Processo principal finite: +MenuButtons--metaInfo--file-name = Nomine del file: +MenuButtons--metaInfo--file-size = Dimension de file: MenuButtons--metaInfo--interval = Intervallo: MenuButtons--metaInfo--buffer-capacity = Capacitate de buffer: MenuButtons--metaInfo--buffer-duration = Capacitate de buffer: diff --git a/locales/it/app.ftl b/locales/it/app.ftl index 8cf025594a..115c1c7508 100644 --- a/locales/it/app.ftl +++ b/locales/it/app.ftl @@ -367,6 +367,16 @@ MarkerTable--duration = Durata MarkerTable--name = Nome MarkerTable--details = Dettagli +## 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 = Mostra solo i marker corrispondenti a: “{ $filter }” + .aria-label = Mostra solo i marker corrispondenti a: “{ $filter }” + ## MenuButtons ## These strings are used for the buttons at the top of the profile viewer. @@ -443,6 +453,8 @@ MenuButtons--metaInfo--profiling-started = Registrazione avviata: MenuButtons--metaInfo--profiling-session = Lunghezza registrazione: MenuButtons--metaInfo--main-process-started = Processo principale avviato: MenuButtons--metaInfo--main-process-ended = Processo principale completato: +MenuButtons--metaInfo--file-name = Nome file: +MenuButtons--metaInfo--file-size = Dimensione file: MenuButtons--metaInfo--interval = Intervallo: MenuButtons--metaInfo--buffer-capacity = Capacità buffer: MenuButtons--metaInfo--buffer-duration = Durata buffer: diff --git a/locales/nl/app.ftl b/locales/nl/app.ftl index c8c53ff0c8..2f9cb7d349 100644 --- a/locales/nl/app.ftl +++ b/locales/nl/app.ftl @@ -451,6 +451,16 @@ MarkerTable--duration = Duur MarkerTable--name = Naam MarkerTable--details = Details +## 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 = Alleen markeringen tonen die overeenkomen met: ‘{ $filter }’ + .aria-label = Alleen markeringen tonen die overeenkomen met: ‘{ $filter }’ + ## MenuButtons ## These strings are used for the buttons at the top of the profile viewer. @@ -527,6 +537,8 @@ MenuButtons--metaInfo--profiling-started = Opname gestart: MenuButtons--metaInfo--profiling-session = Opnameduur: MenuButtons--metaInfo--main-process-started = Hoofdproces gestart: MenuButtons--metaInfo--main-process-ended = Hoofdproces beëindigd: +MenuButtons--metaInfo--file-name = Bestandsnaam: +MenuButtons--metaInfo--file-size = Bestandsgrootte: MenuButtons--metaInfo--interval = Interval: MenuButtons--metaInfo--buffer-capacity = Buffercapaciteit: MenuButtons--metaInfo--buffer-duration = Bufferduur: diff --git a/locales/pt-BR/app.ftl b/locales/pt-BR/app.ftl index 01dc2a81ea..fb19db7a7c 100644 --- a/locales/pt-BR/app.ftl +++ b/locales/pt-BR/app.ftl @@ -39,6 +39,14 @@ AppViewRouter--error-from-localhost-url-safari = Devido a uma limitação esp AppViewRouter--route-not-found--home = .specialMessage = A URL que você tentou acessar não foi reconhecida. +## 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 = (inlined) + .title = { $function } foi inlined no chamador pelo compilador. + ## CallNodeContextMenu ## This is used as a context menu for the Call Tree, Flame Graph and Stack Chart ## panels. @@ -366,6 +374,16 @@ MarkerTable--duration = Duração MarkerTable--name = Nome MarkerTable--details = Detalhes +## 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 = Mostrar apenas marcadores correspondentes a: “{ $filter }” + .aria-label = Mostrar apenas marcadores correspondentes a: “{ $filter }” + ## MenuButtons ## These strings are used for the buttons at the top of the profile viewer. @@ -442,6 +460,8 @@ MenuButtons--metaInfo--profiling-started = Gravação iniciada: MenuButtons--metaInfo--profiling-session = Duração da gravação: MenuButtons--metaInfo--main-process-started = Processo principal iniciado: MenuButtons--metaInfo--main-process-ended = Processo principal finalizado: +MenuButtons--metaInfo--file-name = Nome do arquivo: +MenuButtons--metaInfo--file-size = Tamanho do arquivo: MenuButtons--metaInfo--interval = Intervalo: MenuButtons--metaInfo--buffer-capacity = Capacidade do buffer: MenuButtons--metaInfo--buffer-duration = Duração do buffer: diff --git a/locales/ru/app.ftl b/locales/ru/app.ftl index d1fbb57896..27b4369238 100644 --- a/locales/ru/app.ftl +++ b/locales/ru/app.ftl @@ -449,6 +449,16 @@ MarkerTable--duration = Длительность MarkerTable--name = Имя MarkerTable--details = Подробности +## 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 = Показать только подходящие маркеры: «{ $filter }» + .aria-label = Показать только подходящие маркеры: «{ $filter }» + ## MenuButtons ## These strings are used for the buttons at the top of the profile viewer. @@ -535,6 +545,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 = Длительность буфера: @@ -824,7 +836,7 @@ TrackContextMenu--hide-all-matching-tracks = Скрыть все совпада # any track. # Variables: # $searchFilter (String) - The search filter string that user enters. -TrackContextMenu--no-results-found = Не найдено результатов для «{ $searchFilter }» +TrackContextMenu--no-results-found = Не найдено результатов по запросу «{ $searchFilter }» # This button appears when hovering a track name and is displayed as an X icon. TrackNameButton--hide-track = .title = Скрыть трек diff --git a/locales/sv-SE/app.ftl b/locales/sv-SE/app.ftl index 882fa9d3a6..3d219db975 100644 --- a/locales/sv-SE/app.ftl +++ b/locales/sv-SE/app.ftl @@ -446,6 +446,16 @@ MarkerTable--duration = Längd MarkerTable--name = Namn MarkerTable--details = Detaljer +## 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 = Visa endast markörer som matchar: "{ $filter }" + .aria-label = Visa endast markörer som matchar: "{ $filter }" + ## MenuButtons ## These strings are used for the buttons at the top of the profile viewer. @@ -522,6 +532,8 @@ MenuButtons--metaInfo--profiling-started = Inspelningen startade: MenuButtons--metaInfo--profiling-session = Inspelningslängd: MenuButtons--metaInfo--main-process-started = Huvudprocessen startade: MenuButtons--metaInfo--main-process-ended = Huvudprocessen avslutad: +MenuButtons--metaInfo--file-name = Filnamn: +MenuButtons--metaInfo--file-size = Filstorlek: MenuButtons--metaInfo--interval = Intervall: MenuButtons--metaInfo--buffer-capacity = Buffertkapacitet: MenuButtons--metaInfo--buffer-duration = Buffertlängd: diff --git a/locales/tr/app.ftl b/locales/tr/app.ftl index 17a95499ea..65e99ff693 100644 --- a/locales/tr/app.ftl +++ b/locales/tr/app.ftl @@ -357,6 +357,16 @@ MarkerTable--duration = Süre MarkerTable--name = Ad MarkerTable--details = Ayrıntılar +## 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 = Yalnızca şununla eşleşen işaretleri göster: “{ $filter }” + .aria-label = Yalnızca şununla eşleşen işaretleri göster: “{ $filter }” + ## MenuButtons ## These strings are used for the buttons at the top of the profile viewer. diff --git a/locales/zh-CN/app.ftl b/locales/zh-CN/app.ftl index 29646aed80..aaef8d3848 100644 --- a/locales/zh-CN/app.ftl +++ b/locales/zh-CN/app.ftl @@ -39,6 +39,14 @@ AppViewRouter--error-from-localhost-url-safari = 由于 Safari 浏览器的 AppViewRouter--route-not-found--home = .specialMessage = 无法识别您尝试访问的 URL。 +## 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 = (已内联) + .title = 编译器已将 { $function } 内联至其调用方。 + ## CallNodeContextMenu ## This is used as a context menu for the Call Tree, Flame Graph and Stack Chart ## panels. @@ -332,7 +340,7 @@ MarkerContextMenu--select-the-sender-thread = 选择 Sender 线程“{ $ # This string is used on the marker filters menu item when clicked on the filter icon. # Variables: # $filter (String) - Search string that will be used to filter the markers. -MarkerFiltersContextMenu--drop-samples-outside-of-markers-matching = 不用标记过滤器“{ $filter }”标记此样本 +MarkerFiltersContextMenu--drop-samples-outside-of-markers-matching = 丢弃与标记(匹配条件:“{ $filter }”)不相关的样本 ## MarkerSettings ## This is used in all panels related to markers. @@ -356,6 +364,16 @@ MarkerTable--duration = 持续时间 MarkerTable--name = 名称 MarkerTable--details = 详情 +## 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 = 仅显示匹配“{ $filter }”的标记 + .aria-label = 仅显示匹配“{ $filter }”的标记 + ## MenuButtons ## These strings are used for the buttons at the top of the profile viewer. @@ -939,7 +957,7 @@ TransformNavigator--collapse-function-subtree = 折叠子树:{ $item } # "Drop samples outside of markers matching ..." transform. # Variables: # $item (String) - Search filter of the markers that transform will apply to. -TransformNavigator--drop-samples-outside-of-markers-matching = 不用过滤器 “{ $item }” 标记该样本 +TransformNavigator--drop-samples-outside-of-markers-matching = 丢弃与标记(匹配条件:“{ $item }”)不相关的样本 ## "Bottom box" - a view which contains the source view and the assembly view, ## at the bottom of the profiler UI diff --git a/locales/zh-TW/app.ftl b/locales/zh-TW/app.ftl index ca75bd3d5e..af2f33ef56 100644 --- a/locales/zh-TW/app.ftl +++ b/locales/zh-TW/app.ftl @@ -363,6 +363,16 @@ MarkerTable--duration = 持續時間 MarkerTable--name = 名稱 MarkerTable--details = 詳細資訊 +## 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 = 只顯示符合「{ $filter }」的標記 + .aria-label = 只顯示符合「{ $filter }」的標記 + ## MenuButtons ## These strings are used for the buttons at the top of the profile viewer. @@ -431,6 +441,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/package.json b/package.json index 206786bf6c..e5a4616f09 100644 --- a/package.json +++ b/package.json @@ -63,12 +63,12 @@ "@codemirror/lang-rust": "^6.0.2", "@codemirror/language": "^6.11.3", "@codemirror/state": "^6.5.2", - "@codemirror/view": "^6.38.4", + "@codemirror/view": "^6.38.6", "@firefox-devtools/react-contextmenu": "^5.2.3", "@fluent/bundle": "^0.19.1", "@fluent/langneg": "^0.7.0", "@fluent/react": "^0.15.2", - "@lezer/highlight": "^1.2.1", + "@lezer/highlight": "^1.2.2", "@tgwf/co2": "^0.16.9", "array-move": "^3.0.1", "array-range": "^1.0.1", @@ -76,7 +76,7 @@ "classnames": "^2.5.1", "common-tags": "^1.8.2", "copy-to-clipboard": "^3.3.3", - "core-js": "^3.45.1", + "core-js": "^3.46.0", "escape-string-regexp": "^4.0.0", "gecko-profiler-demangle": "^0.4.0", "idb": "^8.0.3", @@ -93,7 +93,7 @@ "query-string": "^9.3.1", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-intersection-observer": "^9.16.0", + "react-intersection-observer": "^10.0.0", "react-redux": "^9.2.0", "react-splitter-layout": "^4.0.0", "react-transition-group": "^4.4.5", @@ -102,19 +102,20 @@ "redux-thunk": "^3.1.0", "reselect": "^4.1.8", "url": "^0.11.4", + "valibot": "^1.1.0", "weaktuplemap": "^1.0.0", "workbox-window": "^7.3.0" }, "devDependencies": { "@babel/cli": "^7.28.3", - "@babel/core": "^7.28.4", - "@babel/eslint-parser": "^7.28.4", + "@babel/core": "^7.28.5", + "@babel/eslint-parser": "^7.28.5", "@babel/eslint-plugin": "^7.27.1", "@babel/plugin-proposal-class-properties": "^7.18.6", - "@babel/preset-env": "^7.28.3", - "@babel/preset-react": "^7.27.1", - "@babel/preset-typescript": "^7.27.1", - "@eslint/js": "^9.36.0", + "@babel/preset-env": "^7.28.5", + "@babel/preset-react": "^7.28.5", + "@babel/preset-typescript": "^7.28.5", + "@eslint/js": "^9.39.1", "@testing-library/dom": "^10.4.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.0", @@ -122,7 +123,7 @@ "@types/common-tags": "^1.8.4", "@types/jest": "^30.0.0", "@types/minimist": "^1.2.5", - "@types/node": "^22.18.10", + "@types/node": "^22.19.1", "@types/query-string": "^6.3.0", "@types/react": "^18.3.26", "@types/react-dom": "^18.3.1", @@ -130,23 +131,22 @@ "@types/react-transition-group": "^4.4.5", "@types/redux-logger": "^3.0.6", "@types/tgwf__co2": "^0.14.2", - "@typescript-eslint/eslint-plugin": "^8.45.0", - "@typescript-eslint/parser": "^8.45.0", + "@typescript-eslint/eslint-plugin": "^8.46.2", + "@typescript-eslint/parser": "^8.46.2", "alex": "^11.0.1", "autoprefixer": "^10.4.21", "babel-jest": "^30.2.0", "babel-loader": "^10.0.0", "babel-plugin-module-resolver": "^5.0.2", - "browserslist": "^4.26.3", - "caniuse-lite": "^1.0.30001743", + "browserslist": "^4.27.0", + "caniuse-lite": "^1.0.30001751", "circular-dependency-plugin": "^5.2.1", - "codecov": "^3.8.3", "copy-webpack-plugin": "^13.0.1", "cross-env": "^10.1.0", "css-loader": "^7.1.2", - "cssnano": "^7.1.1", + "cssnano": "^7.1.2", "devtools-license-check": "^0.9.0", - "eslint": "^9.36.0", + "eslint": "^9.39.1", "eslint-config-prettier": "^10.1.8", "eslint-import-resolver-alias": "^1.1.2", "eslint-plugin-import": "^2.32.0", @@ -154,13 +154,13 @@ "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.6.8", + "eslint-plugin-testing-library": "^7.13.3", "espree": "^10.4.0", - "fake-indexeddb": "^6.2.2", - "fetch-mock": "^12.5.4", + "fake-indexeddb": "^6.2.4", + "fetch-mock": "^12.5.5", "file-loader": "^6.2.0", "glob": "^11.0.3", - "globals": "^16.4.0", + "globals": "^16.5.0", "html-webpack-plugin": "^5.6.4", "husky": "^4.3.8", "jest": "^30.2.0", @@ -178,13 +178,13 @@ "postinstall-postinstall": "^2.1.0", "prettier": "^3.6.2", "raw-loader": "^4.0.2", - "rimraf": "^5.0.10", + "rimraf": "^6.1.0", "style-loader": "^4.0.0", "stylelint": "^16.25.0", "stylelint-config-idiomatic-order": "^10.0.0", "stylelint-config-standard": "^39.0.1", "typescript": "^5.9.3", - "webpack": "^5.102.0", + "webpack": "^5.102.1", "webpack-cli": "^6.0.1", "webpack-dev-server": "^5.2.2", "workbox-webpack-plugin": "^7.3.0", diff --git a/res/contribute.json b/res/contribute.json index 6e01e88f6d..940804eef7 100644 --- a/res/contribute.json +++ b/res/contribute.json @@ -4,7 +4,7 @@ "repository": { "url": "https://github.com/firefox-devtools/profiler", "license": "MPL2", - "tests": "https://circleci.com/gh/firefox-devtools/profiler" + "tests": "https://github.com/firefox-devtools/profiler/actions" }, "participate": { "home": "https://github.com/firefox-devtools/profiler/blob/main/CONTRIBUTING.md", diff --git a/res/photon/server.js b/res/photon/server.js index 7fa2178f74..dc08bc121a 100644 --- a/res/photon/server.js +++ b/res/photon/server.js @@ -6,7 +6,7 @@ const port = process.env.FX_PROFILER_PHOTON_PORT || 4243; const host = process.env.FX_PROFILER_PHOTON_HOST || 'localhost'; const serverConfig = { - allowedHosts: ['localhost', '.gitpod.io'], + allowedHosts: ['localhost', '.app.github.dev'], host, port, static: false, diff --git a/server.js b/server.js index 8a270193a6..d2071b82b7 100644 --- a/server.js +++ b/server.js @@ -29,7 +29,7 @@ config.cache = { type: 'filesystem', }; const serverConfig = { - allowedHosts: ['localhost', '.gitpod.io'], + allowedHosts: ['localhost', '.app.github.dev'], host, port, // We disable hot reloading because this takes lot of CPU and memory in the diff --git a/src/actions/profile-view.ts b/src/actions/profile-view.ts index 9ca988d8cc..e518623d26 100644 --- a/src/actions/profile-view.ts +++ b/src/actions/profile-view.ts @@ -1917,7 +1917,7 @@ export function changeTableViewOptions( export function updateBottomBoxContentsAndMaybeOpen( currentTab: TabSlug, - { libIndex, sourceIndex, nativeSymbols }: BottomBoxInfo + { libIndex, sourceIndex, nativeSymbols, lineNumber }: BottomBoxInfo ): Action { // TODO: If the set has more than one element, pick the native symbol with // the highest total sample count @@ -1932,6 +1932,7 @@ export function updateBottomBoxContentsAndMaybeOpen( currentTab, shouldOpenBottomBox: sourceIndex !== null || nativeSymbol !== null, shouldOpenAssemblyView: sourceIndex === null && nativeSymbol !== null, + lineNumber, }; } diff --git a/src/components/app/AssemblyViewToggleButton.tsx b/src/components/app/AssemblyViewToggleButton.tsx index d65bb7e5bb..dd45b51de7 100644 --- a/src/components/app/AssemblyViewToggleButton.tsx +++ b/src/components/app/AssemblyViewToggleButton.tsx @@ -6,6 +6,7 @@ import React from 'react'; import classNames from 'classnames'; import { getAssemblyViewIsOpen } from 'firefox-profiler/selectors/url-state'; +import { getIsAssemblyViewAvailable } from 'firefox-profiler/selectors/code'; import { openAssemblyView, closeAssemblyView, @@ -18,6 +19,7 @@ import { Localized } from '@fluent/react'; type StateProps = { readonly assemblyViewIsOpen: boolean; + readonly isAssemblyViewAvailable: boolean; }; type DispatchProps = { @@ -37,7 +39,7 @@ class AssemblyViewToggleButtonImpl extends React.PureComponent { }; override render() { - const { assemblyViewIsOpen } = this.props; + const { assemblyViewIsOpen, isAssemblyViewAvailable } = this.props; return assemblyViewIsOpen ? ( @@ -51,6 +53,7 @@ class AssemblyViewToggleButtonImpl extends React.PureComponent { title="Hide the assembly view" type="button" onClick={this._onClick} + disabled={!isAssemblyViewAvailable} /> ) : ( @@ -64,6 +67,7 @@ class AssemblyViewToggleButtonImpl extends React.PureComponent { title="Show the assembly view" type="button" onClick={this._onClick} + disabled={!isAssemblyViewAvailable} /> ); @@ -77,6 +81,7 @@ export const AssemblyViewToggleButton = explicitConnect< >({ mapStateToProps: (state) => ({ assemblyViewIsOpen: getAssemblyViewIsOpen(state), + isAssemblyViewAvailable: getIsAssemblyViewAvailable(state), }), mapDispatchToProps: { openAssemblyView, diff --git a/src/components/app/BottomBox.tsx b/src/components/app/BottomBox.tsx index 4f5d8d2434..c2631c42fb 100644 --- a/src/components/app/BottomBox.tsx +++ b/src/components/app/BottomBox.tsx @@ -14,6 +14,7 @@ import { CodeLoadingOverlay } from './CodeLoadingOverlay'; import { CodeErrorOverlay } from './CodeErrorOverlay'; import { getSourceViewScrollGeneration, + getSourceViewLineNumber, getAssemblyViewIsOpen, getAssemblyViewNativeSymbol, getAssemblyViewScrollGeneration, @@ -54,6 +55,7 @@ type StateProps = { readonly sourceViewFile: string | null; readonly sourceViewCode: SourceCodeStatus | void; readonly sourceViewScrollGeneration: number; + readonly sourceViewLineNumber?: number; readonly globalLineTimings: LineTimings; readonly selectedCallNodeLineTimings: LineTimings; readonly assemblyViewIsOpen: boolean; @@ -160,6 +162,7 @@ class BottomBoxImpl extends React.PureComponent { globalLineTimings, disableOverscan, sourceViewScrollGeneration, + sourceViewLineNumber, selectedCallNodeLineTimings, assemblyViewIsOpen, assemblyViewScrollGeneration, @@ -231,7 +234,9 @@ class BottomBoxImpl extends React.PureComponent { sourceCode={sourceCode} filePath={path} scrollToHotSpotGeneration={sourceViewScrollGeneration} + scrollToLineNumber={sourceViewLineNumber} hotSpotTimings={selectedCallNodeLineTimings} + highlightedLine={sourceViewLineNumber} ref={this._sourceView} /> ) : null} @@ -300,6 +305,7 @@ export const BottomBox = explicitConnect<{}, StateProps, DispatchProps>({ selectedCallNodeLineTimings: selectedNodeSelectors.getSourceViewLineTimings(state), sourceViewScrollGeneration: getSourceViewScrollGeneration(state), + sourceViewLineNumber: getSourceViewLineNumber(state), assemblyViewNativeSymbol: getAssemblyViewNativeSymbol(state), assemblyViewCode: getAssemblyViewCode(state), globalAddressTimings: diff --git a/src/components/app/MenuButtons/MetaInfo.tsx b/src/components/app/MenuButtons/MetaInfo.tsx index f9b319342a..8defaf2ef9 100644 --- a/src/components/app/MenuButtons/MetaInfo.tsx +++ b/src/components/app/MenuButtons/MetaInfo.tsx @@ -9,6 +9,7 @@ import { getProfile, getSymbolicationStatus, getProfileExtraInfo, + getProfileTimelineUnit, } from 'firefox-profiler/selectors/profile'; import { resymbolicateProfile } from 'firefox-profiler/actions/receive-profile'; import { formatFromMarkerSchema } from 'firefox-profiler/profile-logic/marker-schema'; @@ -29,6 +30,7 @@ import type { Profile, SymbolicationStatus, ExtraProfileInfoSection, + TimelineUnit, } from 'firefox-profiler/types'; import type { ConnectedProps } from 'firefox-profiler/utils/connect'; import { StringTable } from 'firefox-profiler/utils/string-table'; @@ -43,6 +45,7 @@ type StateProps = Readonly<{ profile: Profile; symbolicationStatus: SymbolicationStatus; profileExtraInfo: ExtraProfileInfoSection[]; + timelineUnit: TimelineUnit; }>; type DispatchProps = Readonly<{ @@ -186,138 +189,151 @@ class MetaInfoPanelImpl extends React.PureComponent { ); } - override render() { - const { meta, profilerOverhead } = this.props.profile; + _renderRecordingInfo() { + const { meta } = this.props.profile; const { configuration } = meta; - - const platformInformation = formatPlatform(meta); - - let cpuCount = null; - if (meta.physicalCPUs && meta.logicalCPUs) { - cpuCount = ( - - ); - } else if (meta.physicalCPUs) { - cpuCount = ( - - ); - } else if (meta.logicalCPUs) { - cpuCount = ( - - ); - } + const { timelineUnit } = this.props; return ( - <> -
- {meta.profilingStartTime !== undefined && meta.startTime ? ( +
+ {meta.profilingStartTime !== undefined && meta.startTime ? ( +
+ + + Recording started: + + + {_formatDate(meta.startTime + meta.profilingStartTime)} +
+ ) : null} + {meta.profilingStartTime !== undefined && meta.profilingEndTime ? ( +
+ + + Recording length: + + + {formatTimestamp(meta.profilingEndTime - meta.profilingStartTime)} +
+ ) : null} + {meta.fileName ? ( +
+ + + File name: + + + {meta.fileName} +
+ ) : null} + {meta.fileSize ? ( +
+ + + File size: + + + {formatBytes(meta.fileSize)} +
+ ) : null} + {meta.profilingStartTime === undefined && meta.startTime ? ( +
+ + + Main process started: + + + {_formatDate(meta.startTime)} +
+ ) : null} + {meta.endTime ? ( +
+ + + Main process ended: + + + {_formatDate(meta.endTime)} +
+ ) : null} + {meta.interval ? ( +
+ + + Interval: + + + {timelineUnit === 'bytes' + ? formatBytes(meta.interval) + : formatTimestamp(meta.interval, 4, 1)} +
+ ) : null} + {configuration ? ( + <>
- - Recording started: + + Buffer capacity: - {_formatDate(meta.startTime + meta.profilingStartTime)} + { + /* The capacity is expressed in "entries", where 1 entry == 8 bytes. */ + formatBytes(configuration.capacity * 8, 0) + }
- ) : null} - {meta.profilingStartTime !== undefined && meta.profilingEndTime ? (
- - Recording length: + + Buffer duration: - {formatTimestamp(meta.profilingEndTime - meta.profilingStartTime)} -
- ) : null} - {meta.profilingStartTime === undefined && meta.startTime ? ( -
- - - Main process started: + {configuration.duration ? ( + + {'{$configurationDuration} seconds'} - - {_formatDate(meta.startTime)} -
- ) : null} - {meta.endTime ? ( -
- - - Main process ended: + ) : ( + + Unlimited - - {_formatDate(meta.endTime)} + )}
- ) : null} - {meta.interval ? ( -
- - - Interval: - - - {formatTimestamp(meta.interval, 4, 1)} +
+ {_renderRowOfList( + 'MenuButtons--metaInfo-renderRowOfList-label-features', + configuration.features + )} + {_renderRowOfList( + 'MenuButtons--metaInfo-renderRowOfList-label-threads-filter', + configuration.threads + )}
- ) : null} - {configuration ? ( - <> -
- - - Buffer capacity: - - - { - /* The capacity is expressed in "entries", where 1 entry == 8 bytes. */ - formatBytes(configuration.capacity * 8, 0) - } -
-
- - - Buffer duration: - - - {configuration.duration ? ( - - {'{$configurationDuration} seconds'} - - ) : ( - - Unlimited - - )} -
-
- {_renderRowOfList( - 'MenuButtons--metaInfo-renderRowOfList-label-features', - configuration.features - )} - {_renderRowOfList( - 'MenuButtons--metaInfo-renderRowOfList-label-threads-filter', - configuration.threads - )} -
- - ) : null} - {this.renderSymbolication()} -
+ + ) : null} + {this.renderSymbolication()} +
+ ); + } + + _renderApplicationSection() { + const { meta } = this.props.profile; + + if ( + !meta.product && + !meta.profilingStartTime && + !meta.updateChannel && + !meta.appBuildID && + meta.debug === undefined && + !meta.extensions && + !meta.arguments + ) { + return null; + } + + return ( + <>

Application @@ -408,6 +424,57 @@ class MetaInfoPanelImpl extends React.PureComponent {

) : null} + + ); + } + + _renderPlatformSection() { + const { meta } = this.props.profile; + + if ( + !meta.device && + !meta.oscpu && + !meta.platform && + !meta.abi && + !meta.CPUName && + !meta.physicalCPUs && + !meta.logicalCPUs && + !meta.mainMemory + ) { + return null; + } + + const platformInformation = formatPlatform(meta); + + let cpuCount = null; + if (meta.physicalCPUs && meta.logicalCPUs) { + cpuCount = ( + + ); + } else if (meta.physicalCPUs) { + cpuCount = ( + + ); + } else if (meta.logicalCPUs) { + cpuCount = ( + + ); + } + + return ( + <>

Platform

@@ -469,45 +536,67 @@ class MetaInfoPanelImpl extends React.PureComponent { ) : null} - {meta.visualMetrics ? ( - <> -

- - Visual metrics + + ); + } + + _renderVisualMetricsSection() { + const { meta } = this.props.profile; + + if (!meta.visualMetrics) { + return null; + } + + return ( + <> +

+ + Visual metrics + +

+
+
+ + + Speed Index: -

-
-
- - - Speed Index: - - - {meta.visualMetrics.SpeedIndex} -
-
- - - Perceptual Speed Index: - - - {meta.visualMetrics.PerceptualSpeedIndex} -
-
- - - Contentful Speed Index: - - - {meta.visualMetrics.ContentfulSpeedIndex} -
-
- - ) : null} + + {meta.visualMetrics.SpeedIndex} + +
+ + + Perceptual Speed Index: + + + {meta.visualMetrics.PerceptualSpeedIndex} +
+
+ + + Contentful Speed Index: + + + {meta.visualMetrics.ContentfulSpeedIndex} +
+ + + ); + } + + override render() { + const { profilerOverhead } = this.props.profile; + + return ( + <> + {this._renderRecordingInfo()} + {this._renderApplicationSection()} + {this._renderPlatformSection()} + {this._renderVisualMetricsSection()} {/* - Older profiles(before FF 70) don't have any overhead info. - Don't show anything if that's the case. - */} + Older profiles(before FF 70) don't have any overhead info. + Don't show anything if that's the case. + */} {profilerOverhead ? ( ) : null} @@ -559,6 +648,7 @@ export const MetaInfoPanel = explicitConnect<{}, StateProps, DispatchProps>({ profile: getProfile(state), symbolicationStatus: getSymbolicationStatus(state), profileExtraInfo: getProfileExtraInfo(state), + timelineUnit: getProfileTimelineUnit(state), }), mapDispatchToProps: { resymbolicateProfile, diff --git a/src/components/marker-chart/Canvas.tsx b/src/components/marker-chart/Canvas.tsx index d1051b4c4d..ec6544ad32 100644 --- a/src/components/marker-chart/Canvas.tsx +++ b/src/components/marker-chart/Canvas.tsx @@ -17,6 +17,7 @@ import type { changeRightClickedMarker, changeMouseTimePosition, changeSelectedMarker, + updateBottomBoxContentsAndMaybeOpen, } from 'firefox-profiler/actions/profile-view'; type UpdatePreviewSelection = typeof updatePreviewSelection; @@ -35,7 +36,10 @@ import type { MarkerIndex, MarkerSchemaByName, GraphColor, + Thread, + IndexIntoStackTable, } from 'firefox-profiler/types'; +import type { TabSlug } from 'firefox-profiler/app-logic/tabs-handling'; import { getStartEndRangeForMarker } from 'firefox-profiler/utils'; import { getStrokeColor, @@ -45,6 +49,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 type { ChartCanvasScale, @@ -81,6 +86,9 @@ type OwnProps = { readonly selectedMarkerIndex: MarkerIndex | null; readonly rightClickedMarkerIndex: MarkerIndex | null; readonly shouldDisplayTooltips: () => boolean; + readonly thread: Thread; + readonly updateBottomBoxContentsAndMaybeOpen: typeof updateBottomBoxContentsAndMaybeOpen; + readonly selectedTab: TabSlug; }; type Props = OwnProps & { @@ -869,11 +877,37 @@ class MarkerChartCanvasImpl extends React.PureComponent { changeRightClickedMarker(threadsKey, markerIndex); }; + _onStackFrameClick = (stackIndex: IndexIntoStackTable) => { + const { thread, selectedTab, updateBottomBoxContentsAndMaybeOpen } = + this.props; + const bottomBoxInfo = getBottomBoxInfoForStackFrame(stackIndex, thread); + updateBottomBoxContentsAndMaybeOpen(selectedTab, bottomBoxInfo); + }; + + isMarkerVisible = (markerIndex: MarkerIndex): boolean => { + const { markerTimingAndBuckets } = this.props; + // Check if the marker appears in the visible marker timing data + for (const markerTiming of markerTimingAndBuckets) { + if (typeof markerTiming === 'string') { + continue; + } + if (markerTiming.index.includes(markerIndex)) { + return true; + } + } + return false; + }; + getHoveredMarkerInfo = (markerIndex: MarkerIndex): React.ReactNode => { if (!this.props.shouldDisplayTooltips() || markerIndex === null) { return null; } + // Check if the marker is visible (not filtered out) + if (!this.isMarkerVisible(markerIndex)) { + return null; + } + const marker = this.props.getMarker(markerIndex); return ( { marker={marker} threadsKey={this.props.threadsKey} restrictHeightWidth={true} + onStackFrameClick={this._onStackFrameClick} /> ); }; diff --git a/src/components/marker-chart/index.tsx b/src/components/marker-chart/index.tsx index 68a12c1dc6..93bb9a1e96 100644 --- a/src/components/marker-chart/index.tsx +++ b/src/components/marker-chart/index.tsx @@ -17,12 +17,16 @@ import { getMarkerSchemaByName, } from 'firefox-profiler/selectors/profile'; import { selectedThreadSelectors } from 'firefox-profiler/selectors/per-thread'; -import { getSelectedThreadsKey } from 'firefox-profiler/selectors/url-state'; +import { + getSelectedThreadsKey, + getSelectedTab, +} from 'firefox-profiler/selectors/url-state'; import { updatePreviewSelection, changeRightClickedMarker, changeMouseTimePosition, changeSelectedMarker, + updateBottomBoxContentsAndMaybeOpen, } from 'firefox-profiler/actions/profile-view'; import { ContextMenuTrigger } from 'firefox-profiler/components/shared/ContextMenuTrigger'; @@ -35,7 +39,9 @@ import type { StartEndRange, PreviewSelection, ThreadsKey, + Thread, } from 'firefox-profiler/types'; +import type { TabSlug } from 'firefox-profiler/app-logic/tabs-handling'; import type { ConnectedProps } from 'firefox-profiler/utils/connect'; @@ -48,6 +54,7 @@ type DispatchProps = { readonly changeRightClickedMarker: typeof changeRightClickedMarker; readonly changeMouseTimePosition: typeof changeMouseTimePosition; readonly changeSelectedMarker: typeof changeSelectedMarker; + readonly updateBottomBoxContentsAndMaybeOpen: typeof updateBottomBoxContentsAndMaybeOpen; }; type StateProps = { @@ -62,6 +69,8 @@ type StateProps = { readonly previewSelection: PreviewSelection | null; readonly rightClickedMarkerIndex: MarkerIndex | null; readonly selectedMarkerIndex: MarkerIndex | null; + readonly thread: Thread; + readonly selectedTab: TabSlug; }; type Props = ConnectedProps<{}, StateProps, DispatchProps>; @@ -171,6 +180,10 @@ class MarkerChartImpl extends React.PureComponent { selectedMarkerIndex, rightClickedMarkerIndex, shouldDisplayTooltips: this._shouldDisplayTooltips, + thread: this.props.thread, + updateBottomBoxContentsAndMaybeOpen: + this.props.updateBottomBoxContentsAndMaybeOpen, + selectedTab: this.props.selectedTab, }} /> @@ -206,6 +219,8 @@ export const MarkerChart = explicitConnect<{}, StateProps, DispatchProps>({ selectedThreadSelectors.getRightClickedMarkerIndex(state), selectedMarkerIndex: selectedThreadSelectors.getSelectedMarkerIndex(state), + thread: selectedThreadSelectors.getThread(state), + selectedTab: getSelectedTab(state), }; }, mapDispatchToProps: { @@ -213,6 +228,7 @@ export const MarkerChart = explicitConnect<{}, StateProps, DispatchProps>({ changeMouseTimePosition, changeRightClickedMarker, changeSelectedMarker, + updateBottomBoxContentsAndMaybeOpen, }, component: MarkerChartImpl, }); diff --git a/src/components/shared/Backtrace.css b/src/components/shared/Backtrace.css index b38ab0bad2..2cb6cf9b76 100644 --- a/src/components/shared/Backtrace.css +++ b/src/components/shared/Backtrace.css @@ -30,3 +30,13 @@ .backtraceStackFrame_isFrameLabel { color: rgb(0 0 0 / 0.6); } + +.backtraceStackFrame_link { + display: block; + color: inherit; + text-decoration: none; +} + +.backtraceStackFrame_link:hover { + background-color: rgb(0 0 0 / 0.05); +} diff --git a/src/components/shared/Backtrace.tsx b/src/components/shared/Backtrace.tsx index 7fce17be26..661eb8fd4d 100644 --- a/src/components/shared/Backtrace.tsx +++ b/src/components/shared/Backtrace.tsx @@ -2,6 +2,7 @@ * 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 classNames from 'classnames'; import { Localized } from '@fluent/react'; import { getBacktraceItemsForStack } from 'firefox-profiler/profile-logic/transforms'; @@ -24,68 +25,115 @@ type Props = { readonly stackIndex: IndexIntoStackTable; readonly implementationFilter: ImplementationFilter; readonly categories: CategoryList; + readonly onStackFrameClick?: (stackIndex: IndexIntoStackTable) => void; }; -export function Backtrace(props: Props) { - const { stackIndex, thread, implementationFilter, maxStacks, categories } = - props; - const funcNamesAndOrigins = getBacktraceItemsForStack( - stackIndex, - implementationFilter, - thread - ); +export class Backtrace extends React.PureComponent { + _handleStackFrameClick = (event: React.MouseEvent) => { + event.preventDefault(); + const stackIdx = event.currentTarget.getAttribute('data-stackindex'); + this.props.onStackFrameClick!(Number(stackIdx)); + }; - if (funcNamesAndOrigins.length) { - return ( -
    - {funcNamesAndOrigins - // Truncate the stacks - .slice(0, maxStacks) - .map( - ({ funcName, origin, isFrameLabel, category, inlineDepth }, i) => ( -
  • - - {inlineDepth > 0 ? ( - + {backtraceItems + // Truncate the stacks + .slice(0, maxStacks) + .map( + ( + { + funcName, + origin, + isFrameLabel, + category, + inlineDepth, + stackIndex, + }, + i + ) => { + const frameContent = ( + <> + + {inlineDepth > 0 ? ( + + + (inlined) + + + ) : null} + {funcName} + {origin} + + ); + + return ( +
  • - - (inlined) - - - ) : null} - {funcName} - {origin} -
  • - ) - )} - {funcNamesAndOrigins.length > maxStacks - ? [ - , - '…', - ] - : null} -
+ {onStackFrameClick && stackIndex !== null ? ( +
+ {frameContent} + + ) : ( + frameContent + )} + + ); + } + )} + {backtraceItems.length > maxStacks + ? [ + , + '…', + ] + : null} + + ); + } + return ( +
+ (The stack is empty because all of its frames are filtered out by the + implementation filter. Switch the implementation filter in the call tree + to see more frames.) +
); } - return ( -
- (The stack is empty because all of its frames are filtered out by the - implementation filter. Switch the implementation filter in the call tree - to see more frames.) -
- ); } diff --git a/src/components/shared/CodeView.css b/src/components/shared/CodeView.css index 4d47ab4139..0798eb14ad 100644 --- a/src/components/shared/CodeView.css +++ b/src/components/shared/CodeView.css @@ -125,6 +125,10 @@ background-color: #edf6ff; } +.cm-highlightedLine { + background-color: #fffbcc; +} + .cm-content { font-family: ui-monospace, 'Roboto Mono', monospace; hyphens: none; diff --git a/src/components/shared/SourceView-codemirror.ts b/src/components/shared/SourceView-codemirror.ts index 446d78b745..dfed4e60fa 100644 --- a/src/components/shared/SourceView-codemirror.ts +++ b/src/components/shared/SourceView-codemirror.ts @@ -31,12 +31,16 @@ import type { LineTimings } from 'firefox-profiler/types'; import { timingsExtension, updateTimingsEffect, + createHighlightedLineExtension, } from 'firefox-profiler/utils/codemirror-shared'; // This "compartment" allows us to swap the syntax highlighting language when // the file path changes. const languageConf = new Compartment(); +// This "compartment" allows us to swap the highlighted line when it changes. +const highlightedLineConf = new Compartment(); + // Detect the right language based on the file extension. function _languageExtForPath( path: string | null @@ -89,6 +93,7 @@ export class SourceViewEditor { initialText: string, path: string, timings: LineTimings, + highlightedLine: number | null, domParent: Element ) { let state = EditorState.create({ @@ -97,6 +102,7 @@ export class SourceViewEditor { timingsExtension, lineNumbers(), languageConf.of(_languageExtForPath(path)), + highlightedLineConf.of(createHighlightedLineExtension(highlightedLine)), syntaxHighlighting(classHighlighter), codeViewerExtension, ], @@ -137,6 +143,15 @@ export class SourceViewEditor { }); } + setHighlightedLine(lineNumber: number | null) { + // Update the highlighted line by reconfiguring the compartment. + this._view.dispatch({ + effects: highlightedLineConf.reconfigure( + createHighlightedLineExtension(lineNumber) + ), + }); + } + scrollToLine(lineNumber: number) { // Clamp the line number to the document's line count. lineNumber = clamp(lineNumber, 1, this._view.state.doc.lines); diff --git a/src/components/shared/SourceView.tsx b/src/components/shared/SourceView.tsx index 7add445b0c..7563e42d79 100644 --- a/src/components/shared/SourceView.tsx +++ b/src/components/shared/SourceView.tsx @@ -43,7 +43,9 @@ type SourceViewProps = { readonly disableOverscan: boolean; readonly filePath: string | null; readonly scrollToHotSpotGeneration: number; + readonly scrollToLineNumber?: number; readonly hotSpotTimings: LineTimings; + readonly highlightedLine?: number; }; let editorModulePromise: Promise | null = null; @@ -140,10 +142,16 @@ export class SourceView extends React.PureComponent { this._getSourceCodeOrFallback(), this.props.filePath, this.props.timings, + this.props.highlightedLine ?? null, domParent ); this._editor = editor; - this._scrollToHotSpot(this.props.hotSpotTimings); + // 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); + } })(); } @@ -174,11 +182,20 @@ export class SourceView extends React.PureComponent { this.props.scrollToHotSpotGeneration !== prevProps.scrollToHotSpotGeneration ) { - this._scrollToHotSpot(this.props.hotSpotTimings); + // 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); + } } if (this.props.timings !== prevProps.timings) { this._editor.setTimings(this.props.timings); } + + if (this.props.highlightedLine !== prevProps.highlightedLine) { + this._editor.setHighlightedLine(this.props.highlightedLine ?? null); + } } } diff --git a/src/components/shared/chart/Canvas.tsx b/src/components/shared/chart/Canvas.tsx index beac8505f3..a7b6c86509 100644 --- a/src/components/shared/chart/Canvas.tsx +++ b/src/components/shared/chart/Canvas.tsx @@ -370,13 +370,13 @@ export class ChartCanvas extends React.Component< this.state.selectedItem !== null && prevState.selectedItem === this.state.selectedItem ) { - // The props have changed but not the selectedItem. This mean that the - // selected item can get out of sync. Invalidate it to make sure that - // it's always fresh. This setState will cause a rerender, but we have - // to do it to prevent any crashes or incorrect tooltip positions. - // This is okay to do it because the main `prevProps !== this.props` - // check above will return false and will not schedule additional drawing. - this.setState({ selectedItem: null }); + // The props have changed but not the selectedItem. Check if it's still valid + // by attempting to get its info. If it returns null, the item is no longer + // valid (e.g., filtered out). + const info = this.props.getHoveredItemInfo(this.state.selectedItem); + if (info === null) { + this.setState({ selectedItem: null }); + } } this._scheduleDraw(); } else if ( diff --git a/src/components/sidebar/MarkerSidebar.tsx b/src/components/sidebar/MarkerSidebar.tsx index ca661dab3c..0ffbd9db69 100644 --- a/src/components/sidebar/MarkerSidebar.tsx +++ b/src/components/sidebar/MarkerSidebar.tsx @@ -7,21 +7,46 @@ import { Localized } from '@fluent/react'; import explicitConnect from 'firefox-profiler/utils/connect'; import { selectedThreadSelectors } from 'firefox-profiler/selectors/per-thread'; -import { getSelectedThreadsKey } from 'firefox-profiler/selectors/url-state'; +import { + getSelectedThreadsKey, + getSelectedTab, +} 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 type { ConnectedProps } from 'firefox-profiler/utils/connect'; -import type { ThreadsKey, Marker, MarkerIndex } from 'firefox-profiler/types'; +import type { + ThreadsKey, + Marker, + MarkerIndex, + IndexIntoStackTable, + Thread, +} from 'firefox-profiler/types'; +import type { TabSlug } from 'firefox-profiler/app-logic/tabs-handling'; type StateProps = { readonly selectedThreadsKey: ThreadsKey; readonly marker: Marker | null; readonly markerIndex: MarkerIndex | null; + readonly thread: Thread; + readonly selectedTab: TabSlug; }; -type Props = ConnectedProps<{}, StateProps, {}>; +type DispatchProps = { + readonly updateBottomBoxContentsAndMaybeOpen: typeof updateBottomBoxContentsAndMaybeOpen; +}; + +type Props = ConnectedProps<{}, StateProps, DispatchProps>; class MarkerSidebarImpl extends React.PureComponent { + _onStackFrameClick = (stackIndex: IndexIntoStackTable) => { + const { thread, selectedTab, updateBottomBoxContentsAndMaybeOpen } = + this.props; + const bottomBoxInfo = getBottomBoxInfoForStackFrame(stackIndex, thread); + updateBottomBoxContentsAndMaybeOpen(selectedTab, bottomBoxInfo); + }; + override render() { const { marker, markerIndex, selectedThreadsKey } = this.props; @@ -45,6 +70,7 @@ class MarkerSidebarImpl extends React.PureComponent { marker={marker} threadsKey={selectedThreadsKey} restrictHeightWidth={false} + onStackFrameClick={this._onStackFrameClick} /> @@ -52,11 +78,16 @@ class MarkerSidebarImpl extends React.PureComponent { } } -export const MarkerSidebar = explicitConnect<{}, StateProps, {}>({ +export const MarkerSidebar = explicitConnect<{}, StateProps, DispatchProps>({ mapStateToProps: (state) => ({ marker: selectedThreadSelectors.getSelectedMarker(state), markerIndex: selectedThreadSelectors.getSelectedMarkerIndex(state), selectedThreadsKey: getSelectedThreadsKey(state), + thread: selectedThreadSelectors.getThread(state), + selectedTab: getSelectedTab(state), }), + mapDispatchToProps: { + updateBottomBoxContentsAndMaybeOpen, + }, component: MarkerSidebarImpl, }); diff --git a/src/components/stack-chart/Canvas.tsx b/src/components/stack-chart/Canvas.tsx index d158b1d7ff..0c113a3872 100644 --- a/src/components/stack-chart/Canvas.tsx +++ b/src/components/stack-chart/Canvas.tsx @@ -8,7 +8,7 @@ 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 { formatMilliseconds } from '../../utils/format-numbers'; +import { formatMilliseconds, formatBytes } from '../../utils/format-numbers'; import { bisectionLeft, bisectionRight } from '../../utils/bisect'; import type { updatePreviewSelection, @@ -36,6 +36,7 @@ import type { Marker, InnerWindowID, Page, + TimelineUnit, } from 'firefox-profiler/types'; import type { CallNodeInfo } from 'firefox-profiler/profile-logic/call-node-info'; @@ -77,6 +78,7 @@ type OwnProps = { readonly marginLeft: CssPixels; readonly displayStackType: boolean; readonly useStackChartSameWidths: boolean; + readonly timelineUnit: TimelineUnit; }; type Props = Readonly< @@ -617,6 +619,12 @@ class StackChartCanvasImpl extends React.PureComponent { const duration = timing.end[stackTimingIndex] - timing.start[stackTimingIndex]; + const { timelineUnit } = this.props; + const durationText = + timelineUnit === 'bytes' + ? formatBytes(duration) + : formatMilliseconds(duration); + return ( { categories={categories} // The stack chart doesn't support other call tree summary types. callTreeSummaryStrategy="timing" - durationText={formatMilliseconds(duration)} + durationText={durationText} displayStackType={displayStackType} /> ); diff --git a/src/components/stack-chart/index.tsx b/src/components/stack-chart/index.tsx index d2fdf4fa5e..ad706dd84c 100644 --- a/src/components/stack-chart/index.tsx +++ b/src/components/stack-chart/index.tsx @@ -17,6 +17,7 @@ import { getCategories, getInnerWindowIDToPageMap, getProfileUsesMultipleStackTypes, + getProfileTimelineUnit, } from '../../selectors/profile'; import { getStackChartSameWidths, @@ -55,6 +56,7 @@ import type { ThreadsKey, InnerWindowID, Page, + TimelineUnit, } from 'firefox-profiler/types'; import type { CallNodeInfo } from 'firefox-profiler/profile-logic/call-node-info'; @@ -84,6 +86,7 @@ type StateProps = { readonly displayStackType: boolean; readonly hasFilteredCtssSamples: boolean; readonly useStackChartSameWidths: boolean; + readonly timelineUnit: TimelineUnit; }; type DispatchProps = { @@ -223,6 +226,7 @@ class StackChartImpl extends React.PureComponent { displayStackType, hasFilteredCtssSamples, useStackChartSameWidths, + timelineUnit, } = this.props; const maxViewportHeight = combinedTimingRows.length * STACK_FRAME_HEIGHT; @@ -282,6 +286,7 @@ class StackChartImpl extends React.PureComponent { marginLeft: TIMELINE_MARGIN_LEFT, displayStackType: displayStackType, useStackChartSameWidths, + timelineUnit, }} /> @@ -324,6 +329,7 @@ export const StackChart = explicitConnect<{}, StateProps, DispatchProps>({ hasFilteredCtssSamples: selectedThreadSelectors.getHasFilteredCtssSamples(state), useStackChartSameWidths: getStackChartSameWidths(state), + timelineUnit: getProfileTimelineUnit(state), }; }, mapDispatchToProps: { diff --git a/src/components/timeline/FullTimeline.tsx b/src/components/timeline/FullTimeline.tsx index 32fa0629b2..97eb3b65d3 100644 --- a/src/components/timeline/FullTimeline.tsx +++ b/src/components/timeline/FullTimeline.tsx @@ -19,7 +19,7 @@ import { getProfileTimelineUnit, getGlobalTracks, getGlobalTrackReferences, - getHiddenTrackCount, + getTrackCount, getGlobalTrackOrder, getPanelLayoutGeneration, } from 'firefox-profiler/selectors'; @@ -39,7 +39,7 @@ import type { GlobalTrack, InitialSelectedTrackReference, GlobalTrackReference, - HiddenTrackCount, + TrackCount, Milliseconds, StartEndRange, TimelineUnit, @@ -60,7 +60,7 @@ type StateProps = { readonly panelLayoutGeneration: number; readonly zeroAt: Milliseconds; readonly profileTimelineUnit: TimelineUnit; - readonly hiddenTrackCount: HiddenTrackCount; + readonly trackCount: TrackCount; }; type DispatchProps = { @@ -75,7 +75,7 @@ type State = { }; class TimelineSettingsHiddenTracks extends React.PureComponent<{ - readonly hiddenTrackCount: HiddenTrackCount; + readonly trackCount: TrackCount; readonly changeRightClickedTrack: typeof changeRightClickedTrack; }> { _showMenu = (event: React.MouseEvent) => { @@ -90,7 +90,7 @@ class TimelineSettingsHiddenTracks extends React.PureComponent<{ }; override render() { - const { hiddenTrackCount } = this.props; + const { trackCount } = this.props; return ( , }} vars={{ - visibleTrackCount: hiddenTrackCount.total - hiddenTrackCount.hidden, - totalTrackCount: hiddenTrackCount.total, + visibleTrackCount: trackCount.total - trackCount.hidden, + totalTrackCount: trackCount.total, }} > @@ -146,7 +146,7 @@ class FullTimelineImpl extends React.PureComponent { width, globalTrackReferences, panelLayoutGeneration, - hiddenTrackCount, + trackCount, changeRightClickedTrack, innerElementRef, } = this.props; @@ -155,10 +155,14 @@ class FullTimelineImpl extends React.PureComponent { <>
- + {trackCount.total > 1 ? ( + + ) : ( +
+ )} { - + {trackCount.total > 1 ? : null} ); } @@ -211,7 +215,7 @@ export const FullTimeline = explicitConnect< zeroAt: getZeroAt(state), profileTimelineUnit: getProfileTimelineUnit(state), panelLayoutGeneration: getPanelLayoutGeneration(state), - hiddenTrackCount: getHiddenTrackCount(state), + trackCount: getTrackCount(state), }), mapDispatchToProps: { changeGlobalTrackOrder, diff --git a/src/components/timeline/GlobalTrack.tsx b/src/components/timeline/GlobalTrack.tsx index e560a1433d..a8046f1368 100644 --- a/src/components/timeline/GlobalTrack.tsx +++ b/src/components/timeline/GlobalTrack.tsx @@ -70,6 +70,7 @@ type StateProps = { readonly selectedTab: TabSlug; readonly processesWithMemoryTrack: Set; readonly progressGraphData: ProgressGraphData[] | null; + readonly totalTrackCount: number; }; type DispatchProps = { @@ -243,6 +244,7 @@ class GlobalTrackComponent extends PureComponent { localTracks, pid, globalTrack, + totalTrackCount, } = this.props; if (isHidden) { @@ -290,17 +292,19 @@ class GlobalTrackComponent extends PureComponent {
) : null} - -
void; + readonly showKeys?: boolean; }; type StateProps = { @@ -272,8 +277,10 @@ class MarkerTooltipContents extends React.PureComponent { continue; } + // When Alt is pressed (showKeys is true), display the field key instead of label + const displayLabel = this.props.showKeys ? key : label || key; details.push( - + {formatMarkupFromMarkerSchema( schema.name, format, @@ -428,6 +435,7 @@ class MarkerTooltipContents extends React.PureComponent { implementationFilter, restrictHeightWidth, categories, + onStackFrameClick, } = this.props; const { data, start } = marker; if (!data || !('cause' in data) || !data.cause) { @@ -463,6 +471,7 @@ class MarkerTooltipContents extends React.PureComponent { thread={thread} implementationFilter={implementationFilter} categories={categories} + onStackFrameClick={onStackFrameClick} />
, @@ -549,7 +558,7 @@ class MarkerTooltipContents extends React.PureComponent { } } -export const TooltipMarker = explicitConnect< +const ConnectedMarkerTooltipContents = explicitConnect< OwnProps, StateProps, DispatchProps @@ -574,3 +583,9 @@ export const TooltipMarker = explicitConnect< mapDispatchToProps: { changeMarkersSearchString }, component: MarkerTooltipContents, }); + +// Wrapper component that provides the Alt key state +export function TooltipMarker(props: OwnProps) { + const showKeys = useAltKey(); + return ; +} diff --git a/src/hooks/useAltKey.ts b/src/hooks/useAltKey.ts new file mode 100644 index 0000000000..acde68a190 --- /dev/null +++ b/src/hooks/useAltKey.ts @@ -0,0 +1,64 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +import * as React from 'react'; + +// Global state for Alt key tracking +let isAltPressed = false; +const listeners = new Set<(pressed: boolean) => void>(); + +// Initialize global event listeners once +if (typeof window !== 'undefined') { + const handleKeyDown = (event: KeyboardEvent) => { + if (event.altKey && !isAltPressed) { + isAltPressed = true; + listeners.forEach((listener) => listener(true)); + } + }; + + const handleKeyUp = (event: KeyboardEvent) => { + if (!event.altKey && isAltPressed) { + isAltPressed = false; + listeners.forEach((listener) => listener(false)); + } + }; + + const handleBlur = () => { + // Reset Alt state when window loses focus + if (isAltPressed) { + isAltPressed = false; + listeners.forEach((listener) => listener(false)); + } + }; + + window.addEventListener('keydown', handleKeyDown); + window.addEventListener('keyup', handleKeyUp); + window.addEventListener('blur', handleBlur); +} + +/** + * Custom hook that tracks whether the Alt key is currently pressed. + * Returns true when Alt is pressed, false otherwise. + * The state is global and shared across all component instances. + */ +export function useAltKey(): boolean { + const [altPressed, setAltPressed] = React.useState(isAltPressed); + + React.useEffect(() => { + // Set initial state + setAltPressed(isAltPressed); + + // Subscribe to changes + const listener = (pressed: boolean) => { + setAltPressed(pressed); + }; + listeners.add(listener); + + return () => { + listeners.delete(listener); + }; + }, []); + + return altPressed; +} diff --git a/src/profile-logic/global-data-collector.ts b/src/profile-logic/global-data-collector.ts index 8f9899529c..c23f0af7c9 100644 --- a/src/profile-logic/global-data-collector.ts +++ b/src/profile-logic/global-data-collector.ts @@ -3,6 +3,8 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { StringTable } from '../utils/string-table'; +import { getEmptySourceTable } from './data-structures'; + import type { Lib, LibMapping, @@ -26,7 +28,7 @@ export class GlobalDataCollector { _libKeyToLibIndex: Map = new Map(); _stringArray: string[] = []; _stringTable: StringTable = StringTable.withBackingArray(this._stringArray); - _sources: SourceTable = { length: 0, uuid: [], filename: [] }; + _sources: SourceTable = getEmptySourceTable(); _uuidToSourceIndex: Map = new Map(); _filenameToSourceIndex: Map = new Map(); diff --git a/src/profile-logic/line-timings.ts b/src/profile-logic/line-timings.ts index b806654c24..68c0a679b5 100644 --- a/src/profile-logic/line-timings.ts +++ b/src/profile-logic/line-timings.ts @@ -82,6 +82,10 @@ export function getStackLineInfo( if (sourceIndexOfThisStack === sourceViewSourceIndex) { selfLine = frameTable.line[frame]; + // Fallback to func line info if frame line info is not available + if (selfLine === null) { + selfLine = funcTable.lineNumber[func]; + } if (selfLine !== null) { // Add this stack's line to this stack's totalLines. The rest of this stack's // totalLines is the same as for the parent stack. @@ -120,6 +124,7 @@ export function getStackLineInfo( export function getStackLineInfoForCallNode( stackTable: StackTable, frameTable: FrameTable, + funcTable: FuncTable, callNodeIndex: IndexIntoCallNodeTable, callNodeInfo: CallNodeInfo ): StackLineInfo { @@ -128,12 +133,14 @@ export function getStackLineInfoForCallNode( ? getStackLineInfoForCallNodeInverted( stackTable, frameTable, + funcTable, callNodeIndex, callNodeInfoInverted ) : getStackLineInfoForCallNodeNonInverted( stackTable, frameTable, + funcTable, callNodeIndex, callNodeInfo ); @@ -205,6 +212,7 @@ export function getStackLineInfoForCallNode( export function getStackLineInfoForCallNodeNonInverted( stackTable: StackTable, frameTable: FrameTable, + funcTable: FuncTable, callNodeIndex: IndexIntoCallNodeTable, callNodeInfo: CallNodeInfo ): StackLineInfo { @@ -229,6 +237,11 @@ export function getStackLineInfoForCallNodeNonInverted( // 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]); } @@ -280,6 +293,7 @@ export function getStackLineInfoForCallNodeNonInverted( export function getStackLineInfoForCallNodeInverted( stackTable: StackTable, frameTable: FrameTable, + funcTable: FuncTable, callNodeIndex: IndexIntoCallNodeTable, callNodeInfo: CallNodeInfoInverted ): StackLineInfo { @@ -318,7 +332,12 @@ export function getStackLineInfoForCallNodeInverted( // 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. - const line = frameTable.line[frameForCallNode]; + 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) { diff --git a/src/profile-logic/merge-compare.ts b/src/profile-logic/merge-compare.ts index be1bdaabdd..4cdfe05f76 100644 --- a/src/profile-logic/merge-compare.ts +++ b/src/profile-logic/merge-compare.ts @@ -19,6 +19,7 @@ import { getEmptyRawMarkerTable, getEmptySamplesTableWithEventDelay, shallowCloneRawMarkerTable, + getEmptySourceTable, } from './data-structures'; import { filterRawThreadSamplesToRange, @@ -499,7 +500,7 @@ function mergeSources( sources: SourceTable; translationMaps: TranslationMapForSources[]; } { - const newSources: SourceTable = { length: 0, uuid: [], filename: [] }; + const newSources = getEmptySourceTable(); const mapOfInsertedSources: Map = new Map(); const translationMaps = sourcesPerProfile.map((sources, profileIndex) => { diff --git a/src/profile-logic/mozilla-symbolication-api.ts b/src/profile-logic/mozilla-symbolication-api.ts index 2c8ecfc092..98c2cbee63 100644 --- a/src/profile-logic/mozilla-symbolication-api.ts +++ b/src/profile-logic/mozilla-symbolication-api.ts @@ -1,6 +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 * as v from 'valibot'; import type { AddressResult, LibSymbolicationRequest, @@ -28,156 +29,68 @@ export type QuerySymbolicationApiCallback = ( requestJson: string ) => Promise; -type APIFoundModulesV5 = { - // For every requested library in the memoryMap, this object contains a string - // key of the form `${debugName}/${breakpadId}`. The value is null if no - // address with the module index was requested, and otherwise a boolean that - // says whether the symbol server had symbols for this library. - [key: string]: null | boolean; -}; +// Valibot schemas for API response validation -type APIInlineFrameInfoV5 = { +// For every requested library in the memoryMap, this object contains a string +// key of the form `${debugName}/${breakpadId}`. The value is null if no +// address with the module index was requested, and otherwise a boolean that +// says whether the symbol server had symbols for this library. +const APIFoundModulesV5Schema = v.record(v.string(), v.nullable(v.boolean())); + +// Information about functions that were inlined at this address. +const APIInlineFrameInfoV5Schema = v.object({ // The name of the function this inline frame was in, if known. - function?: string; + function: v.optional(v.string()), // The path of the file that contains the function this inline frame was in, optional. - file?: string; + file: v.optional(v.string()), // The line number that contains the source code for this inline frame that // contributed to the instruction at the looked-up address, optional. // e.g. 543 - line?: number; -}; + line: v.optional(v.number()), +}); -type APIFrameInfoV5 = { +const APIFrameInfoV5Schema = v.object({ // The hex version of the address that we requested (e.g. "0x5ab"). - module_offset: string; + module_offset: v.string(), // The debugName of the library that this frame was in. - module: string; + module: v.string(), // The index of this APIFrameInfo in its enclosing APIStack. - frame: number; + frame: v.number(), // The name of the function this frame was in, if symbols were found. - function?: string; + function: v.optional(v.string()), // The hex offset between the requested address and the start of the function, // e.g. "0x3c". - function_offset?: string; + function_offset: v.optional(v.string()), // An optional size, in bytes, of the machine code of the outer function that // this address belongs to, as a hex string, e.g. "0x270". - function_size?: string; + function_size: v.optional(v.string()), // The path of the file that contains the function this frame was in, optional. - // As of June 2021, this is only supported on the staging symbolication server - // ("Eliot") but not on the implementation that's currently in production ("Tecken"). - // e.g. "hg:hg.mozilla.org/mozilla-central:js/src/vm/Interpreter.cpp:24938c537a55f9db3913072d33b178b210e7d6b5" - file?: string; + file: v.optional(v.string()), // The line number that contains the source code that generated the instructions at the address, optional. - // (Same support as file.) - // e.g. 543 - line?: number; + line: v.optional(v.number()), // Information about functions that were inlined at this address. // Ordered from inside to outside. - // As of November 2021, this is only supported by profiler-symbol-server. - // Adding this functionality to the Mozilla symbol server is tracked in - // https://bugzilla.mozilla.org/show_bug.cgi?id=1636194 - inlines?: APIInlineFrameInfoV5[]; -}; + inlines: v.optional(v.array(APIInlineFrameInfoV5Schema)), +}); -type APIStackV5 = APIFrameInfoV5[]; +const APIStackV5Schema = v.array(APIFrameInfoV5Schema); -type APIJobResultV5 = { - found_modules: APIFoundModulesV5; - stacks: APIStackV5[]; -}; +const APIJobResultV5Schema = v.object({ + found_modules: APIFoundModulesV5Schema, + stacks: v.array(APIStackV5Schema), +}); -type APIResultV5 = { - results: APIJobResultV5[]; -}; +const APIResultV5Schema = v.object({ + results: v.array(APIJobResultV5Schema), +}); -// Make sure that the JSON blob we receive from the API conforms to our flow -// type definition. -function _ensureIsAPIResultV5(result: unknown): APIResultV5 { - // It's possible (especially when running tests with Jest) that the parameter - // inherits from a `Object` global from another realm. By using toString - // this issue is solved wherever the parameter comes from. - const isObject = (subject: unknown) => - Object.prototype.toString.call(subject) === '[object Object]'; +type APIJobResultV5 = v.InferOutput; +type APIResultV5 = v.InferOutput; - if (!isObject(result) || !('results' in (result as object))) { - throw new Error('Expected an object with property `results`'); - } - const results = (result as { results: unknown }).results; - if (!Array.isArray(results)) { - throw new Error('Expected `results` to be an array'); - } - for (const jobResult of results) { - if ( - !isObject(jobResult) || - !('found_modules' in jobResult) || - !('stacks' in jobResult) - ) { - throw new Error( - 'Expected jobResult to have `found_modules` and `stacks` properties' - ); - } - const found_modules = jobResult.found_modules; - if (!isObject(found_modules)) { - throw new Error('Expected `found_modules` to be an object'); - } - const stacks = jobResult.stacks; - if (!Array.isArray(stacks)) { - throw new Error('Expected `stacks` to be an array'); - } - for (const stack of stacks) { - if (!Array.isArray(stack)) { - throw new Error('Expected `stack` to be an array'); - } - for (const frameInfo of stack) { - if (!isObject(frameInfo)) { - throw new Error('Expected `frameInfo` to be an object'); - } - if ( - !('module_offset' in frameInfo) || - !('module' in frameInfo) || - !('frame' in frameInfo) - ) { - throw new Error( - 'Expected frameInfo to have `module_offset`, `module` and `frame` properties' - ); - } - if ('file' in frameInfo && typeof frameInfo.file !== 'string') { - throw new Error('Expected frameInfo.file to be a string, if present'); - } - if ('line' in frameInfo && typeof frameInfo.line !== 'number') { - throw new Error('Expected frameInfo.line to be a number, if present'); - } - if ( - 'function_offset' in frameInfo && - typeof frameInfo.function_offset !== 'string' - ) { - throw new Error( - 'Expected frameInfo.function_offset to be a string, if present' - ); - } - if ( - 'function_size' in frameInfo && - typeof frameInfo.function_size !== 'string' - ) { - throw new Error( - 'Expected frameInfo.function_size to be a string, if present' - ); - } - if ('inlines' in frameInfo) { - const inlines = frameInfo.inlines; - if (!Array.isArray(inlines)) { - throw new Error('Expected `inlines` to be an array'); - } - for (const inlineFrame of inlines) { - if (!isObject(inlineFrame)) { - throw new Error('Expected `inlineFrame` to be an object'); - } - } - } - } - } - } - return result as APIResultV5; +// Make sure that the JSON blob we receive from the API conforms to our +// type definition using valibot validation. +function _ensureIsAPIResultV5(result: unknown): APIResultV5 { + return v.parse(APIResultV5Schema, result); } function getV5ResultForLibRequest( diff --git a/src/profile-logic/profile-data.ts b/src/profile-logic/profile-data.ts index 7c9b4911ae..51a54457e1 100644 --- a/src/profile-logic/profile-data.ts +++ b/src/profile-logic/profile-data.ts @@ -4005,6 +4005,57 @@ export function getBottomBoxInfoForCallNode( }; } +/** + * 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/transforms.ts b/src/profile-logic/transforms.ts index 0e52d6429b..47f6001661 100644 --- a/src/profile-logic/transforms.ts +++ b/src/profile-logic/transforms.ts @@ -1591,6 +1591,7 @@ export type BacktraceItem = { origin: string; // The inline depth of this frame. Frames with inline depth > 0 are inlined. inlineDepth: number; + stackIndex: IndexIntoStackTable; }; /** @@ -1619,6 +1620,7 @@ export function getBacktraceItemsForStack( frameLine: frameTable.line[frameIndex], frameColumn: frameTable.column[frameIndex], inlineDepth: frameTable.inlineDepth[frameIndex], + stackIndex, }); } @@ -1627,7 +1629,14 @@ export function getBacktraceItemsForStack( funcMatchesImplementation(thread, funcIndex) ); return path.map( - ({ category, funcIndex, frameLine, frameColumn, inlineDepth }) => { + ({ + category, + funcIndex, + frameLine, + frameColumn, + inlineDepth, + stackIndex, + }) => { return { funcName: stringTable.getString(funcTable.name[funcIndex]), category, @@ -1642,6 +1651,7 @@ export function getBacktraceItemsForStack( frameColumn ), inlineDepth, + stackIndex, }; } ); diff --git a/src/reducers/app.ts b/src/reducers/app.ts index d3cff8785e..5c3653dfd9 100644 --- a/src/reducers/app.ts +++ b/src/reducers/app.ts @@ -3,7 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { combineReducers } from 'redux'; -import { tabSlugs } from '../app-logic/tabs-handling'; +import { tabSlugs, tabsShowingSampleData } from '../app-logic/tabs-handling'; import type { TabSlug } from '../app-logic/tabs-handling'; import type { BrowserConnectionStatus } from '../app-logic/browser-connection'; @@ -170,6 +170,10 @@ const lastVisibleThreadTabSlug: Reducer = ( } return state; case 'FOCUS_CALL_TREE': + if (tabsShowingSampleData.includes(state)) { + // Don't switch to call tree if the tab is already showing sample data. + return state; + } return 'calltree'; default: return state; diff --git a/src/reducers/url-state.ts b/src/reducers/url-state.ts index 9469e9a90e..bebe69468d 100644 --- a/src/reducers/url-state.ts +++ b/src/reducers/url-state.ts @@ -5,7 +5,7 @@ import { combineReducers } from 'redux'; import { oneLine } from 'common-tags'; import { objectEntries } from '../utils/types'; -import { tabSlugs } from '../app-logic/tabs-handling'; +import { tabSlugs, tabsShowingSampleData } from '../app-logic/tabs-handling'; import type { ThreadIndex, @@ -98,6 +98,10 @@ const selectedTab: Reducer = (state = 'calltree', action) => { case 'CHANGE_TAB_FILTER': return action.selectedTab; case 'FOCUS_CALL_TREE': + if (tabsShowingSampleData.includes(state)) { + // Don't switch to call tree if the tab is already showing sample data. + return state; + } return 'calltree'; default: return state; @@ -570,6 +574,7 @@ const sourceView: Reducer = ( scrollGeneration: state.scrollGeneration + 1, libIndex: action.libIndex, sourceIndex: action.sourceIndex, + lineNumber: action.lineNumber, }; } default: diff --git a/src/selectors/code.tsx b/src/selectors/code.tsx index 210d102046..adb5a46152 100644 --- a/src/selectors/code.tsx +++ b/src/selectors/code.tsx @@ -35,13 +35,23 @@ const getAssemblyViewNativeSymbolLib: Selector = createSelector( getAssemblyViewNativeSymbol, getProfileOrNull, (nativeSymbol, profile) => { - if (profile === null || nativeSymbol === null) { + if ( + profile === null || + nativeSymbol === null || + profile.libs.length === 0 + ) { return null; } return profile.libs[nativeSymbol.libIndex]; } ); +export const getIsAssemblyViewAvailable: Selector = createSelector( + getAssemblyViewNativeSymbol, + getAssemblyViewNativeSymbolLib, + (nativeSymbol, lib) => nativeSymbol !== null && lib !== null +); + export const getAssemblyViewCode: Selector = createSelector( getAssemblyCodeCache, diff --git a/src/selectors/per-thread/index.ts b/src/selectors/per-thread/index.ts index 3fe4e3917e..df364eccb4 100644 --- a/src/selectors/per-thread/index.ts +++ b/src/selectors/per-thread/index.ts @@ -294,6 +294,7 @@ export const selectedNodeSelectors: NodeSelectors = (() => { return getStackLineInfoForCallNode( stackTable, frameTable, + funcTable, selectedCallNodeIndex, callNodeInfo ); diff --git a/src/selectors/profile.ts b/src/selectors/profile.ts index 38d2e68e9e..a0fa0b175d 100644 --- a/src/selectors/profile.ts +++ b/src/selectors/profile.ts @@ -56,7 +56,7 @@ import type { TrackReference, LastNonShiftClickInformation, PreviewSelection, - HiddenTrackCount, + TrackCount, Selector, DangerousSelectorWithArguments, State, @@ -609,7 +609,7 @@ export const getLocalTrackName = ( * then all its children are as well. This function walks all of the data to determine * the correct hidden counts. */ -export const getHiddenTrackCount: Selector = createSelector( +export const getTrackCount: Selector = createSelector( getGlobalTracks, getLocalTracksByPid, UrlState.getHiddenLocalTracksByPid, diff --git a/src/selectors/url-state.ts b/src/selectors/url-state.ts index ecb21e6103..48b7e17146 100644 --- a/src/selectors/url-state.ts +++ b/src/selectors/url-state.ts @@ -75,6 +75,8 @@ 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 getAssemblyViewIsOpen: Selector = (state) => getProfileSpecificState(state).assemblyView.isOpen; export const getAssemblyViewNativeSymbol: Selector = ( diff --git a/src/test/components/ThreadActivityGraph.test.tsx b/src/test/components/ThreadActivityGraph.test.tsx index a7d83f7cf6..48ceb4a170 100644 --- a/src/test/components/ThreadActivityGraph.test.tsx +++ b/src/test/components/ThreadActivityGraph.test.tsx @@ -264,7 +264,7 @@ describe('ThreadActivityGraph', function () { expect(getCallNodePath()).toEqual([]); }); - it('when clicking a stack, this selects the call tree panel', function () { + it('when clicking a stack while on a tab that does not show sample data, this selects the call tree panel', function () { const { dispatch, getState, clickActivityGraph } = setup(); expect(getSelectedTab(getState())).toBe('calltree'); @@ -277,6 +277,19 @@ describe('ThreadActivityGraph', function () { expect(getLastVisibleThreadTabSlug(getState())).toBe('calltree'); }); + it('when clicking a stack while on a tab that shows sample data, it should not change the selected panel', function () { + const { dispatch, getState, clickActivityGraph } = setup(); + + expect(getSelectedTab(getState())).toBe('calltree'); + dispatch(changeSelectedTab('flame-graph')); + + // The full call node at this sample is: + // A -> B -> C -> F -> G + clickActivityGraph(1, 0.2); + expect(getSelectedTab(getState())).toBe('flame-graph'); + expect(getLastVisibleThreadTabSlug(getState())).toBe('flame-graph'); + }); + it(`when clicking outside of the graph, this doesn't select the call tree panel`, function () { const { dispatch, getState, clickActivityGraph } = setup(); diff --git a/src/test/components/__snapshots__/GlobalTrack.test.tsx.snap b/src/test/components/__snapshots__/GlobalTrack.test.tsx.snap index aec00c6cf8..76a77cfc01 100644 --- a/src/test/components/__snapshots__/GlobalTrack.test.tsx.snap +++ b/src/test/components/__snapshots__/GlobalTrack.test.tsx.snap @@ -589,11 +589,6 @@ exports[`timeline/GlobalTrack will not render markers to the timeline-overview w > Process 0 - - - -