diff --git a/.circleci/config.yml b/.circleci/config.yml index bd3e9d4c51..52b0b859e3 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,7 +3,7 @@ version: 2.1 executors: node: docker: - - image: cimg/node:16.13 + - image: cimg/node:18.14 base: docker: - image: cimg/base:stable diff --git a/.gitpod.yml b/.gitpod.yml index 719e57e45d..6a3361b76b 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -1,7 +1,7 @@ ports: - port: 4242 tasks: -- before: nvm install 16 +- before: nvm install 18 init: yarn install command: FX_PROFILER_HOST="0.0.0.0" yarn start - openMode: split-right diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000000..2ef3430431 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +18.14 diff --git a/.stylelintrc b/.stylelintrc index c81754525b..a667eec955 100644 --- a/.stylelintrc +++ b/.stylelintrc @@ -5,7 +5,6 @@ "extends": [ "stylelint-config-standard", "stylelint-config-idiomatic-order", - "stylelint-config-prettier" ], "rules": { "prettier/prettier": [ diff --git a/appveyor.yml b/appveyor.yml index 9a3f8b4ca0..c686371818 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,6 +1,6 @@ clone_depth: 5 environment: - nodejs_version: "16" + nodejs_version: "18.14" platform: x64 # flow needs 64b platforms branches: @@ -12,12 +12,12 @@ branches: install: # 1. Select the right node # The following command works by fully reinstalling a version of node. It's a - # lot slower than the other command. This needs the full version (eg: 16.12) - # - ps: Update-NodeJsInstallation (Get-NodeJsLatestBuild $env:nodejs_version) $env:platform + # lot slower than the other command. This needs the full version (eg: 18.12) + - ps: Update-NodeJsInstallation (Get-NodeJsLatestBuild $env:nodejs_version) $env:platform # Use the following command if the wanted version is already part of # appveyor's windows machines, see https://www.appveyor.com/docs/windows-images-software/#node-js - # This needs the major version only (eg: 16) - - ps: Install-Product node $env:nodejs_version $env:platform + # This needs the major version only (eg: 18) + # - ps: Install-Product node $env:nodejs_version $env:platform # 2. Setup the project - yarn install --frozen-lockfile diff --git a/bin/pre-install.js b/bin/pre-install.js index f59c8c8120..eb7d13e55a 100644 --- a/bin/pre-install.js +++ b/bin/pre-install.js @@ -60,7 +60,7 @@ function checkNode(agents /*: AgentsVersion */) { 'You can use a tool like `nvm` to install and manage installed node versions.' ); console.error( - 'You can look at https://github.com/creationix/nvm to install this tool.\n' + 'You can look at https://github.com/nvm-sh/nvm to install this tool.\n' ); console.error( 'Once `nvm` is installed you can use the following commands to upgrade:\n' + diff --git a/locales/be/app.ftl b/locales/be/app.ftl index c9227c934e..d61251b09d 100644 --- a/locales/be/app.ftl +++ b/locales/be/app.ftl @@ -51,13 +51,53 @@ AppViewRouter--route-not-found--home = ## This is used as a context menu for the Call Tree, Flame Graph and Stack Chart ## panels. +# Variables: +# $fileName (String) - Name of the file to open. +CallNodeContextMenu--show-file = Паказаць { $fileName } CallNodeContextMenu--transform-merge-function = Аб'яднаць функцыю .title = Аб'яднанне функцыі выдаляе яе з профілю і прызначае яе час функцыі, якая яе выклікала. Гэта адбываецца ўсюды, дзе функцыя была выклікана ў дрэве. +CallNodeContextMenu--transform-merge-call-node = Аб'яднаць толькі вузел + .title = + Аб'яднанне вузла выдаляе яго з профілю і прызначае яго час + вузлу функцыі, які яго выклікаў. Гэта толькі выдаляе функцыю з гэтай + канкрэтнай часткі дрэва. Любыя іншыя месцы, адкуль была выклікана функцыя, + застануцца ў профілі. +# This is used as the context menu item title for "Focus on function" and "Focus +# on function (inverted)" transforms. +CallNodeContextMenu--transform-focus-function-title = + Засяроджванне ўвагі на функцыі выдаліць усе ўзоры, якія не ўключаюць яе + функцыя. Акрамя таго, ён паўторна выкараняе дрэва выклікаў, каб функцыя + з'яўляецца адзіным коранем дрэва. Гэта можа аб'яднаць некалькі сайтаў выкліку функцый + праз профіль у адзін вузел выкліку. CallNodeContextMenu--transform-focus-function = Фокус на функцыі .title = { CallNodeContextMenu--transform-focus-function-title } +CallNodeContextMenu--transform-focus-function-inverted = Фокус на функцыі (інвертавана) + .title = { CallNodeContextMenu--transform-focus-function-title } +CallNodeContextMenu--transform-focus-subtree = Фокус толькі на паддрэве + .title = Фокус на паддрэве прывядзе да выдалення любога ўзору, які не ўключае гэтую канкрэтную частку дрэва выклікаў. Гэта выдаляе галіну дрэва выклікаў, але робіць гэта толькі для аднаго вузла выкліку. Усе іншыя выклікі функцый ігнаруюцца. +# This is used as the context menu item to apply the "Focus on category" transform. +# Variables: +# $categoryName (String) - Name of the category to focus on. +CallNodeContextMenu--transform-focus-category = Фокус на катэгорыі { $categoryName } + .title = + Факусіраванне на вузлах, якія адносяцца да той жа катэгорыі, што і абраны вузел, + такім чынам аб'ядноўваючы ўсе вузлы, якія належаць да іншай катэгорыі. +CallNodeContextMenu--transform-collapse-function-subtree = Згарнуць функцыю + .title = Згортванне функцыі выдаляе ўсё, што яна выклікала, і прызначае ўвесь час гэтай функцыі. Гэта можа дапамагчы спрасціць профіль, які выклікае код, які не трэба аналізаваць. +# This is used as the context menu item to apply the "Collapse resource" transform. +# Variables: +# $nameForResource (String) - Name of the resource to collapse. +CallNodeContextMenu--transform-collapse-resource = Згарнуць { $nameForResource } + .title = Згортванне рэсурсу згладзіць усе выклікі да гэтага рэсурсу ў адзіны згорнуты вузел выкліку. +CallNodeContextMenu--transform-collapse-direct-recursion2 = Згарнуць прамую рэкурсію + .title = Згортванне прамой рэкурсіі выдаляе выклікі, якія шматразова рэкурсіруюць у адну і тую ж функцыю без прамежкавых функцый у стэку. +CallNodeContextMenu--transform-collapse-indirect-recursion = Згарнуць непрамой рэкурсію + .title = Згортванне непрамой рэкурсіі выдаляе выклікі, якія шматразова рэкурсіруюць у адну і тую ж функцыю, нават з прамежкавымі функцыямі ў стэку. +CallNodeContextMenu--transform-drop-function = Адкінуць узоры з гэтай функцыяй + .title = Адкідванне ўзораў выдаляе іх час з профілю. Гэта карысна для выдалення інфармацыі аб часе, які не мае дачынення да аналізу. CallNodeContextMenu--expand-all = Разгарнуць усё # Searchfox is a source code indexing tool for Mozilla Firefox. # See: https://searchfox.org/ @@ -73,10 +113,28 @@ CallTree--tracing-ms-total = Час працы (мс) .title = «Агульны» час працы ўключае суму ўсяго часу, на працягу якога гэта функцыя знаходзілася ў стэку. Сюды ўваходзіць час, - на працягу якога функцыя фактычна выконвалася, а таксама час выканання - функцый, якія вызвала гэта функцыі. + на працягу якога функцыя фактычна выконвалася, а таксама час выканання выкліканых ёю функцый. +CallTree--tracing-ms-self = Уласны (мс) + .title = + "Уласны" час уключае толькі час, калі функцыя была канцом стэка. + Калі гэтая функцыя выклікала іншыя функцыі, то час «іншых» функцый не ўлічваецца. «Уласны» час карысны для разумення таго, на што быў фактычна выдаткаваны час у праграме. CallTree--samples-total = Усяго (узоры) - .title = Лічыльнік “Усяго (узоры)” уключае ў сабе суму кожнага ўзору, у якога гэтая функцыя была выяўлена ў стэку. Сюды ўваходзіць час фактычнай працы функцыі, а таксама час, чакання вызаваў, якія рабіла гэтая функцыя. + .title = Лічыльнік “Усяго (узоры)” уключае ў сабе суму кожнага ўзору, у якога гэтая функцыя была выяўлена ў стэку. Сюды ўваходзіць час фактычнай працы функцыі, а таксама час чакання выкліканых ёю функцый. +CallTree--samples-self = Уласны + .title = + "Уласны" падлік выбарак уключае толькі ўзоры, дзе функцыя была канцом стэка. + Калі гэтая функцыя выклікала іншыя функцыі, то час «іншых» функцый не ўлічваецца. + «Уласны» падлік карысны для таго, каб зразумець, які час на самай справе быў выдаткаваны на праграму. +CallTree--bytes-total = Агульны памер (байты) + .title = + «Агульны памер» уключае суму ўсіх байтаў, выдзеленых або + вызваленых, пакуль гэтая функцыя знаходзілася ў стэку. + Гэта ўключае ў сябе як байты, дзе функцыя фактычна выконвалася, так і байты выкліканых ёю функцый. +CallTree--bytes-self = Уласны (байты) + .title = + "Уласная" колькасць байтаў уключае суму ўсіх байтаў, выдзеленых або вызваленых, калі функцыя знаходзілася ў канцы стэка. + Калі гэтая функцыя выклікае іншыя функцыі, байты "іншых" функцый не ўключаюцца. + «Уласны» падлік байтаў карысны для разумення таго, колькі памяці было фактычна выдзелена або вызвалена ў праграме. ## Call tree "badges" (icons) with tooltips ## @@ -84,15 +142,30 @@ CallTree--samples-total = Усяго (узоры) ## functions for native code (C / C++ / Rust). They're a small "inl" icon with ## a tooltip. +# Variables: +# $calledFunction (String) - Name of the function whose call was sometimes inlined. +CallTree--divergent-inlining-badge = + .title = Некаторыя выклікі { $calledFunction } былі ўбудаваны кампілятарам. +# Variables: +# $calledFunction (String) - Name of the function whose call was inlined. +# $outerFunction (String) - Name of the outer function into which the called function was inlined. +CallTree--inlining-badge = (убудаваны) + .title = Выклікі функціі { $calledFunction } былі ўбудаваны кампілятарам у { $outerFunction }. ## CallTreeSidebar ## This is the sidebar component that is used in Call Tree and Flame Graph panels. +CallTreeSidebar--select-a-node = Выберыце вузел, каб паказаць інфармацыю аб ім. ## CompareHome ## This is used in the page to compare two profiles. ## See: https://profiler.firefox.com/compare/ +CompareHome--instruction-title = Увядзіце URL-адрасы профіляў, якія вы хочаце параўнаць +CompareHome--instruction-content = + Інструмент будзе браць даныя з выбранай дарожкі і дыяпазону для + кожнага профілю і размяшчаць іх у адным выглядзе для зручнага + параўнання. CompareHome--form-label-profile1 = Профіль 1: CompareHome--form-label-profile2 = Профіль 2: CompareHome--submit-button = @@ -102,6 +175,10 @@ CompareHome--submit-button = ## This is displayed at the top of the analysis page when the loaded profile is ## a debug build of Firefox. +DebugWarning--warning-message = + .message = + Гэты профіль быў запісаны для зборцы без фінальных (рэлізных) аптымізацый. + Назіраемая прадукцыйнасць можа адрознівацца ад фінальнай (рэлізнай) зборкі. ## Details ## This is the bottom panel in the analysis UI. They are generic strings to be @@ -129,6 +206,13 @@ FooterLinks--hide-button = ## The timeline component of the full view in the analysis UI at the top of the ## page. +# This string is used as the text of the track selection button. +# Displays the ratio of visible tracks count to total tracks count in the timeline. +# We have spans here to make the numbers bold. +# Variables: +# $visibleTrackCount (Number) - Visible track count in the timeline +# $totalTrackCount (Number) - Total track count in the timeline +FullTimeline--tracks-button = Дарожак: { $visibleTrackCount } / { $totalTrackCount } ## Home page @@ -141,6 +225,27 @@ Home--menu-button = Уключыць кнопку меню { -profiler-brand-nam Home--menu-button-instructions = Уключыце кнопку меню прафайлера, каб пачаць запіс профілю прадукцыйнасці у { -firefox-brand-name }, затым прааналізуйце яго і падзяліцеся з profiler.firefox.com. +Home--profile-firefox-android-instructions = + Вы таксама можаце зрабіць профіль { -firefox-android-brand-name }. Падрабязней + можна даведацца ў дакументацыі: + Прафіляванне { -firefox-android-brand-name } непасрэдна на прыладзе. +# The word WebChannel should not be translated. +# This message can be seen on https://main--perf-html.netlify.app/ in the tooltip +# of the "Enable Firefox Profiler menu button" button. +Home--enable-button-unavailable = + .title = Гэты экзэмпляр прафайлера не змог падключыцца да WebChannel, таму не атрымалася ўключыць кнопку меню прафайлера. +# The word WebChannel, the pref name, and the string "about:config" should not be translated. +# This message can be seen on https://main--perf-html.netlify.app/ . +Home--web-channel-unavailable = + Гэты экзэмпляр прафайлера не змог падключыцца да WebChannel. Звычайна гэта азначае, + што ён працуе на хосце адрозным ад таго, які пазначаны ў параметрах + devtools.performance.recording.ui-base-url. Калі вы хочаце запісаць новыя + профілі з дапамогай гэтага экзэмпляра і даць яму праграмнае кіраванне кнопкай меню + прафайлера, вы можаце перайсці да about:config і змяніць налады. +Home--record-instructions = + Каб пачаць запіс профілю, націсніце кнопку запісу або выкарыстоўвайце + спалучэнне клавіш. Падчас запісу профілю значок стане сіняга колеру. + Націсніце Захапіць, каб запампаваць даныя на profiler.firefox.com. Home--instructions-content = Для запісу профіляў прадукцыйнасці патрабуецца { -firefox-brand-name }. Аднак існуючыя профілі можна праглядаць у любым сучасным браўзеры. @@ -151,6 +256,14 @@ Home--additional-content-title = Загрузіць існуючыя профі Home--additional-content-content = Вы можаце перацягнуць файл профілю сюды, каб загрузіць яго, або: Home--compare-recordings-info = Вы таксама можаце параўнаць запісы. Адкрыць інтэрфейс параўнання. Home--your-recent-uploaded-recordings-title = Вашы нядаўна запампаваныя запісы +# We replace the elements such as and with links to the +# documentation to use these tools. +Home--load-files-from-other-tools = + { -profiler-brand-name } таксама можа імпартаваць профілі з іншых прафайлераў, такіх як + Linux perf, Android SimplePerf, + панэль прадукцыйнасці Chrome, Android Studio або + любога файла, які выкарыстоўвае фармат dhat. Даведайцеся, + як напісаць свой уласны імпарцёр. ## IdleSearchField ## The component that is used for all the search inputs in the application. @@ -176,11 +289,26 @@ ListOfPublishedProfiles--published-profiles-link = ListOfPublishedProfiles--published-profiles-delete-button-disabled = Выдаліць .title = Гэты профіль не можа быць выдалены, таму што мы не маем інфармацыі пра аўтарызацыю. ListOfPublishedProfiles--uploaded-profile-information-list-empty = Ніводнага профілю яшчэ не запампавана! +# This string is used below the 'Your recent uploaded recordings' list section. +# Variables: +# $profilesRestCount (Number) - Remaining numbers of the uploaded profiles which are not listed under 'Your recent uploaded recordings'. +ListOfPublishedProfiles--uploaded-profile-information-label = Прагляд усіх вашых запісаў і кіраванне імі (яшчэ { $profilesRestCount }) +# Depending on the number of uploaded profiles, the message is different. +# Variables: +# $uploadedProfileCount (Number) - Total numbers of the uploaded profiles. +ListOfPublishedProfiles--uploaded-profile-information-list = + { $uploadedProfileCount -> + [one] Кіраваць гэтым запісам + [few] Кіраваць гэтымі запісамі + [many] Кіраваць гэтымі запісамі + *[other] Кіраваць гэтымі запісамі + } ## MarkerContextMenu ## This is used as a context menu for the Marker Chart, Marker Table and Network ## panels. +MarkerContextMenu--set-selection-from-duration = Наладзьце выбарку на аснове працягласці маркера MarkerContextMenu--start-selection-here = Пачаць вылучэнне тут MarkerContextMenu--end-selection-here = Скончыць вылучэнне тут MarkerContextMenu--start-selection-at-marker-start = Пачаць вылучэнне ад пачатку маркера @@ -206,10 +334,14 @@ MarkerContextMenu--select-the-sender-thread = Выберыце паток-адп ## MarkerSettings ## This is used in all panels related to markers. +MarkerSettings--panel-search = + .label = Фільтра маркераў + .title = Паказваць толькі маркеры, якія адпавядаюць пэўнаму імені ## MarkerSidebar ## This is the sidebar component that is used in Marker Table panel. +MarkerSidebar--select-a-marker = Выберыце маркер, каб паглядзець інфармацыі пра яго. ## MarkerTable ## This is the component for Marker Table panel. @@ -234,6 +366,8 @@ MenuButtons--index--share-error-uploading = .label = Памылка запампоўкі MenuButtons--index--revert = Вярнуцца да зыходнага профілю MenuButtons--index--docs = Дакументы +MenuButtons--permalink--button = + .label = Пастаянная спасылка ## MetaInfo panel ## These strings are used in the panel containing the meta information about @@ -243,11 +377,52 @@ MenuButtons--index--profile-info-uploaded-label = Запампавана: MenuButtons--index--profile-info-uploaded-actions = Выдаліць MenuButtons--index--metaInfo-subtitle = Інфармацыя аб профілі MenuButtons--metaInfo--symbols = Сімвалы: +MenuButtons--metaInfo--profile-symbolicated = Профіль сімвалізаваны +MenuButtons--metaInfo--profile-not-symbolicated = Профіль не сімвалізаваны +MenuButtons--metaInfo--resymbolicate-profile = Паўторна сімвалізаваць профіль +MenuButtons--metaInfo--symbolicate-profile = Сімвалізаваць профіль +MenuButtons--metaInfo--attempting-resymbolicate = Спроба паўторна сімвалізаваць профіль +MenuButtons--metaInfo--currently-symbolicating = Зараз профіль сімвалізуецца MenuButtons--metaInfo--cpu-model = Мадэль ЦП: MenuButtons--metaInfo--cpu-cores = Ядра ЦП: MenuButtons--metaInfo--main-memory = Асноўная памяць: MenuButtons--index--show-moreInfo-button = Паказаць больш MenuButtons--index--hide-moreInfo-button = Паказаць менш +# This string is used when we have the information about both physical and +# logical CPU cores. +# Variable: +# $physicalCPUs (Number), $logicalCPUs (Number) - Number of Physical and Logical CPU Cores +MenuButtons--metaInfo--physical-and-logical-cpu = + { $physicalCPUs -> + [one] + { $logicalCPUs -> + [one] { $physicalCPUs } фізічнае ядро, { $logicalCPUs } лагічнае ядро + [few] { $physicalCPUs } фізічнае ядро, { $logicalCPUs } лагічныя ядры + [many] { $physicalCPUs } фізічнае ядро, { $logicalCPUs } лагічных ядзер + *[other] { $physicalCPUs } фізічнае ядро, { $logicalCPUs } лагічных ядзер + } + [few] + { $logicalCPUs -> + [one] { $physicalCPUs } фізічныя ядры, { $logicalCPUs } лагічнае ядро + [few] { $physicalCPUs } фізічныя ядры, { $logicalCPUs } лагічныя ядры + [many] { $physicalCPUs } фізічныя ядры, { $logicalCPUs } лагічных ядзер + *[other] { $physicalCPUs } фізічныя ядры, { $logicalCPUs } лагічных ядзер + } + [many] + { $logicalCPUs -> + [one] { $physicalCPUs } фізічных ядзер, { $logicalCPUs } лагічнае ядро + [few] { $physicalCPUs } фізічных ядзер, { $logicalCPUs } лагічныя ядры + [many] { $physicalCPUs } фізічных ядзер, { $logicalCPUs } лагічных ядзер + *[other] { $physicalCPUs } фізічных ядзер, { $logicalCPUs } лагічных ядзер + } + *[other] + { $logicalCPUs -> + [one] { $physicalCPUs } фізічных ядзер, { $logicalCPUs } лагічнае ядро + [few] { $physicalCPUs } фізічных ядзер, { $logicalCPUs } лагічныя ядры + [many] { $physicalCPUs } фізічных ядзер, { $logicalCPUs } лагічных ядзер + *[other] { $physicalCPUs } фізічных ядзер, { $logicalCPUs } лагічных ядзер + } + } # This string is used when we only have the information about the number of # physical CPU cores. # Variable: @@ -273,6 +448,16 @@ MenuButtons--metaInfo--main-process-ended = Асноўны працэс скон MenuButtons--metaInfo--interval = Інтэрвал: MenuButtons--metaInfo--buffer-capacity = Ёмістасць буфера: MenuButtons--metaInfo--buffer-duration = Працягласць буфера: +# Buffer Duration in Seconds in Meta Info Panel +# Variable: +# $configurationDuration (Number) - Configuration Duration in Seconds +MenuButtons--metaInfo--buffer-duration-seconds = + { $configurationDuration -> + [one] { $configurationDuration } секунда + [few] { $configurationDuration } секунды + [many] { $configurationDuration } секунд + *[other] { $configurationDuration } секунд + } # Adjective refers to the buffer duration MenuButtons--metaInfo--buffer-duration-unlimited = Неабмежавана MenuButtons--metaInfo--application = Праграма @@ -308,20 +493,34 @@ MenuButtons--metaInfo-renderRowOfList-label-extensions = Пашырэнні: ## Overhead refers to the additional resources used to run the profiler. ## These strings are displayed at the bottom of the "Profile Info" panel. +MenuButtons--metaOverheadStatistics-subtitle = Накладныя выдаткі { -profiler-brand-short-name } MenuButtons--metaOverheadStatistics-mean = Сярэдняе MenuButtons--metaOverheadStatistics-max = Макс MenuButtons--metaOverheadStatistics-min = Мін +MenuButtons--metaOverheadStatistics-statkeys-overhead = Накладныя выдаткі + .title = Час затрачаны на атрыманне ўсіх патокаў. +MenuButtons--metaOverheadStatistics-statkeys-cleaning = Ачыстка + .title = Час затрачаны на выдаленне старых даных. MenuButtons--metaOverheadStatistics-statkeys-counter = Лічыльнік .title = Час збору ўсіх лічыльнікаў MenuButtons--metaOverheadStatistics-statkeys-interval = Інтэрвал .title = Зафіксаваны інтэрвал паміж двума ўзорамі +MenuButtons--metaOverheadStatistics-statkeys-lockings = Блакіроўкі + .title = Час затрачаны на атрыманне блакіроўкі перад правядзеннем вымярэнняў. +MenuButtons--metaOverheadStatistics-overhead-duration = Працягласць накладных выдаткаў: +MenuButtons--metaOverheadStatistics-overhead-percentage = Працэнт накладных выдаткаў: MenuButtons--metaOverheadStatistics-profiled-duration = Працягласць запісу профілю: ## Publish panel ## These strings are used in the publishing panel. +MenuButtons--publish--renderCheckbox-label-hidden-threads = Уключыць схаваныя патокі MenuButtons--publish--renderCheckbox-label-include-other-tabs = Уключыць даныя з іншых картак +MenuButtons--publish--renderCheckbox-label-hidden-time = Уключыць схаваны дыяпазон часу +MenuButtons--publish--renderCheckbox-label-include-screenshots = Уключыць здымкі экрана +MenuButtons--publish--renderCheckbox-label-resource = Уключыць URL-адрасы і шляхі рэсурсаў MenuButtons--publish--renderCheckbox-label-extension = Уключыць інфармацыю аб пашырэнні +MenuButtons--publish--renderCheckbox-label-preference = Уключыць значэнні параметраў MenuButtons--publish--renderCheckbox-label-private-browsing = Уключыць даныя з вокнаў прыватнага прагляду MenuButtons--publish--renderCheckbox-label-private-browsing-warning-image = .title = Гэты профіль змяшчае даныя прыватнага прагляду @@ -329,9 +528,12 @@ MenuButtons--publish--reupload-performance-profile = Паўторна запам MenuButtons--publish--share-performance-profile = Абагуліць профіль прадукцыйнасці MenuButtons--publish--info-description = Запампуйце свой профіль і зрабіце яго даступным для ўсіх, хто мае спасылку. MenuButtons--publish--info-description-default = Тыпова вашы асабістыя даныя выдаляюцца. +MenuButtons--publish--info-description-firefox-nightly2 = Гэты профіль ад { -firefox-nightly-brand-name }, таму большая частка інфармацыі ўключана па змаўчанні. +MenuButtons--publish--include-additional-data = Уключыць дадатковыя даныя, якія могуць раскрыць вашу асобу MenuButtons--publish--button-upload = Запампаваць MenuButtons--publish--upload-title = Запампоўванне профілю… MenuButtons--publish--cancel-upload = Скасаваць запампоўку +MenuButtons--publish--message-something-went-wrong = Ой, нешта пайшло не так падчас загрузкі профілю. MenuButtons--publish--message-try-again = Паспрабаваць зноў MenuButtons--publish--download = Спампаваць MenuButtons--publish--compressing = Сцісканне… @@ -339,6 +541,9 @@ MenuButtons--publish--compressing = Сцісканне… ## NetworkSettings ## This is used in the network chart. +NetworkSettings--panel-search = + .label = Фільтраваць сеткі: + .title = Паказваць толькі сеткавыя запыты, якія адпавядаюць пэўнаму імені ## Timestamp formatting primitive @@ -357,6 +562,7 @@ NumberFormat--short-date = { SHORTDATE($date) } ## PanelSearch ## The component that is used for all the search input hints in the application. +PanelSearch--search-field-hint = Вы ведаеце, што для пошуку па некалькіх тэрмінах можна выкарыстоўваць коску (,)? ## Profile Delete Button @@ -371,10 +577,16 @@ ProfileDeleteButton--delete-button = ## This panel is displayed when the user clicks on the Profile Delete Button, ## it's a confirmation dialog. +# This string is used when there's an error while deleting a profile. The link +# will show the error message when hovering. +ProfileDeletePanel--delete-error = Пры выдаленні гэтага профілю адбылася памылка. Навядзіце курсор, каб даведацца больш. # This is the title of the dialog # Variables: # $profileName (string) - Some string that identifies the profile ProfileDeletePanel--dialog-title = Выдаліць { $profileName } +ProfileDeletePanel--dialog-confirmation-question = + Вы ўпэўнены, што хочаце выдаліць запампаваныя даныя для гэтага профілю? Спасылкі, + якія былі абагулены раней, больш не будуць працаваць. ProfileDeletePanel--dialog-cancel-button = .value = Скасаваць ProfileDeletePanel--dialog-delete-button = @@ -399,10 +611,13 @@ ProfileFilterNavigator--full-range-with-duration = Поўны дыяпазон ( ## Profile Loader Animation +ProfileLoaderAnimation--loading-unpublished = Імпарт профілю непасрэдна з { -firefox-brand-name }… +ProfileLoaderAnimation--loading-from-file = Чытанне файла і апрацоўка профілю… ProfileLoaderAnimation--loading-local = Яшчэ не рэалізавана. ProfileLoaderAnimation--loading-public = Спампоўка і апрацоўка профілю… ProfileLoaderAnimation--loading-from-url = Спампоўка і апрацоўка профілю… ProfileLoaderAnimation--loading-compare = Чытанне і апрацоўка профіляў… +ProfileLoaderAnimation--loading-view-not-found = Прагляд не знойдзены ## ProfileRootMessage @@ -413,20 +628,47 @@ ProfileRootMessage--additional = Вярнуцца на галоўную ## This is the component responsible for handling the service worker installation ## and update. It appears at the top of the UI. +ServiceWorkerManager--applying-button = Прымяненне… +ServiceWorkerManager--pending-button = Прымяніць і перазагрузіць ServiceWorkerManager--installed-button = Перазагрузіць праграму +ServiceWorkerManager--updated-while-not-ready = Новая версія праграмы была прыменена да поўнай загрузкі гэтай старонкі. Вы можаце сутыкнуцца з няспраўнасцямі. ServiceWorkerManager--new-version-is-ready = Новая версія праграмы спампавана і гатова да выкарыстання. +ServiceWorkerManager--hide-notice-button = + .title = Схаваць паведамленне аб перазагрузцы + .aria-label = Схаваць паведамленне аб перазагрузцы ## StackSettings ## This is the settings component that is used in Call Tree, Flame Graph and Stack ## Chart panels. It's used to switch between different views of the stack. +StackSettings--implementation-all-stacks = Усе стэкі StackSettings--implementation-javascript = JavaScript +StackSettings--implementation-native = Уласны StackSettings--use-data-source-label = Крыніца даных: +StackSettings--call-tree-strategy-timing = Таймінгі + .title = Стварыць зводку асобных стэкаў кода, выкананых за пэўны перыяд часу +StackSettings--call-tree-strategy-js-allocations = Выдзяленне рэсурсаў JavaScript + .title = Сумаваць выдзеленыя байты JavaScript (без вызвалення) +StackSettings--call-tree-strategy-native-retained-allocations = Утрыманая памяць + .title = Сумаваць байты памяці, якія былі выдзелены, але ніколі не вызваляліся ў бягучым выбары папярэдняга прагляду +StackSettings--call-tree-native-allocations = Выдзеленая памяць + .title = Сумаваць байты выдзеленай памяці +StackSettings--call-tree-strategy-native-deallocations-memory = Вызваленая памяць + .title = Сумаваць байты вызваленай памяці па сайтах, дзе яны былі выдзелены +StackSettings--call-tree-strategy-native-deallocations-sites = Вызваленыя сайты + .title = Сумаваць байты вызваленай памяці па сайтах, дзе яны былі вызвалены +StackSettings--invert-call-stack = Інвертаваць стэк выклікаў + .title = Сартаваць па часе, праведзенаму ў вузле выкліку, ігнаруючы яго даччыныя вузлы. +StackSettings--show-user-timing = Паказаць таймінгі карыстальніка +StackSettings--panel-search = + .label = Фільтр стэкаў: + .title = Паказаць толькі стэкі, якія змяшчаюць функцыю, назва якой адпавядае гэтаму падрадку ## Tab Bar for the bottom half of the analysis UI. TabBar--calltree-tab = Дрэва выклікаў TabBar--flame-graph-tab = Флэйм-дыяграма +TabBar--stack-chart-tab = Дыяграма стэка TabBar--marker-chart-tab = Маркерная дыяграма TabBar--marker-table-tab = Маркерная табліца TabBar--network-tab = Сетка @@ -441,16 +683,39 @@ TrackContextMenu--only-show-this-process = Паказваць толькі гэ # Variables: # $trackName (String) - Name of the selected track to isolate. TrackContextMenu--only-show-track = Паказваць толькі “{ $trackName }” +TrackContextMenu--hide-other-screenshots-tracks = Схаваць дарожкі іншых здымкаў # This is used as the context menu item to hide the given track. # Variables: # $trackName (String) - Name of the selected track to hide. TrackContextMenu--hide-track = Схаваць “{ $trackName }” +TrackContextMenu--show-all-tracks = Паказаць усе дарожкі +TrackContextMenu--show-local-tracks-in-process = Паказаць усе дарожкі ў гэтым працэсе +# This is used in the tracks context menu as a button to show all the tracks +# that match the search filter. +TrackContextMenu--show-all-matching-tracks = Паказаць усе адпаведныя дарожкі +# This is used in the tracks context menu as a button to hide all the tracks +# that match the search filter. +TrackContextMenu--hide-all-matching-tracks = Схаваць усе адпаведныя дарожкі +# This is used in the tracks context menu when the search filter doesn't match +# any track. +# Variables: +# $searchFilter (String) - The search filter string that user enters. +TrackContextMenu--no-results-found = Няма вынікаў для “{ $searchFilter }” +# This button appears when hovering a track name and is displayed as an X icon. +TrackNameButton--hide-track = + .title = Схаваць дарожку +# This button appears when hovering a global track name and is displayed as an X icon. +TrackNameButton--hide-process = + .title = Схаваць працэс ## TrackMemoryGraph ## This is used to show the memory graph of that process in the timeline part of ## the UI. To learn more about it, visit: ## https://profiler.firefox.com/docs/#/./memory-allocations?id=memory-track +TrackMemoryGraph--relative-memory-at-this-time = адносная памяць на гэты момант +TrackMemoryGraph--memory-range-in-graph = дыяпазон памяці ў графіку +TrackMemoryGraph--operations-since-the-previous-sample = аперацый, пачынаючы з папярэдняга ўзору ## TrackPower ## This is used to show the power used by the CPU and other chips in a computer, @@ -462,10 +727,65 @@ TrackContextMenu--hide-track = Схаваць “{ $trackName }” ## consumption. The carbon dioxide equivalent represents the equivalent amount ## of CO₂ to achieve the same level of global warming potential. +# This is used in the tooltip when the power value uses the watt unit. +# Variables: +# $value (String) - the power value at this location +TrackPower--tooltip-power-watt = { $value } Вт + .label = Магутнасць +# This is used in the tooltip when the instant power value uses the milliwatt unit. +# Variables: +# $value (String) - the power value at this location +TrackPower--tooltip-power-milliwatt = { $value } мВт + .label = Магутнасць +# This is used in the tooltip when the energy used in the current range uses the +# watt-hour unit. +# Variables: +# $value (String) - the energy value for this range +# $carbonValue (string) - the carbon dioxide equivalent (CO₂e) value (grams) +TrackPower--tooltip-energy-carbon-used-in-range-watthour = { $value } Вт·гад ({ $carbonValue } г CO₂e) + .label = Энергія, якая спажываецца ў бачным дыяпазоне +# This is used in the tooltip when the energy used in the current range uses the +# milliwatt-hour unit. +# Variables: +# $value (String) - the energy value for this range +# $carbonValue (string) - the carbon dioxide equivalent (CO₂e) value (milligrams) +TrackPower--tooltip-energy-carbon-used-in-range-milliwatthour = { $value } мВт·гад ({ $carbonValue } мг CO₂e) + .label = Энергія, якая спажываецца ў бачным дыяпазоне +# This is used in the tooltip when the energy used in the current range uses the +# microwatt-hour unit. +# Variables: +# $value (String) - the energy value for this range +# $carbonValue (string) - the carbon dioxide equivalent (CO₂e) value (milligrams) +TrackPower--tooltip-energy-carbon-used-in-range-microwatthour = { $value } мкВт·гад ({ $carbonValue } мг CO₂e) + .label = Энергія, якая спажываецца ў бачным дыяпазоне +# This is used in the tooltip when the energy used in the current preview +# selection uses the watt-hour unit. +# Variables: +# $value (String) - the energy value for this range +# $carbonValue (string) - the carbon dioxide equivalent (CO₂e) value (grams) +TrackPower--tooltip-energy-carbon-used-in-preview-watthour = { $value } Вт·гад ({ $carbonValue } г CO₂e) + .label = Энергія, якая спажываецца ў бягучай выбарцы +# This is used in the tooltip when the energy used in the current preview +# selection uses the milliwatt-hour unit. +# Variables: +# $value (String) - the energy value for this range +# $carbonValue (string) - the carbon dioxide equivalent (CO₂e) value (milligrams) +TrackPower--tooltip-energy-carbon-used-in-preview-milliwatthour = { $value } мВт·гад ({ $carbonValue } мг CO₂e) + .label = Энергія, якая спажываецца ў бягучай выбарцы +# This is used in the tooltip when the energy used in the current preview +# selection uses the microwatt-hour unit. +# Variables: +# $value (String) - the energy value for this range +# $carbonValue (string) - the carbon dioxide equivalent (CO₂e) value (milligrams) +TrackPower--tooltip-energy-carbon-used-in-preview-microwatthour = { $value } мкВт·гад ({ $carbonValue } мг CO₂e) + .label = Энергія, якая спажываецца ў бягучай выбарцы ## TrackSearchField ## The component that is used for the search input in the track context menu. +TrackSearchField--search-input = + .placeholder = Увядзіце ўмовы фільтра + .title = Адлюстроўваць толькі дарожкі, якія адпавядаюць пэўнаму тэксту ## TransformNavigator ## Navigator for the applied transforms in the Call Tree, Flame Graph, and Stack @@ -476,15 +796,119 @@ TrackContextMenu--hide-track = Схаваць “{ $trackName }” ## To learn more about them, visit: ## https://profiler.firefox.com/docs/#/./guide-filtering-call-trees?id=transforms +# Root item in the transform navigator. +# "Complete" is an adjective here, not a verb. +# See: https://profiler.firefox.com/docs/#/./guide-filtering-call-trees?id=collapse +# Variables: +# $item (String) - Name of the current thread. E.g.: Web Content. +TransformNavigator--complete = “{ $item }” поўнасцю +# "Collapse resource" transform. +# See: https://profiler.firefox.com/docs/#/./guide-filtering-call-trees?id=collapse +# Variables: +# $item (String) - Name of the resource that collapsed. E.g.: libxul.so. +TransformNavigator--collapse-resource = Згарнуць: { $item } +# "Focus subtree" transform. +# See: https://profiler.firefox.com/docs/#/./guide-filtering-call-trees?id=focus +# Variables: +# $item (String) - Name of the function that transform applied to. +TransformNavigator--focus-subtree = Вузел у фокусе: { $item } +# "Focus function" transform. +# See: https://profiler.firefox.com/docs/#/./guide-filtering-call-trees?id=focus +# Variables: +# $item (String) - Name of the function that transform applied to. +TransformNavigator--focus-function = Фокус: { $item } +# "Focus category" transform. The word "Focus" has the meaning of an adjective here. +# See: https://profiler.firefox.com/docs/#/./guide-filtering-call-trees?id=focus-category +# Variables: +# $item (String) - Name of the category that transform applied to. +TransformNavigator--focus-category = Катэгорыя ў фокусе: { $item } +# "Merge call node" transform. +# See: https://profiler.firefox.com/docs/#/./guide-filtering-call-trees?id=merge +# Variables: +# $item (String) - Name of the function that transform applied to. +TransformNavigator--merge-call-node = Аб'яднаць вузел: { $item } +# "Merge function" transform. +# See: https://profiler.firefox.com/docs/#/./guide-filtering-call-trees?id=merge +# Variables: +# $item (String) - Name of the function that transform applied to. +TransformNavigator--merge-function = Аб'яднаць: { $item } +# "Drop function" transform. +# See: https://profiler.firefox.com/docs/#/./guide-filtering-call-trees?id=drop +# Variables: +# $item (String) - Name of the function that transform applied to. +TransformNavigator--drop-function = Адхілена: { $item } +# "Collapse direct recursion" transform. +# See: https://profiler.firefox.com/docs/#/./guide-filtering-call-trees?id=collapse +# Variables: +# $item (String) - Name of the function that transform applied to. +TransformNavigator--collapse-direct-recursion2 = Згарнуць прамую рэкурсію: { $item } +# "Collapse indirect recursion" transform. +# See: https://profiler.firefox.com/docs/#/./guide-filtering-call-trees?id=collapse +# Variables: +# $item (String) - Name of the function that transform applied to. +TransformNavigator--collapse-indirect-recursion = Згарнуць непрамую рэкурсію: { $item } +# "Collapse function subtree" transform. +# See: https://profiler.firefox.com/docs/#/./guide-filtering-call-trees?id=collapse +# Variables: +# $item (String) - Name of the function that transform applied to. +TransformNavigator--collapse-function-subtree = Згарнуць паддрэва: { $item } ## Source code view in a box at the bottom of the UI. +# Displayed while the source view is waiting for the network request which +# delivers the source code. +# Variables: +# $host (String) - The "host" part of the URL, e.g. hg.mozilla.org +SourceView--loading-url = Чаканне { $host }… +# Displayed while the source view is waiting for the browser to deliver +# the source code. +SourceView--loading-browser-connection = Чаканне { -firefox-brand-name }… # Displayed whenever the source view was not able to get the source code for # a file. SourceView--source-not-available-title = Зыходны код недаступны +# Displayed whenever the source view was not able to get the source code for +# a file. +# Elements: +# link text - A link to the github issue about supported scenarios. +SourceView--source-not-available-text = Глядзіце абмеркаванне #3741 каб даведацца аб сцэнарыях, якія падтрымліваюцца, і запланаваных паляпшэннях. # Displayed below SourceView--cannot-obtain-source, if the profiler does not # know which URL to request source code from. SourceView--no-known-cors-url = Для гэтага файла няма вядомага cross-origin-accessible URL-адраса. +# Displayed below SourceView--cannot-obtain-source, if there was a network error +# when fetching the source code for a file. +# Variables: +# $url (String) - The URL which we tried to get the source code from +# $networkErrorMessage (String) - The raw internal error message that was encountered by the network request, not localized +SourceView--network-error-when-obtaining-source = Пры атрыманні URL { $url } адбылася памылка сеткі: { $networkErrorMessage } +# Displayed below SourceView--cannot-obtain-source, if the browser could not +# be queried for source code using the symbolication API. +# Variables: +# $browserConnectionErrorMessage (String) - The raw internal error message, not localized +SourceView--browser-connection-error-when-obtaining-source = Не ўдалося запытаць API сімвалізацыі браўзера: { $browserConnectionErrorMessage } +# Displayed below SourceView--cannot-obtain-source, if the browser was queried +# for source code using the symbolication API, and this query returned an error. +# Variables: +# $apiErrorMessage (String) - The raw internal error message from the API, not localized +SourceView--browser-api-error-when-obtaining-source = API сімвалізацыі браўзера вярнула памылку: { $apiErrorMessage } +# Displayed below SourceView--cannot-obtain-source, if a symbol server which is +# running locally was queried for source code using the symbolication API, and +# this query returned an error. +# Variables: +# $apiErrorMessage (String) - The raw internal error message from the API, not localized +SourceView--local-symbol-server-api-error-when-obtaining-source = API сімвалізацыі лакальнага сервера сімвалаў вярнула памылку: { $apiErrorMessage } +# Displayed below SourceView--cannot-obtain-source, if a file could not be found in +# an archive file (.tar.gz) which was downloaded from crates.io. +# Variables: +# $url (String) - The URL from which the "archive" file was downloaded. +# $pathInArchive (String) - The raw path of the member file which was not found in the archive. +SourceView--not-in-archive-error-when-obtaining-source = Файл { $pathInArchive } не быў знойдзены ў архіве з { $url }. +# Displayed below SourceView--cannot-obtain-source, if the file format of an +# "archive" file was not recognized. The only supported archive formats at the +# moment are .tar and .tar.gz, because that's what crates.io uses for .crates files. +# Variables: +# $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 } SourceView--close-button = .title = Закрыць акно з кодам diff --git a/locales/es-CL/app.ftl b/locales/es-CL/app.ftl index 4a79f73261..340def1f2a 100644 --- a/locales/es-CL/app.ftl +++ b/locales/es-CL/app.ftl @@ -289,12 +289,12 @@ MarkerContextMenu--copy-as-json = Copiar como JSON # IPC marker. # Variables: # $threadName (String) - Name of the thread that will be selected. -MarkerContextMenu--select-the-receiver-thread = Seleccione el hilo receptor "{ $threadName }" +MarkerContextMenu--select-the-receiver-thread = Selecciona el hilo receptor "{ $threadName }" # This string is used on the marker context menu item when right clicked on an # IPC marker. # Variables: # $threadName (String) - Name of the thread that will be selected. -MarkerContextMenu--select-the-sender-thread = Seleccione el hilo remitente "{ $threadName }" +MarkerContextMenu--select-the-sender-thread = Selecciona el hilo remitente "{ $threadName }" ## MarkerSettings ## This is used in all panels related to markers. diff --git a/locales/kab/app.ftl b/locales/kab/app.ftl index 0fc58bc2b5..649b5cd648 100644 --- a/locales/kab/app.ftl +++ b/locales/kab/app.ftl @@ -42,6 +42,9 @@ AppViewRouter--route-not-found--home = ## This is used as a context menu for the Call Tree, Flame Graph and Stack Chart ## panels. +# Variables: +# $fileName (String) - Name of the file to open. +CallNodeContextMenu--show-file = Sken { $fileName } CallNodeContextMenu--transform-merge-function = Smezdi tawuri .title = Asmezdi n twuri itekkes-itt seg umaɣnu, ad tmudd akud-ines i @@ -436,8 +439,12 @@ TrackMemoryGraph--relative-memory-at-this-time = takatut tamassaɣt deg wakud-a ## TrackPower ## This is used to show the power used by the CPU and other chips in a computer, ## graphed over time. -## It's not displayed by default in the UI, but an example can be found at +## It's not always displayed in the UI, but an example can be found at ## https://share.firefox.dev/3a1fiT7. +## For the strings in this group, the carbon dioxide equivalent is computed from +## the used energy, using the carbon dioxide equivalent for electricity +## consumption. The carbon dioxide equivalent represents the equivalent amount +## of CO₂ to achieve the same level of global warming potential. ## TrackSearchField diff --git a/package.json b/package.json index 084d18dc18..2d1d9e2c98 100644 --- a/package.json +++ b/package.json @@ -49,11 +49,11 @@ }, "dependencies": { "@codemirror/lang-cpp": "^6.0.2", - "@codemirror/lang-javascript": "^6.1.3", + "@codemirror/lang-javascript": "^6.1.4", "@codemirror/lang-rust": "^6.0.1", "@codemirror/language": "^6.4.0", "@codemirror/state": "^6.2.0", - "@codemirror/view": "^6.7.3", + "@codemirror/view": "^6.8.1", "@firefox-devtools/react-contextmenu": "^5.1.0", "@fluent/bundle": "^0.17.1", "@fluent/langneg": "^0.6.2", @@ -66,7 +66,7 @@ "classnames": "^2.3.2", "common-tags": "^1.8.2", "copy-to-clipboard": "^3.3.3", - "core-js": "^3.27.2", + "core-js": "^3.28.0", "escape-string-regexp": "^4.0.0", "gecko-profiler-demangle": "^0.3.3", "idb": "^7.1.1", @@ -79,7 +79,7 @@ "query-string": "^8.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-intersection-observer": "^9.4.1", + "react-intersection-observer": "^9.4.2", "react-redux": "^8.0.5", "react-splitter-layout": "^4.0.0", "react-transition-group": "^4.4.5", @@ -109,7 +109,7 @@ "babel-loader": "^9.1.2", "babel-plugin-module-resolver": "^5.0.0", "browserslist": "^4.21.5", - "caniuse-lite": "^1.0.30001442", + "caniuse-lite": "^1.0.30001451", "circular-dependency-plugin": "^5.2.1", "codecov": "^3.8.3", "copy-webpack-plugin": "^11.0.0", @@ -117,7 +117,7 @@ "css-loader": "^6.7.3", "cssnano": "^5.1.14", "devtools-license-check": "^0.9.0", - "eslint": "^8.32.0", + "eslint": "^8.34.0", "eslint-config-prettier": "^8.6.0", "eslint-import-resolver-alias": "^1.1.2", "eslint-plugin-flowtype": "^8.0.3", @@ -126,7 +126,7 @@ "eslint-plugin-jest-dom": "^4.0.3", "eslint-plugin-jest-formatting": "^3.1.0", "eslint-plugin-prettier": "^4.2.1", - "eslint-plugin-react": "^7.32.1", + "eslint-plugin-react": "^7.32.2", "eslint-plugin-testing-library": "^5.10.0", "fake-indexeddb": "^4.0.1", "fetch-mock-jest": "^1.5.1", @@ -141,21 +141,20 @@ "jest-environment-jsdom": "^29.4.1", "jest-extended": "^3.2.3", "json-loader": "^0.5.7", - "local-web-server": "^5.2.1", + "local-web-server": "^5.3.0", "lockfile-lint": "^4.10.0", "mkdirp": "^2.1.3", "node-fetch": "^2.6.7", "npm-run-all": "^4.1.5", "postcss": "^8.4.21", "postcss-loader": "^7.0.2", - "prettier": "^2.8.3", + "prettier": "^2.8.4", "raw-loader": "^4.0.2", - "rimraf": "^4.1.1", + "rimraf": "^4.1.2", "style-loader": "^3.3.1", - "stylelint": "^14.16.1", + "stylelint": "^15.1.0", "stylelint-config-idiomatic-order": "^9.0.0", - "stylelint-config-prettier": "^9.0.4", - "stylelint-config-standard": "^29.0.0", + "stylelint-config-standard": "^30.0.1", "stylelint-prettier": "^2.0.0", "webpack": "^5.75.0", "webpack-cli": "^5.0.1", diff --git a/src/actions/profile-view.js b/src/actions/profile-view.js index 9d78b10029..d59f040dd4 100644 --- a/src/actions/profile-view.js +++ b/src/actions/profile-view.js @@ -75,6 +75,7 @@ import type { KeyboardModifiers, TableViewOptions, SelectionContext, + BottomBoxInfo, } from 'firefox-profiler/types'; import { funcHasDirectRecursiveCall, @@ -1948,11 +1949,35 @@ export function changeTableViewOptions( }; } -export function openSourceView(file: string, currentTab: TabSlug): Action { +export function updateBottomBoxContentsAndMaybeOpen( + currentTab: TabSlug, + { libIndex, sourceFile, nativeSymbols }: BottomBoxInfo +): Action { + // TODO: If the set has more than one element, pick the native symbol with + // the highest total sample count + const nativeSymbol = nativeSymbols.length !== 0 ? nativeSymbols[0] : null; + return { - type: 'OPEN_SOURCE_VIEW', - file, + type: 'UPDATE_BOTTOM_BOX', + libIndex, + sourceFile, + nativeSymbol, + allNativeSymbolsForInitiatingCallNode: nativeSymbols, currentTab, + shouldOpenBottomBox: sourceFile !== null || nativeSymbol !== null, + shouldOpenAssemblyView: sourceFile === null && nativeSymbol !== null, + }; +} + +export function openAssemblyView(): Action { + return { + type: 'OPEN_ASSEMBLY_VIEW', + }; +} + +export function closeAssemblyView(): Action { + return { + type: 'CLOSE_ASSEMBLY_VIEW', }; } diff --git a/src/app-logic/l10n.js b/src/app-logic/l10n.js index bceb3644bc..5c276aab6e 100644 --- a/src/app-logic/l10n.js +++ b/src/app-logic/l10n.js @@ -20,6 +20,7 @@ import { SHORTDATE } from 'firefox-profiler/utils/l10n-ftl-functions'; // Also note that the order specified here is the order they'll be displayed in the // language switcher, so it's important to keep the alphabetical order. export const AVAILABLE_LOCALES_TO_LOCALIZED_NAMES = { + be: 'Беларуская', de: 'Deutsch', el: 'Ελληνικά', 'en-GB': 'English (GB)', diff --git a/src/app-logic/url-handling.js b/src/app-logic/url-handling.js index fc07965080..db71236de1 100644 --- a/src/app-logic/url-handling.js +++ b/src/app-logic/url-handling.js @@ -38,6 +38,7 @@ import type { ThreadIndex, TimelineType, SourceViewState, + AssemblyViewState, } from 'firefox-profiler/types'; import { decodeUintArrayFromUrlComponent, @@ -411,8 +412,8 @@ export function getQueryStringFromUrlState(urlState: UrlState): string { : urlState.profileSpecific.lastSelectedCallTreeSummaryStrategy; const { sourceView, isBottomBoxOpenPerPanel } = urlState.profileSpecific; query.sourceView = - sourceView.file !== null && isBottomBoxOpenPerPanel[selectedTab] - ? sourceView.file + sourceView.sourceFile !== null && isBottomBoxOpenPerPanel[selectedTab] + ? sourceView.sourceFile : undefined; break; } @@ -575,13 +576,20 @@ export function stateFromLocation( const selectedTab = toValidTabSlug(pathParts[selectedTabPathPart]) || 'calltree'; const sourceView: SourceViewState = { - activationGeneration: 0, - file: null, + scrollGeneration: 0, + libIndex: null, + sourceFile: null, + }; + const assemblyView: AssemblyViewState = { + isOpen: false, + scrollGeneration: 0, + nativeSymbol: null, + allNativeSymbolsForInitiatingCallNode: [], }; const isBottomBoxOpenPerPanel = {}; tabSlugs.forEach((tabSlug) => (isBottomBoxOpenPerPanel[tabSlug] = false)); if (query.sourceView) { - sourceView.file = query.sourceView; + sourceView.sourceFile = query.sourceView; isBottomBoxOpenPerPanel[selectedTab] = true; } @@ -617,6 +625,7 @@ export function stateFromLocation( networkSearchString: query.networkSearch || '', transforms, sourceView, + assemblyView, isBottomBoxOpenPerPanel, timelineType: validateTimelineType(query.timelineType), full: { diff --git a/src/components/app/BottomBox.js b/src/components/app/BottomBox.js index 9cb1dfe1a9..58506974c6 100644 --- a/src/components/app/BottomBox.js +++ b/src/components/app/BottomBox.js @@ -9,7 +9,7 @@ import classNames from 'classnames'; import { SourceView } from '../shared/SourceView'; import { getSourceViewFile, - getSourceViewActivationGeneration, + getSourceViewScrollGeneration, } from 'firefox-profiler/selectors/url-state'; import { selectedThreadSelectors, @@ -34,7 +34,7 @@ type StateProps = {| +sourceViewSource: FileSourceStatus | void, +globalLineTimings: LineTimings, +selectedCallNodeLineTimings: LineTimings, - +sourceViewActivationGeneration: number, + +sourceViewScrollGeneration: number, +disableOverscan: boolean, |}; @@ -218,7 +218,7 @@ class BottomBoxImpl extends React.PureComponent { sourceViewSource, globalLineTimings, disableOverscan, - sourceViewActivationGeneration, + sourceViewScrollGeneration, selectedCallNodeLineTimings, } = this.props; const source = @@ -253,7 +253,7 @@ class BottomBoxImpl extends React.PureComponent { timings={globalLineTimings} source={source} filePath={path} - scrollToHotSpotGeneration={sourceViewActivationGeneration} + scrollToHotSpotGeneration={sourceViewScrollGeneration} hotSpotTimings={selectedCallNodeLineTimings} ref={this._sourceView} /> @@ -275,7 +275,7 @@ export const BottomBox = explicitConnect<{||}, StateProps, DispatchProps>({ globalLineTimings: selectedThreadSelectors.getSourceViewLineTimings(state), selectedCallNodeLineTimings: selectedNodeSelectors.getSourceViewLineTimings(state), - sourceViewActivationGeneration: getSourceViewActivationGeneration(state), + sourceViewScrollGeneration: getSourceViewScrollGeneration(state), disableOverscan: getPreviewSelection(state).isModifying, }), mapDispatchToProps: { diff --git a/src/components/calltree/CallTree.js b/src/components/calltree/CallTree.js index e4f71f7cc6..658b392152 100644 --- a/src/components/calltree/CallTree.js +++ b/src/components/calltree/CallTree.js @@ -30,8 +30,8 @@ import { changeExpandedCallNodes, addTransformToStack, handleCallNodeTransformShortcut, - openSourceView, changeTableViewOptions, + updateBottomBoxContentsAndMaybeOpen, } from 'firefox-profiler/actions/profile-view'; import { assertExhaustiveCheck } from 'firefox-profiler/utils/flow'; @@ -82,7 +82,7 @@ type DispatchProps = {| +changeExpandedCallNodes: typeof changeExpandedCallNodes, +addTransformToStack: typeof addTransformToStack, +handleCallNodeTransformShortcut: typeof handleCallNodeTransformShortcut, - +openSourceView: typeof openSourceView, + +updateBottomBoxContentsAndMaybeOpen: typeof updateBottomBoxContentsAndMaybeOpen, +onTableViewOptionsChange: (TableViewOptions) => any, |}; @@ -289,12 +289,9 @@ class CallTreeImpl extends PureComponent { }; _onEnterOrDoubleClick = (nodeId: IndexIntoCallNodeTable) => { - const { tree, openSourceView } = this.props; - const file = tree.getRawFileNameForCallNode(nodeId); - if (file === null) { - return; - } - openSourceView(file, 'calltree'); + const { tree, updateBottomBoxContentsAndMaybeOpen } = this.props; + const bottomBoxInfo = tree.getBottomBoxInfoForCallNode(nodeId); + updateBottomBoxContentsAndMaybeOpen('calltree', bottomBoxInfo); }; maybeProcureInterestingInitialSelection() { @@ -431,7 +428,7 @@ export const CallTree = explicitConnect<{||}, StateProps, DispatchProps>({ changeExpandedCallNodes, addTransformToStack, handleCallNodeTransformShortcut, - openSourceView, + updateBottomBoxContentsAndMaybeOpen, onTableViewOptionsChange: (options: TableViewOptions) => changeTableViewOptions('calltree', options), }, diff --git a/src/components/flame-graph/Canvas.js b/src/components/flame-graph/Canvas.js index f1e85e89b0..0ccb59cc7e 100644 --- a/src/components/flame-graph/Canvas.js +++ b/src/components/flame-graph/Canvas.js @@ -11,6 +11,7 @@ import { type Viewport, } from '../shared/chart/Viewport'; import { ChartCanvas } from '../shared/chart/Canvas'; +import { FastFillStyle } from '../../utils'; import TextMeasurement from '../../utils/text-measurement'; import { mapCategoryColorNameToStackChartStyles } from '../../utils/colors'; import { @@ -122,16 +123,6 @@ class FlameGraphCanvasImpl extends React.PureComponent { } } - // Provide a memoized function that maps the category color names to specific color - // choices that are used across this project's charts. - _mapCategoryColorNameToStyles = memoize( - mapCategoryColorNameToStackChartStyles, - { - // Memoize every color that is seen. - limit: Infinity, - } - ); - _scrollSelectionIntoView = () => { const { selectedCallNodeIndex, @@ -183,8 +174,9 @@ class FlameGraphCanvasImpl extends React.PureComponent { this._textMeasurement = new TextMeasurement(ctx); } const textMeasurement = this._textMeasurement; + const fastFillStyle = new FastFillStyle(ctx); - ctx.fillStyle = '#ffffff'; + fastFillStyle.set('#ffffff'); ctx.fillRect(0, 0, containerWidth, containerHeight); const startDepth = Math.floor( @@ -205,23 +197,17 @@ class FlameGraphCanvasImpl extends React.PureComponent { const startTime = stackTiming.start[i]; const endTime = stackTiming.end[i]; - const x: CssPixels = startTime * containerWidth; - const y: CssPixels = - (maxStackDepth - depth - 1) * ROW_HEIGHT - viewportTop; const w: CssPixels = (endTime - startTime) * containerWidth; - const h: CssPixels = ROW_HEIGHT - 1; - if (w < 2) { // Skip sending draw calls for sufficiently small boxes. continue; } + const x: CssPixels = startTime * containerWidth; + const y: CssPixels = + (maxStackDepth - depth - 1) * ROW_HEIGHT - viewportTop; + const h: CssPixels = ROW_HEIGHT - 1; const callNodeIndex = stackTiming.callNode[i]; - const funcIndex = callNodeTable.func[callNodeIndex]; - const funcName = thread.stringTable.getString( - thread.funcTable.name[funcIndex] - ); - const isSelected = selectedCallNodeIndex === callNodeIndex; const isRightClicked = rightClickedCallNodeIndex === callNodeIndex; const isHovered = @@ -232,30 +218,33 @@ class FlameGraphCanvasImpl extends React.PureComponent { const categoryIndex = callNodeTable.category[callNodeIndex]; const category = categories[categoryIndex]; - const colorStyles = this._mapCategoryColorNameToStyles(category.color); + const colorStyles = mapCategoryColorNameToStackChartStyles( + category.color + ); const background = isHighlighted ? colorStyles.selectedFillStyle : colorStyles.unselectedFillStyle; - const foreground = isHighlighted - ? colorStyles.selectedTextColor - : '#000'; - ctx.fillStyle = background; - ctx.fillRect(x, y, w, h); - // Ensure spacing between blocks. - ctx.fillStyle = '#ffffff'; - ctx.fillRect(x, y, 1, h); + fastFillStyle.set(background); + // Draw rect at an offset to ensure spacing between blocks. + ctx.fillRect(x + 1, y, w - 1, h); // TODO - L10N RTL. // Constrain the x coordinate to the leftmost area. const x2: CssPixels = Math.max(x, 0) + TEXT_OFFSET_START; const w2: CssPixels = Math.max(0, w - (x2 - x)); - if (w2 > textMeasurement.minWidth) { + const funcIndex = callNodeTable.func[callNodeIndex]; + const funcName = thread.stringTable.getString( + thread.funcTable.name[funcIndex] + ); const fittedText = textMeasurement.getFittedText(funcName, w2); if (fittedText) { - ctx.fillStyle = foreground; + const foreground = isHighlighted + ? colorStyles.selectedTextColor + : '#000'; + fastFillStyle.set(foreground); ctx.fillText(fittedText, x2, y + TEXT_OFFSET_TOP); } } diff --git a/src/components/flame-graph/FlameGraph.js b/src/components/flame-graph/FlameGraph.js index 1589e5047f..0cd10c5ab7 100644 --- a/src/components/flame-graph/FlameGraph.js +++ b/src/components/flame-graph/FlameGraph.js @@ -29,7 +29,7 @@ import { changeSelectedCallNode, changeRightClickedCallNode, handleCallNodeTransformShortcut, - openSourceView, + updateBottomBoxContentsAndMaybeOpen, } from 'firefox-profiler/actions/profile-view'; import type { @@ -96,7 +96,7 @@ type DispatchProps = {| +changeSelectedCallNode: typeof changeSelectedCallNode, +changeRightClickedCallNode: typeof changeRightClickedCallNode, +handleCallNodeTransformShortcut: typeof handleCallNodeTransformShortcut, - +openSourceView: typeof openSourceView, + +updateBottomBoxContentsAndMaybeOpen: typeof updateBottomBoxContentsAndMaybeOpen, |}; type Props = ConnectedProps<{||}, StateProps, DispatchProps>; @@ -131,16 +131,15 @@ class FlameGraphImpl extends React.PureComponent { ); }; - _onCallNodeDoubleClick = (callNodeIndex: IndexIntoCallNodeTable | null) => { + _onCallNodeEnterOrDoubleClick = ( + callNodeIndex: IndexIntoCallNodeTable | null + ) => { if (callNodeIndex === null) { return; } - const { callTree, openSourceView } = this.props; - const file = callTree.getRawFileNameForCallNode(callNodeIndex); - if (file === null) { - return; - } - openSourceView(file, 'flame-graph'); + const { callTree, updateBottomBoxContentsAndMaybeOpen } = this.props; + const bottomBoxInfo = callTree.getBottomBoxInfoForCallNode(callNodeIndex); + updateBottomBoxContentsAndMaybeOpen('flame-graph', bottomBoxInfo); }; _shouldDisplayTooltips = () => this.props.rightClickedCallNodeIndex === null; @@ -222,7 +221,6 @@ class FlameGraphImpl extends React.PureComponent { rightClickedCallNodeIndex, changeSelectedCallNode, handleCallNodeTransformShortcut, - openSourceView, } = this.props; if ( @@ -298,10 +296,7 @@ class FlameGraphImpl extends React.PureComponent { } if (event.key === 'Enter') { - const file = callTree.getRawFileNameForCallNode(nodeIndex); - if (file !== null) { - openSourceView(file, 'flame-graph'); - } + this._onCallNodeEnterOrDoubleClick(nodeIndex); return; } @@ -398,7 +393,7 @@ class FlameGraphImpl extends React.PureComponent { stackFrameHeight: STACK_FRAME_HEIGHT, onSelectionChange: this._onSelectedCallNodeChange, onRightClick: this._onRightClickedCallNodeChange, - onDoubleClick: this._onCallNodeDoubleClick, + onDoubleClick: this._onCallNodeEnterOrDoubleClick, shouldDisplayTooltips: this._shouldDisplayTooltips, interval, isInverted, @@ -462,7 +457,7 @@ export const FlameGraph = explicitConnect<{||}, StateProps, DispatchProps>({ changeSelectedCallNode, changeRightClickedCallNode, handleCallNodeTransformShortcut, - openSourceView, + updateBottomBoxContentsAndMaybeOpen, }, options: { forwardRef: true }, component: FlameGraphImpl, diff --git a/src/components/shared/CallNodeContextMenu.js b/src/components/shared/CallNodeContextMenu.js index 9eea175baa..1673ab62cb 100644 --- a/src/components/shared/CallNodeContextMenu.js +++ b/src/components/shared/CallNodeContextMenu.js @@ -15,13 +15,14 @@ import { funcHasIndirectRecursiveCall, } from 'firefox-profiler/profile-logic/transforms'; import { getFunctionName } from 'firefox-profiler/profile-logic/function-info'; +import { getBottomBoxInfoForCallNode } from 'firefox-profiler/profile-logic/profile-data'; import { getCategories } from 'firefox-profiler/selectors'; import copy from 'copy-to-clipboard'; import { addTransformToStack, expandAllCallNodeDescendants, - openSourceView, + updateBottomBoxContentsAndMaybeOpen, setContextMenuVisibility, } from 'firefox-profiler/actions/profile-view'; import { @@ -71,7 +72,7 @@ type StateProps = {| type DispatchProps = {| +addTransformToStack: typeof addTransformToStack, +expandAllCallNodeDescendants: typeof expandAllCallNodeDescendants, - +openSourceView: typeof openSourceView, + +updateBottomBoxContentsAndMaybeOpen: typeof updateBottomBoxContentsAndMaybeOpen, +setContextMenuVisibility: typeof setContextMenuVisibility, |}; @@ -179,11 +180,23 @@ class CallNodeContextMenuImpl extends React.PureComponent { } showFile(): void { - const filePath = this._getFilePath(); - if (filePath) { - const { openSourceView, selectedTab } = this.props; - openSourceView(filePath, selectedTab); + const { updateBottomBoxContentsAndMaybeOpen, selectedTab } = this.props; + + const rightClickedCallNodeInfo = this.getRightClickedCallNodeInfo(); + + if (rightClickedCallNodeInfo === null) { + throw new Error( + "The context menu assumes there is a selected call node and there wasn't one." + ); } + + const { callNodeIndex, thread, callNodeInfo } = rightClickedCallNodeInfo; + const bottomBoxInfo = getBottomBoxInfoForCallNode( + callNodeIndex, + callNodeInfo, + thread + ); + updateBottomBoxContentsAndMaybeOpen(selectedTab, bottomBoxInfo); } copyUrl(): void { @@ -808,7 +821,7 @@ export const CallNodeContextMenu = explicitConnect< mapDispatchToProps: { addTransformToStack, expandAllCallNodeDescendants, - openSourceView, + updateBottomBoxContentsAndMaybeOpen, setContextMenuVisibility, }, component: CallNodeContextMenuImpl, diff --git a/src/components/shared/chart/Viewport.js b/src/components/shared/chart/Viewport.js index 6ae44cdb97..77daa89ac8 100644 --- a/src/components/shared/chart/Viewport.js +++ b/src/components/shared/chart/Viewport.js @@ -163,8 +163,6 @@ type State = {| viewportTop: CssPixels, viewportBottom: CssPixels, horizontalViewport: HorizontalViewport, - dragX: CssPixels, - dragY: CssPixels, isDragging: boolean, isScrollHintVisible: boolean, isSizeSet: boolean, @@ -229,6 +227,8 @@ export const withChartViewport: WithChartViewport<*, *> = _lastKeyboardNavigationFrame: number = 0; _keysDown: Set = new Set(); _deltaToZoomFactor = (delta) => Math.pow(ZOOM_SPEED, delta); + _dragX: number = 0; + _dragY: number = 0; constructor(props: ViewportProps) { super(props); @@ -267,8 +267,6 @@ export const withChartViewport: WithChartViewport<*, *> = viewportTop: 0, viewportBottom: startsAtBottom ? maxViewportHeight : 0, horizontalViewport, - dragX: 0, - dragY: 0, isDragging: false, isScrollHintVisible: false, isSizeSet: false, @@ -560,7 +558,7 @@ export const withChartViewport: WithChartViewport<*, *> = _mouseMoveListener = (event: MouseEvent) => { event.preventDefault(); - let { dragX, dragY } = this.state; + let { _dragX: dragX, _dragY: dragY } = this; if (!this.state.isDragging) { dragX = event.clientX; dragY = event.clientY; @@ -569,11 +567,9 @@ export const withChartViewport: WithChartViewport<*, *> = const offsetX = event.clientX - dragX; const offsetY = event.clientY - dragY; - this.setState({ - dragX: event.clientX, - dragY: event.clientY, - isDragging: true, - }); + this._dragX = event.clientX; + this._dragY = event.clientY; + this.setState({ isDragging: true }); this.moveViewport(offsetX, offsetY); }; diff --git a/src/components/stack-chart/Canvas.js b/src/components/stack-chart/Canvas.js index eef88f20f9..6e6a82ac2f 100644 --- a/src/components/stack-chart/Canvas.js +++ b/src/components/stack-chart/Canvas.js @@ -5,7 +5,6 @@ // @flow import { GREY_30 } from 'photon-colors'; import * as React from 'react'; -import memoize from 'memoize-immutable'; import { TIMELINE_MARGIN_RIGHT } from '../../app-logic/constants'; import { withChartViewport, @@ -333,7 +332,7 @@ class StackChartCanvasImpl extends React.PureComponent { depth === hoveredItem.depth && i === hoveredItem.stackTimingIndex; - const colorStyles = this._mapCategoryColorNameToStyles( + const colorStyles = mapCategoryColorNameToStackChartStyles( category.color ); // Draw the box. @@ -396,16 +395,6 @@ class StackChartCanvasImpl extends React.PureComponent { ); }; - // Provide a memoized function that maps the category color names to specific color - // choices that are used across this project's charts. - _mapCategoryColorNameToStyles = memoize( - mapCategoryColorNameToStackChartStyles, - { - // Memoize every color that is seen. - limit: Infinity, - } - ); - _getHoveredStackInfo = ({ depth, stackTimingIndex, diff --git a/src/components/stack-chart/index.js b/src/components/stack-chart/index.js index 540f47e39e..8ed6344774 100644 --- a/src/components/stack-chart/index.js +++ b/src/components/stack-chart/index.js @@ -34,7 +34,7 @@ import { changeSelectedCallNode, changeRightClickedCallNode, handleCallNodeTransformShortcut, - openSourceView, + updateBottomBoxContentsAndMaybeOpen, } from '../../actions/profile-view'; import { getCallNodePathFromIndex } from '../../profile-logic/profile-data'; @@ -93,7 +93,7 @@ type DispatchProps = {| +changeRightClickedCallNode: typeof changeRightClickedCallNode, +updatePreviewSelection: typeof updatePreviewSelection, +handleCallNodeTransformShortcut: typeof handleCallNodeTransformShortcut, - +openSourceView: typeof openSourceView, + +updateBottomBoxContentsAndMaybeOpen: typeof updateBottomBoxContentsAndMaybeOpen, |}; type Props = ConnectedProps<{||}, StateProps, DispatchProps>; @@ -152,7 +152,7 @@ class StackChartImpl extends React.PureComponent { selectedCallNodeIndex, rightClickedCallNodeIndex, handleCallNodeTransformShortcut, - openSourceView, + updateBottomBoxContentsAndMaybeOpen, } = this.props; const nodeIndex = @@ -164,10 +164,8 @@ class StackChartImpl extends React.PureComponent { } if (event.key === 'Enter') { - const file = callTree.getRawFileNameForCallNode(nodeIndex); - if (file !== null) { - openSourceView(file, 'stack-chart'); - } + const bottomBoxInfo = callTree.getBottomBoxInfoForCallNode(nodeIndex); + updateBottomBoxContentsAndMaybeOpen('stack-chart', bottomBoxInfo); return; } @@ -325,7 +323,7 @@ export const StackChart = explicitConnect<{||}, StateProps, DispatchProps>({ changeRightClickedCallNode, updatePreviewSelection, handleCallNodeTransformShortcut, - openSourceView, + updateBottomBoxContentsAndMaybeOpen, }, component: StackChartImpl, }); diff --git a/src/components/timeline/VerticalIndicators.js b/src/components/timeline/VerticalIndicators.js index 8630561b83..6795c0f281 100644 --- a/src/components/timeline/VerticalIndicators.js +++ b/src/components/timeline/VerticalIndicators.js @@ -88,18 +88,16 @@ export class VerticalIndicators extends React.PureComponent { innerWindowIDToPageMap && data && data.type === 'tracing' && - data.category === 'Navigation' + data.category === 'Navigation' && + data.innerWindowID ) { - const innerWindowID = data.innerWindowID; - if (innerWindowID) { - const page = innerWindowIDToPageMap.get(innerWindowID); - if (page) { - url = ( -
- {displayNiceUrl(page.url)} -
- ); - } + const page = innerWindowIDToPageMap.get(data.innerWindowID); + if (page) { + url = ( +
+ {displayNiceUrl(page.url)} +
+ ); } } diff --git a/src/profile-logic/call-tree.js b/src/profile-logic/call-tree.js index 89fd02beb1..27be4e874c 100644 --- a/src/profile-logic/call-tree.js +++ b/src/profile-logic/call-tree.js @@ -9,16 +9,13 @@ import { getSampleIndexToCallNodeIndex, getOriginAnnotationForFunc, getCategoryPairLabel, + getBottomBoxInfoForCallNode, } from './profile-data'; import { resourceTypes } from './data-structures'; import { getFunctionName } from './function-info'; -import { UniqueStringArray } from '../utils/unique-string-array'; import type { CategoryList, Thread, - FuncTable, - ResourceTable, - NativeSymbolTable, IndexIntoFuncTable, SamplesLikeTable, WeightType, @@ -31,6 +28,7 @@ import type { TracedTiming, SamplesTable, ExtraBadgeInfo, + BottomBoxInfo, } from 'firefox-profiler/types'; import ExtensionIcon from '../../res/img/svg/extension.svg'; @@ -70,13 +68,11 @@ function extractFaviconFromLibname(libname: string): string | null { export class CallTree { _categories: CategoryList; + _callNodeInfo: CallNodeInfo; _callNodeTable: CallNodeTable; _callNodeSummary: CallNodeSummary; _callNodeChildCount: Uint32Array; // A table column matching the callNodeTable - _funcTable: FuncTable; - _resourceTable: ResourceTable; - _nativeSymbols: NativeSymbolTable; - _stringTable: UniqueStringArray; + _thread: Thread; _rootTotalSummary: number; _rootCount: number; _displayDataByIndex: Map; @@ -89,9 +85,9 @@ export class CallTree { _weightType: WeightType; constructor( - { funcTable, resourceTable, nativeSymbols, stringTable }: Thread, + thread: Thread, categories: CategoryList, - callNodeTable: CallNodeTable, + callNodeInfo: CallNodeInfo, callNodeSummary: CallNodeSummary, callNodeChildCount: Uint32Array, rootTotalSummary: number, @@ -102,13 +98,11 @@ export class CallTree { weightType: WeightType ) { this._categories = categories; - this._callNodeTable = callNodeTable; + this._callNodeInfo = callNodeInfo; + this._callNodeTable = callNodeInfo.callNodeTable; this._callNodeSummary = callNodeSummary; this._callNodeChildCount = callNodeChildCount; - this._funcTable = funcTable; - this._resourceTable = resourceTable; - this._nativeSymbols = nativeSymbols; - this._stringTable = stringTable; + this._thread = thread; this._rootTotalSummary = rootTotalSummary; this._rootCount = rootCount; this._displayDataByIndex = new Map(); @@ -197,8 +191,8 @@ export class CallTree { getNodeData(callNodeIndex: IndexIntoCallNodeTable): CallNodeData { const funcIndex = this._callNodeTable.func[callNodeIndex]; - const funcName = this._stringTable.getString( - this._funcTable.name[funcIndex] + const funcName = this._thread.stringTable.getString( + this._thread.funcTable.name[funcIndex] ); const total = this._callNodeSummary.total[callNodeIndex]; const totalRelative = total / this._rootTotalSummary; @@ -236,8 +230,8 @@ export class CallTree { } const outerFunction = getFunctionName( - this._stringTable.getString( - this._nativeSymbols.name[inlinedIntoNativeSymbol] + this._thread.stringTable.getString( + this._thread.nativeSymbols.name[inlinedIntoNativeSymbol] ) ); return { @@ -259,8 +253,8 @@ export class CallTree { const categoryIndex = this._callNodeTable.category[callNodeIndex]; const subcategoryIndex = this._callNodeTable.subcategory[callNodeIndex]; const badge = this._getInliningBadge(callNodeIndex, funcName); - const resourceIndex = this._funcTable.resource[funcIndex]; - const resourceType = this._resourceTable.type[resourceIndex]; + const resourceIndex = this._thread.funcTable.resource[funcIndex]; + const resourceType = this._thread.resourceTable.type[resourceIndex]; const isFrameLabel = resourceIndex === -1; const libName = this._getOriginAnnotation(funcIndex); const weightType = this._weightType; @@ -273,8 +267,9 @@ export class CallTree { } else if (resourceType === resourceTypes.addon) { iconSrc = ExtensionIcon; - const resourceNameIndex = this._resourceTable.name[resourceIndex]; - const iconText = this._stringTable.getString(resourceNameIndex); + const resourceNameIndex = + this._thread.resourceTable.name[resourceIndex]; + const iconText = this._thread.stringTable.getString(resourceNameIndex); icon = iconText; } @@ -362,21 +357,20 @@ export class CallTree { _getOriginAnnotation(funcIndex: IndexIntoFuncTable): string { return getOriginAnnotationForFunc( funcIndex, - this._funcTable, - this._resourceTable, - this._stringTable + this._thread.funcTable, + this._thread.resourceTable, + this._thread.stringTable ); } - getRawFileNameForCallNode( + getBottomBoxInfoForCallNode( callNodeIndex: IndexIntoCallNodeTable - ): string | null { - const funcIndex = this._callNodeTable.func[callNodeIndex]; - const fileName = this._funcTable.fileName[funcIndex]; - if (fileName === null) { - return null; - } - return this._stringTable.getString(fileName); + ): BottomBoxInfo { + return getBottomBoxInfoForCallNode( + callNodeIndex, + this._callNodeInfo, + this._thread + ); } } @@ -555,7 +549,7 @@ export function getCallTree( return new CallTree( thread, categories, - callNodeInfo.callNodeTable, + callNodeInfo, callNodeSummary, callNodeChildCount, rootTotalSummary, diff --git a/src/profile-logic/import/chrome.js b/src/profile-logic/import/chrome.js index 96880a8a74..76df566ce6 100644 --- a/src/profile-logic/import/chrome.js +++ b/src/profile-logic/import/chrome.js @@ -40,7 +40,8 @@ export type TracingEventUnion = | ProcessLabelsEvent | ProcessSortIndexEvent | ThreadSortIndexEvent - | ScreenshotEvent; + | ScreenshotEvent + | FallbackEndEvent; type TracingEvent = {| cat: string, @@ -50,11 +51,24 @@ type TracingEvent = {| pid: number, // Process ID tid: number, // Thread ID ts: number, // Timestamp + tts?: number, // Thread Timestamp tdur?: number, // Time duration dur?: number, // Time duration ...Event, |}; +// V8 can generate this backward compatible event. +// See https://github.com/firefox-devtools/profiler/issues/4308#issuecomment-1303551614 +type FallbackEndEvent = TracingEvent<{| + name: 'ProfileChunk', + id: string, + args: {| + data: {| + endTime: number, + |}, + |}, +|}>; + type ProfileEvent = TracingEvent<{| name: 'Profile', args: { @@ -525,6 +539,11 @@ async function processTracingEvents( } for (const profileChunk of profileChunks) { + if (!profileChunk.args.data || !profileChunk.args.data.cpuProfile) { + // This is probably a FallbackEndEvent, ignore it instead of crashing. + continue; + } + const { cpuProfile } = profileChunk.args.data; const { nodes, samples } = cpuProfile; const timeDeltas = getTimeDeltas(profileChunk); diff --git a/src/profile-logic/profile-data.js b/src/profile-logic/profile-data.js index 53708c969a..57a54b4670 100644 --- a/src/profile-logic/profile-data.js +++ b/src/profile-logic/profile-data.js @@ -37,6 +37,7 @@ import type { StackTable, FrameTable, FuncTable, + NativeSymbolTable, ResourceTable, CategoryList, IndexIntoCategoryList, @@ -75,6 +76,9 @@ import type { Address, AddressProof, TimelineType, + NativeSymbolInfo, + BottomBoxInfo, + Bytes, } from 'firefox-profiler/types'; import type { UniqueStringArray } from 'firefox-profiler/utils/unique-string-array'; @@ -3371,6 +3375,138 @@ export function findAddressProofForFile( return null; } +/** + * Calculate a lower bound for the function size, in bytes, of a native symbol. + * This is used when the symbol server does not return a size for a function. + * We need to know the size when we want to show assembly code for the function, + * in order to know how many bytes to disassemble. + * We estimate the size by finding the highest known address for this symbol in + * the frame table, and adding one byte (because the instruction at that address + * is at least one byte long). + */ +export function calculateFunctionSizeLowerBound( + frameTable: FrameTable, + nativeSymbolAddress: Address, + nativeSymbolIndex: IndexIntoNativeSymbolTable +): Bytes { + let maxFrameAddress = nativeSymbolAddress; + for (let i = 0; i < frameTable.length; i++) { + if (frameTable.nativeSymbol[i] === nativeSymbolIndex) { + const frameAddress = frameTable.address[i]; + if (frameAddress > maxFrameAddress) { + maxFrameAddress = frameAddress; + } + } + } + return maxFrameAddress + 1 - nativeSymbolAddress; +} + +/** + * Gathers the native symbols for a given call node. In most cases, a call node + * just has one native symbol (or zero if it's not native code). But in some + * cases, a call node can have its native code in multiple different functions, + * for example in the inverted tree if it was inlined into multiple different + * functions. + */ +export function getNativeSymbolsForCallNode( + callNodeIndex: IndexIntoCallNodeTable, + { stackIndexToCallNodeIndex }: CallNodeInfo, + stackTable: StackTable, + frameTable: FrameTable +): IndexIntoNativeSymbolTable[] { + const set = new Set(); + for (let stackIndex = 0; stackIndex < stackTable.length; stackIndex++) { + if (stackIndexToCallNodeIndex[stackIndex] === callNodeIndex) { + const frame = stackTable.frame[stackIndex]; + const nativeSymbol = frameTable.nativeSymbol[frame]; + if (nativeSymbol !== null) { + set.add(nativeSymbol); + } + } + } + return [...set]; +} + +/** + * Convert a native symbol index into a NativeSymbolInfo object, to create + * something that's meaningful outside of its associated thread. + */ +export function getNativeSymbolInfo( + nativeSymbol: IndexIntoNativeSymbolTable, + nativeSymbols: NativeSymbolTable, + frameTable: FrameTable, + stringTable: UniqueStringArray +): NativeSymbolInfo { + const functionSizeOrNull = nativeSymbols.functionSize[nativeSymbol]; + const functionSize = + functionSizeOrNull ?? + calculateFunctionSizeLowerBound( + frameTable, + nativeSymbols.address[nativeSymbol], + nativeSymbol + ); + return { + libIndex: nativeSymbols.libIndex[nativeSymbol], + address: nativeSymbols.address[nativeSymbol], + name: stringTable.getString(nativeSymbols.name[nativeSymbol]), + functionSize, + functionSizeIsKnown: functionSizeOrNull !== null, + }; +} + +/** + * Calculate the BottomBoxInfo for a call node, i.e. information about which + * things should be shown in the profiler UI's "bottom box" when this call node + * is double-clicked. + * + * We always want to update all panes in the bottom box when a new call node is + * double-clicked, so that we don't show inconsistent information side-by-side. + */ +export function getBottomBoxInfoForCallNode( + callNodeIndex: IndexIntoCallNodeTable, + callNodeInfo: CallNodeInfo, + thread: Thread +): BottomBoxInfo { + const { + stackTable, + frameTable, + funcTable, + stringTable, + resourceTable, + nativeSymbols, + } = thread; + + const funcIndex = callNodeInfo.callNodeTable.func[callNodeIndex]; + const fileName = funcTable.fileName[funcIndex]; + const sourceFile = fileName !== null ? stringTable.getString(fileName) : null; + const resource = funcTable.resource[funcIndex]; + const libIndex = + resource !== -1 && resourceTable.type[resource] === resourceTypes.library + ? resourceTable.lib[resource] + : null; + const nativeSymbolsForCallNode = getNativeSymbolsForCallNode( + callNodeIndex, + callNodeInfo, + stackTable, + frameTable + ); + const nativeSymbolInfosForCallNode = nativeSymbolsForCallNode.map( + (nativeSymbolIndex) => + getNativeSymbolInfo( + nativeSymbolIndex, + nativeSymbols, + frameTable, + stringTable + ) + ); + + return { + libIndex, + sourceFile, + nativeSymbols: nativeSymbolInfosForCallNode, + }; +} + /** * Determines the timeline type by looking at the profile data. * diff --git a/src/profile-logic/tracks.js b/src/profile-logic/tracks.js index 70540bbf98..2011711158 100644 --- a/src/profile-logic/tracks.js +++ b/src/profile-logic/tracks.js @@ -62,15 +62,15 @@ const LOCAL_TRACK_INDEX_ORDER = { const LOCAL_TRACK_DISPLAY_ORDER = { network: 0, memory: 1, + power: 2, // IPC tracks that belong to the global track will appear right after network - // and memory tracks. But we want to show the IPC tracks that belong to the + // and counter tracks. But we want to show the IPC tracks that belong to the // local threads right after their track. This special handling happens inside // the sort function. - ipc: 2, - thread: 3, - 'event-delay': 4, - 'process-cpu': 5, - power: 6, + ipc: 3, + thread: 4, + 'event-delay': 5, + 'process-cpu': 6, }; const GLOBAL_TRACK_INDEX_ORDER = { @@ -113,6 +113,17 @@ function _getDefaultLocalTrackOrder(tracks: LocalTrack[], profile: ?Profile) { return 1; } + if ( + profile && + profile.counters && + tracks[a].type === 'power' && + tracks[b].type === 'power' + ) { + const nameA = profile.counters[tracks[a].counterIndex].name; + const nameB = profile.counters[tracks[b].counterIndex].name; + return naturalSort.compare(nameA, nameB); + } + // If the tracks are both threads, sort them by thread name, and then by // creation time if they have the same name. if (tracks[a].type === 'thread' && tracks[b].type === 'thread' && profile) { diff --git a/src/reducers/app.js b/src/reducers/app.js index a860d229ca..73821f5c2e 100644 --- a/src/reducers/app.js +++ b/src/reducers/app.js @@ -144,7 +144,7 @@ const panelLayoutGeneration: Reducer = (state = 0, action) => { case 'COMMIT_RANGE': case 'POP_COMMITTED_RANGES': // Bottom box: (fallthrough) - case 'OPEN_SOURCE_VIEW': + case 'UPDATE_BOTTOM_BOX': case 'CLOSE_BOTTOM_BOX_FOR_TAB': return state + 1; default: diff --git a/src/reducers/url-state.js b/src/reducers/url-state.js index 69944016f3..e6b18f44f2 100644 --- a/src/reducers/url-state.js +++ b/src/reducers/url-state.js @@ -22,6 +22,7 @@ import type { Reducer, TimelineTrackOrganization, SourceViewState, + AssemblyViewState, IsOpenPerPanelState, } from 'firefox-profiler/types'; @@ -554,14 +555,51 @@ const timelineTrackOrganization: Reducer = ( }; const sourceView: Reducer = ( - state = { activationGeneration: 0, file: null }, + state = { scrollGeneration: 0, libIndex: null, sourceFile: null }, action ) => { switch (action.type) { - case 'OPEN_SOURCE_VIEW': { + case 'UPDATE_BOTTOM_BOX': { return { - activationGeneration: state.activationGeneration + 1, - file: action.file, + scrollGeneration: state.scrollGeneration + 1, + libIndex: action.libIndex, + sourceFile: action.sourceFile, + }; + } + default: + return state; + } +}; + +const assemblyView: Reducer = ( + state = { + scrollGeneration: 0, + nativeSymbol: null, + allNativeSymbolsForInitiatingCallNode: [], + isOpen: false, + }, + action +) => { + switch (action.type) { + case 'UPDATE_BOTTOM_BOX': { + return { + scrollGeneration: state.scrollGeneration + 1, + nativeSymbol: action.nativeSymbol, + allNativeSymbolsForInitiatingCallNode: + action.allNativeSymbolsForInitiatingCallNode, + isOpen: state.isOpen || action.shouldOpenAssemblyView, + }; + } + case 'OPEN_ASSEMBLY_VIEW': { + return { + ...state, + isOpen: true, + }; + } + case 'CLOSE_ASSEMBLY_VIEW': { + return { + ...state, + isOpen: false, }; } default: @@ -580,9 +618,9 @@ const isBottomBoxOpenPerPanel: Reducer = ( action ) => { switch (action.type) { - case 'OPEN_SOURCE_VIEW': { - const { currentTab } = action; - if (!state[currentTab]) { + case 'UPDATE_BOTTOM_BOX': { + const { currentTab, shouldOpenBottomBox } = action; + if (shouldOpenBottomBox && !state[currentTab]) { return { ...state, [currentTab]: true }; } return state; @@ -662,6 +700,7 @@ const profileSpecific = combineReducers({ networkSearchString, transforms, sourceView, + assemblyView, isBottomBoxOpenPerPanel, timelineType, full: fullProfileSpecific, diff --git a/src/selectors/per-thread/composed.js b/src/selectors/per-thread/composed.js index 28ddc2b160..07f021083e 100644 --- a/src/selectors/per-thread/composed.js +++ b/src/selectors/per-thread/composed.js @@ -26,8 +26,6 @@ import type { StackTimingByDepth, } from '../../profile-logic/stack-timing'; -import { ensureExists } from '../../utils/flow'; - /** * Infer the return type from the getStackAndSampleSelectorsPerThread function. This * is done that so that the local type definition with `Selector` is the canonical @@ -92,8 +90,15 @@ export function getComposedSelectorsPerThread( } let hasSamples = samples.length > 0 && stackTable.length > 0; if (hasSamples) { - const stackIndex = ensureExists(samples.stack[0]); - if (stackTable.prefix[stackIndex] === null) { + // Find at least one non-null stack. + const stackIndex = samples.stack.find((stack) => stack !== null); + if ( + stackIndex === undefined || + stackIndex === null // We know that it can't be null at this point, but Flow doesn't. + ) { + // All samples were null. + hasSamples = false; + } else if (stackTable.prefix[stackIndex] === null) { // There's only a single stack frame, check if it's '(root)'. const frameIndex = stackTable.frame[stackIndex]; const funcIndex = frameTable.func[frameIndex]; diff --git a/src/selectors/url-state.js b/src/selectors/url-state.js index c3a2c24649..08e520dab1 100644 --- a/src/selectors/url-state.js +++ b/src/selectors/url-state.js @@ -32,7 +32,6 @@ import type { ProfileSpecificUrlState, FullProfileSpecificUrlState, ActiveTabSpecificProfileUrlState, - IsOpenPerPanelState, } from 'firefox-profiler/types'; import type { TabSlug } from '../app-logic/tabs-handling'; @@ -78,12 +77,9 @@ export const getInvertCallstack: Selector = (state) => export const getShowUserTimings: Selector = (state) => getProfileSpecificState(state).showUserTimings; export const getSourceViewFile: Selector = (state) => - getProfileSpecificState(state).sourceView.file; -export const getSourceViewActivationGeneration: Selector = (state) => - getProfileSpecificState(state).sourceView.activationGeneration; -export const getisBottomBoxOpenPerPanel: Selector = ( - state -) => getProfileSpecificState(state).isBottomBoxOpenPerPanel; + getProfileSpecificState(state).sourceView.sourceFile; +export const getSourceViewScrollGeneration: Selector = (state) => + getProfileSpecificState(state).sourceView.scrollGeneration; export const getShowJsTracerSummary: Selector = (state) => getFullProfileSpecificState(state).showJsTracerSummary; export const getTimelineTrackOrganization: Selector< @@ -216,12 +212,10 @@ export const getTransformStack: DangerousSelectorWithArguments< ); }; -export const getIsBottomBoxOpen: Selector = createSelector( - getisBottomBoxOpenPerPanel, - getSelectedTab, - (isBottomBoxOpenPerPanel, selectedTabSlug) => - isBottomBoxOpenPerPanel[selectedTabSlug] -); +export const getIsBottomBoxOpen: Selector = (state) => { + const tab = getSelectedTab(state); + return getProfileSpecificState(state).isBottomBoxOpenPerPanel[tab]; +}; /** * The URL predictor is used to generate a link for an uploaded profile, to predict diff --git a/src/test/components/WindowTitle.test.js b/src/test/components/WindowTitle.test.js index 0a40840e2c..7916ae1c59 100644 --- a/src/test/components/WindowTitle.test.js +++ b/src/test/components/WindowTitle.test.js @@ -35,7 +35,7 @@ describe('WindowTitle', () => { ); expect(document.title).toBe( - 'Firefox – 1/1/1970, 12:00:00 AM UTC – Firefox Profiler' + 'Firefox – 1/1/1970, 12:00:00\u202FAM UTC – Firefox Profiler' ); }); @@ -56,7 +56,7 @@ describe('WindowTitle', () => { ); expect(document.title).toBe( - 'Firefox – macOS 10.14 – 1/1/1970, 12:00:00 AM UTC – Firefox Profiler' + 'Firefox – macOS 10.14 – 1/1/1970, 12:00:00\u202FAM UTC – Firefox Profiler' ); }); @@ -100,7 +100,9 @@ describe('WindowTitle', () => { ); - expect(document.title).toBe('1/1/1970, 12:00:00 AM UTC – Firefox Profiler'); + expect(document.title).toBe( + '1/1/1970, 12:00:00\u202FAM UTC – Firefox Profiler' + ); }); it('shows the correct title for uploaded recordings', () => { @@ -167,7 +169,7 @@ describe('WindowTitle', () => { ); expect(document.title).toBe( - 'bar/profile1.json – Firefox – 1/1/1970, 12:00:00 AM UTC – Firefox Profiler' + 'bar/profile1.json – Firefox – 1/1/1970, 12:00:00\u202FAM UTC – Firefox Profiler' ); }); }); diff --git a/src/test/components/__snapshots__/FlameGraph.test.js.snap b/src/test/components/__snapshots__/FlameGraph.test.js.snap index 13b0fe1fbb..f981ed623b 100644 --- a/src/test/components/__snapshots__/FlameGraph.test.js.snap +++ b/src/test/components/__snapshots__/FlameGraph.test.js.snap @@ -482,20 +482,9 @@ Array [ ], Array [ "fillRect", - 0, - 284, - 200, - 15, - ], - Array [ - "set fillStyle", - "#ffffff", - ], - Array [ - "fillRect", - 0, - 284, 1, + 284, + 199, 15, ], Array [ @@ -518,20 +507,9 @@ Array [ ], Array [ "fillRect", - 0, - 268, - 200, - 15, - ], - Array [ - "set fillStyle", - "#ffffff", - ], - Array [ - "fillRect", - 0, - 268, 1, + 268, + 199, 15, ], Array [ @@ -554,20 +532,9 @@ Array [ ], Array [ "fillRect", - 0, - 252, - 133.33333333333331, - 15, - ], - Array [ - "set fillStyle", - "#ffffff", - ], - Array [ - "fillRect", - 0, - 252, 1, + 252, + 132.33333333333331, 15, ], Array [ @@ -590,20 +557,9 @@ Array [ ], Array [ "fillRect", - 133.33333333333331, + 134.33333333333331, 252, - 66.66666666666667, - 15, - ], - Array [ - "set fillStyle", - "#ffffff", - ], - Array [ - "fillRect", - 133.33333333333331, - 252, - 1, + 65.66666666666667, 15, ], Array [ @@ -626,20 +582,9 @@ Array [ ], Array [ "fillRect", - 0, - 236, - 66.66666666666666, - 15, - ], - Array [ - "set fillStyle", - "#ffffff", - ], - Array [ - "fillRect", - 0, - 236, 1, + 236, + 65.66666666666666, 15, ], Array [ @@ -662,20 +607,9 @@ Array [ ], Array [ "fillRect", - 66.66666666666666, + 67.66666666666666, 236, - 66.66666666666666, - 15, - ], - Array [ - "set fillStyle", - "#ffffff", - ], - Array [ - "fillRect", - 66.66666666666666, - 236, - 1, + 65.66666666666666, 15, ], Array [ @@ -698,20 +632,9 @@ Array [ ], Array [ "fillRect", - 133.33333333333331, + 134.33333333333331, 236, - 66.66666666666667, - 15, - ], - Array [ - "set fillStyle", - "#ffffff", - ], - Array [ - "fillRect", - 133.33333333333331, - 236, - 1, + 65.66666666666667, 15, ], Array [ @@ -734,20 +657,9 @@ Array [ ], Array [ "fillRect", - 0, - 220, - 66.66666666666666, - 15, - ], - Array [ - "set fillStyle", - "#ffffff", - ], - Array [ - "fillRect", - 0, - 220, 1, + 220, + 65.66666666666666, 15, ], Array [ @@ -770,20 +682,9 @@ Array [ ], Array [ "fillRect", - 66.66666666666666, + 67.66666666666666, 220, - 66.66666666666666, - 15, - ], - Array [ - "set fillStyle", - "#ffffff", - ], - Array [ - "fillRect", - 66.66666666666666, - 220, - 1, + 65.66666666666666, 15, ], Array [ @@ -806,20 +707,9 @@ Array [ ], Array [ "fillRect", - 66.66666666666666, - 204, - 66.66666666666666, - 15, - ], - Array [ - "set fillStyle", - "#ffffff", - ], - Array [ - "fillRect", - 66.66666666666666, + 67.66666666666666, 204, - 1, + 65.66666666666666, 15, ], Array [ diff --git a/src/test/components/__snapshots__/FooterLinks.test.js.snap b/src/test/components/__snapshots__/FooterLinks.test.js.snap index 9f01adba81..8dddb76b4f 100644 --- a/src/test/components/__snapshots__/FooterLinks.test.js.snap +++ b/src/test/components/__snapshots__/FooterLinks.test.js.snap @@ -42,6 +42,11 @@ exports[`correctly renders the FooterLinks component 1`] = ` class="appFooterLinksLanguageSwitcher" title="Change language" > +