diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 5675c992ad76f3..2fd1e6446064e8 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -90,6 +90,7 @@ scripts/ts-node @microsoft/fluentui-react-build scripts/update-release-notes @microsoft/fluentui-react-build scripts/utils @microsoft/fluentui-react-build scripts/webpack @microsoft/fluentui-react-build +scripts/perf-test-flamegrill @microsoft/fluentui-react-build #### Fluent UI N* packages/a11y-rules @microsoft/fluentui-northstar diff --git a/apps/perf-test-react-components/config/perf-test/index.ts b/apps/perf-test-react-components/config/perf-test/index.ts new file mode 100644 index 00000000000000..7f1cfa5658276e --- /dev/null +++ b/apps/perf-test-react-components/config/perf-test/index.ts @@ -0,0 +1,21 @@ +import * as path from 'path'; +import { scenarioIterations } from './scenarioIterations'; +import { scenarioRenderTypes } from './scenarioRenderTypes'; +import { scenarioNames } from './scenarioNames'; + +const projectRoot = path.join(__dirname, '../../'); +const projectRootPath = 'apps/perf-test-react-components'; +const outDir = path.join(projectRoot, './dist'); +const tempDir = path.join(projectRoot, './logfiles'); +const scenariosSrcDirPath = path.join(projectRoot, './src/scenarios'); + +export const config = { + projectName: '@fluentui/react-components', + projectRootPath, + outDir, + tempDir, + scenariosSrcDirPath, + scenarioIterations, + scenarioRenderTypes, + scenarioNames, +}; diff --git a/apps/perf-test-react-components/src/scenarioIterations.ts b/apps/perf-test-react-components/config/perf-test/scenarioIterations.ts similarity index 57% rename from apps/perf-test-react-components/src/scenarioIterations.ts rename to apps/perf-test-react-components/config/perf-test/scenarioIterations.ts index aa2482f34f6cbc..4beb9c38f39826 100644 --- a/apps/perf-test-react-components/src/scenarioIterations.ts +++ b/apps/perf-test-react-components/config/perf-test/scenarioIterations.ts @@ -1,5 +1,6 @@ +import type { ScenarioIterations } from '@fluentui/scripts-tasks'; // You don't have to add scenarios to this structure unless you want their iterations to differ from the default. -export const scenarioIterations = { +export const scenarioIterations: ScenarioIterations = { MakeStyles: 50000, FluentProviderWithTheme: 10, }; diff --git a/apps/perf-test-react-components/src/scenarioNames.ts b/apps/perf-test-react-components/config/perf-test/scenarioNames.ts similarity index 53% rename from apps/perf-test-react-components/src/scenarioNames.ts rename to apps/perf-test-react-components/config/perf-test/scenarioNames.ts index 7bb7aa2c2d28d6..418083e98d1573 100644 --- a/apps/perf-test-react-components/src/scenarioNames.ts +++ b/apps/perf-test-react-components/config/perf-test/scenarioNames.ts @@ -1,3 +1,4 @@ +import type { ScenarioNames } from '@fluentui/scripts-tasks'; // You don't have to add scenarios to this structure unless you want their display name to differ // from their scenario name. -export const scenarioNames = {}; +export const scenarioNames: ScenarioNames = {}; diff --git a/apps/perf-test-react-components/src/scenarioRenderTypes.ts b/apps/perf-test-react-components/config/perf-test/scenarioRenderTypes.ts similarity index 73% rename from apps/perf-test-react-components/src/scenarioRenderTypes.ts rename to apps/perf-test-react-components/config/perf-test/scenarioRenderTypes.ts index e72be02dacf713..d90cf363788559 100644 --- a/apps/perf-test-react-components/src/scenarioRenderTypes.ts +++ b/apps/perf-test-react-components/config/perf-test/scenarioRenderTypes.ts @@ -1,3 +1,4 @@ +import { AllRenderTypes, ScenarioRenderTypes } from '@fluentui/scripts-tasks'; /** * You don't have to add scenarios to this structure unless * you want their render types to differ from the default (mount only). @@ -8,9 +9,6 @@ * memoization logic help avoid certain code paths. */ -const AllRenderTypes = ['mount', 'virtual-rerender', 'virtual-rerender-with-unmount']; -export const DefaultRenderTypes = ['mount']; - -export const scenarioRenderTypes = { +export const scenarioRenderTypes: ScenarioRenderTypes = { FluentProviderWithTheme: AllRenderTypes, }; diff --git a/apps/perf-test-react-components/just.config.ts b/apps/perf-test-react-components/just.config.ts index be016c1495657e..aff98a21110cdc 100644 --- a/apps/perf-test-react-components/just.config.ts +++ b/apps/perf-test-react-components/just.config.ts @@ -1,7 +1,8 @@ -import { getPerfRegressions } from './tasks/perf-test'; -import { preset, task, series } from '@fluentui/scripts-tasks'; +import { preset, task, series, getPerfRegressions } from '@fluentui/scripts-tasks'; + +import { config } from './config/perf-test'; preset(); -task('run-perf-test', getPerfRegressions); +task('run-perf-test', () => getPerfRegressions(config)); task('perf-test', series('build', 'bundle', 'run-perf-test')); diff --git a/apps/perf-test-react-components/package.json b/apps/perf-test-react-components/package.json index 160e3235e64ed9..118e28bd6b5740 100644 --- a/apps/perf-test-react-components/package.json +++ b/apps/perf-test-react-components/package.json @@ -17,6 +17,7 @@ "@fluentui/scripts-webpack": "*" }, "dependencies": { + "@fluentui/scripts-perf-test-flamegrill": "*", "@griffel/core": "^1.9.0", "@fluentui/react-avatar": "^9.4.6", "@fluentui/react-button": "^9.3.6", @@ -27,9 +28,6 @@ "@fluentui/react-spinbutton": "^9.2.6", "@fluentui/react-theme": "^9.1.7", "@microsoft/load-themed-styles": "^1.10.26", - "flamegrill": "0.2.0", - "lodash": "^4.17.15", - "querystring": "^0.2.0", "react": "17.0.2", "react-dom": "17.0.2", "tslib": "^2.1.0" diff --git a/apps/perf-test-react-components/src/app.tsx b/apps/perf-test-react-components/src/app.tsx new file mode 100644 index 00000000000000..08cb2ad24f2dfd --- /dev/null +++ b/apps/perf-test-react-components/src/app.tsx @@ -0,0 +1,10 @@ +import { loadScenarios, render } from '@fluentui/scripts-perf-test-flamegrill'; + +bootstrap(); + +function bootstrap() { + const reqContext = require.context('./scenarios/', false, /^\.\/[^\.]*\.(j|t)sx?$/); + const scenarios = loadScenarios(reqContext); + + render(scenarios); +} diff --git a/apps/perf-test-react-components/src/index.scenarios.tsx b/apps/perf-test-react-components/src/index.scenarios.tsx deleted file mode 100644 index 3c6398b93e7ff2..00000000000000 --- a/apps/perf-test-react-components/src/index.scenarios.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import * as React from 'react'; -import * as ReactDOM from 'react-dom'; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore -import * as qs from 'querystring'; - -const scenarios = require('./scenarios/scenarioList'); - -const div = document.createElement('div'); -document.body.appendChild(div); - -const renderFinishedMarkerId = 'render-done'; -const renderFinishedMarker = document.createElement('div'); -renderFinishedMarker.id = renderFinishedMarkerId; - -// TODO: could default to displaying list of scenarios if param is not provided. -const defaultScenarioName = Object.keys(scenarios)[0]; -const defaultIterations = 10; - -const queryParams = qs.parse(window.location.search.substring(1)); -const iterations = queryParams.iterations ? parseInt(queryParams.iterations as string, 10) : defaultIterations; -const scenario = queryParams.scenario ? (queryParams.scenario as string) : defaultScenarioName; -const renderType = queryParams.renderType; - -const PerfTestScenario = scenarios[scenario]; -if (PerfTestScenario) { - const PerfTestDecorator = PerfTestScenario.decorator || 'div'; - - if (renderType === 'virtual-rerender' || renderType === 'virtual-rerender-with-unmount') { - for (let i = 0; i < iterations - 1; i++) { - ReactDOM.render(, div); - if (renderType === 'virtual-rerender-with-unmount') { - ReactDOM.unmountComponentAtNode(div); - } - } - ReactDOM.render(, div, () => div.appendChild(renderFinishedMarker)); - } else { - // TODO: This seems to increase React (unstable_runWithPriority) render consumption from 4% to 72%! - // const ScenarioContent = Array.from({ length: iterations }, () => scenarios[scenario]); - - // TODO: Using React Fragments increases React (unstable_runWithPriority) render consumption from 4% to 26%. - // It'd be interesting to root cause why at some point. - // ReactDOM.render(<>{Array.from({ length: iterations }, () => (scenarios[scenario]))}, div); - ReactDOM.render( - - {Array.from({ length: iterations }, () => ( - - ))} - , - div, - () => div.appendChild(renderFinishedMarker), - ); - } -} else { - // No PerfTest scenario to render -> done - div.appendChild(renderFinishedMarker); -} diff --git a/apps/perf-test-react-components/src/scenarios/scenarioList.ts b/apps/perf-test-react-components/src/scenarios/scenarioList.ts deleted file mode 100644 index 0da3b397d2c00f..00000000000000 --- a/apps/perf-test-react-components/src/scenarios/scenarioList.ts +++ /dev/null @@ -1,14 +0,0 @@ -const reqContext = require.context('./', false, /^\.\/(?!scenarioList)[^\.]*\.(j|t)sx?$/); - -const scenarios: { [scenarioName: string]: string } = {}; - -reqContext.keys().forEach((key: string) => { - const pathSplit = key.replace(/^\.\//, '').split(/\\\//); - const basename = pathSplit[pathSplit.length - 1]; - const scenarioName = basename.indexOf('.') > -1 ? basename.split('.')[0] : basename; - const scenarioModule = reqContext(key); - - scenarios[scenarioName] = scenarioModule.default || scenarioModule; -}); - -module.exports = scenarios; diff --git a/apps/perf-test-react-components/webpack.config.js b/apps/perf-test-react-components/webpack.config.js index 545d23fe7806e5..e0bbbaa8322cae 100644 --- a/apps/perf-test-react-components/webpack.config.js +++ b/apps/perf-test-react-components/webpack.config.js @@ -8,7 +8,7 @@ const { resources } = require('@fluentui/scripts-webpack'); // TODO: Should root cause why this only works as a serve config. module.exports = resources.createServeConfig( { - entry: './src/index.scenarios.tsx', + entry: './src/app.tsx', mode: 'production', output: { filename: 'perf-test.js', diff --git a/apps/perf-test/config/perf-test/index.ts b/apps/perf-test/config/perf-test/index.ts new file mode 100644 index 00000000000000..1606f5d11c8aab --- /dev/null +++ b/apps/perf-test/config/perf-test/index.ts @@ -0,0 +1,24 @@ +import * as path from 'path'; +import { scenarioIterations } from './scenarioIterations'; +import { scenarioRenderTypes } from './scenarioRenderTypes'; +import { scenarioNames } from './scenarioNames'; + +const projectRoot = path.join(__dirname, '../../'); +const projectRootPath = 'apps/perf-test'; +const outDir = path.join(projectRoot, './dist'); +const tempDir = path.join(projectRoot, './logfiles'); +const scenariosSrcDirPath = path.join(projectRoot, './src/scenarios'); + +export const config = { + projectName: '@fluentui/react', + projectRootPath, + outDir, + tempDir, + scenarioIterations, + scenarioRenderTypes, + scenariosSrcDirPath, + excludeScenarios: [ + // TeachingBubble perf test is hanging after puppeteer pin to v19. Disabling for now to unblock SWC migration work. + 'TeachingBubble', + ], +}; diff --git a/apps/perf-test/src/scenarioIterations.ts b/apps/perf-test/config/perf-test/scenarioIterations.ts similarity index 79% rename from apps/perf-test/src/scenarioIterations.ts rename to apps/perf-test/config/perf-test/scenarioIterations.ts index e04633b61ae235..854ec725e346e5 100644 --- a/apps/perf-test/src/scenarioIterations.ts +++ b/apps/perf-test/config/perf-test/scenarioIterations.ts @@ -1,5 +1,7 @@ +import type { ScenarioIterations } from '@fluentui/scripts-tasks'; + // You don't have to add scenarios to this structure unless you want their iterations to differ from the default. -export const scenarioIterations = { +export const scenarioIterations: ScenarioIterations = { DocumentCardTitle: 1000, Breadcrumb: 1000, CommandBar: 1000, diff --git a/apps/perf-test/src/scenarioNames.ts b/apps/perf-test/config/perf-test/scenarioNames.ts similarity index 78% rename from apps/perf-test/src/scenarioNames.ts rename to apps/perf-test/config/perf-test/scenarioNames.ts index 4a2dd120f69fa5..ec4c0776683a5a 100644 --- a/apps/perf-test/src/scenarioNames.ts +++ b/apps/perf-test/config/perf-test/scenarioNames.ts @@ -1,6 +1,8 @@ +import type { ScenarioNames } from '@fluentui/scripts-tasks'; + // You don't have to add scenarios to this structure unless you want their display name to differ // from their scenario name. -export const scenarioNames = { +export const scenarioNames: ScenarioNames = { DetailsRowFast: 'DetailsRow (fast icons)', DetailsRowNoStyles: 'DetailsRow without styles', DocumentCardTitle: 'DocumentCardTitle with truncation', diff --git a/apps/perf-test/src/scenarioRenderTypes.ts b/apps/perf-test/config/perf-test/scenarioRenderTypes.ts similarity index 75% rename from apps/perf-test/src/scenarioRenderTypes.ts rename to apps/perf-test/config/perf-test/scenarioRenderTypes.ts index 58018e5e5df72f..4e3b2a7adc43b4 100644 --- a/apps/perf-test/src/scenarioRenderTypes.ts +++ b/apps/perf-test/config/perf-test/scenarioRenderTypes.ts @@ -1,3 +1,5 @@ +import { ScenarioRenderTypes, AllRenderTypes } from '@fluentui/scripts-tasks'; + /** * You don't have to add scenarios to this structure unless * you want their render types to differ from the default (mount only). @@ -8,10 +10,7 @@ * memoization logic help avoid certain code paths. */ -const AllRenderTypes = ['mount', 'virtual-rerender', 'virtual-rerender-with-unmount']; -export const DefaultRenderTypes = ['mount']; - -export const scenarioRenderTypes = { +export const scenarioRenderTypes: ScenarioRenderTypes = { ThemeProvider: AllRenderTypes, GroupedList: AllRenderTypes, GroupedListV2: AllRenderTypes, diff --git a/apps/perf-test/just.config.ts b/apps/perf-test/just.config.ts index be016c1495657e..aff98a21110cdc 100644 --- a/apps/perf-test/just.config.ts +++ b/apps/perf-test/just.config.ts @@ -1,7 +1,8 @@ -import { getPerfRegressions } from './tasks/perf-test'; -import { preset, task, series } from '@fluentui/scripts-tasks'; +import { preset, task, series, getPerfRegressions } from '@fluentui/scripts-tasks'; + +import { config } from './config/perf-test'; preset(); -task('run-perf-test', getPerfRegressions); +task('run-perf-test', () => getPerfRegressions(config)); task('perf-test', series('build', 'bundle', 'run-perf-test')); diff --git a/apps/perf-test/package.json b/apps/perf-test/package.json index 76d0185642e22e..f5313c7e582ccf 100644 --- a/apps/perf-test/package.json +++ b/apps/perf-test/package.json @@ -17,12 +17,10 @@ "@fluentui/scripts-webpack": "*" }, "dependencies": { + "@fluentui/scripts-perf-test-flamegrill": "*", "@fluentui/example-data": "^8.4.7", "@fluentui/react": "^8.106.10", "@microsoft/load-themed-styles": "^1.10.26", - "flamegrill": "0.2.0", - "lodash": "^4.17.15", - "querystring": "^0.2.0", "react": "17.0.2", "react-dom": "17.0.2", "tslib": "^2.1.0" diff --git a/apps/perf-test/src/app.tsx b/apps/perf-test/src/app.tsx new file mode 100644 index 00000000000000..5ffdd28d207192 --- /dev/null +++ b/apps/perf-test/src/app.tsx @@ -0,0 +1,14 @@ +import { loadScenarios, render } from '@fluentui/scripts-perf-test-flamegrill'; + +import { initializeIcons } from '@fluentui/react/lib/Icons'; + +bootstrap(); + +function bootstrap() { + const reqContext = require.context('./scenarios/', false, /^\.\/[^\.]*\.(j|t)sx?$/); + const scenarios = loadScenarios(reqContext); + + initializeIcons(); + + render(scenarios); +} diff --git a/apps/perf-test/src/index.scenarios.tsx b/apps/perf-test/src/index.scenarios.tsx deleted file mode 100644 index 14478e96c3c2fd..00000000000000 --- a/apps/perf-test/src/index.scenarios.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import * as React from 'react'; -import * as ReactDOM from 'react-dom'; -import { initializeIcons } from '@fluentui/react/lib/Icons'; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore -import * as qs from 'querystring'; - -const scenarios = require('./scenarios/scenarioList'); - -initializeIcons(); - -const div = document.createElement('div'); -document.body.appendChild(div); - -const renderFinishedMarkerId = 'render-done'; -const renderFinishedMarker = document.createElement('div'); -renderFinishedMarker.id = renderFinishedMarkerId; - -// TODO: could default to displaying list of scenarios if param is not provided. -const defaultScenarioName = Object.keys(scenarios)[0]; -const defaultIterations = 10; - -const queryParams = qs.parse(window.location.search.substring(1)); -const iterations = queryParams.iterations ? parseInt(queryParams.iterations as string, 10) : defaultIterations; -const scenario = queryParams.scenario ? (queryParams.scenario as string) : defaultScenarioName; -const renderType = queryParams.renderType; - -const PerfTestScenario = scenarios[scenario]; -if (PerfTestScenario) { - const PerfTestDecorator = PerfTestScenario.decorator || 'div'; - - if (renderType === 'virtual-rerender' || renderType === 'virtual-rerender-with-unmount') { - for (let i = 0; i < iterations - 1; i++) { - ReactDOM.render(, div); - if (renderType === 'virtual-rerender-with-unmount') { - ReactDOM.unmountComponentAtNode(div); - } - } - ReactDOM.render(, div, () => div.appendChild(renderFinishedMarker)); - } else { - // TODO: This seems to increase React (unstable_runWithPriority) render consumption from 4% to 72%! - // const ScenarioContent = Array.from({ length: iterations }, () => scenarios[scenario]); - - // TODO: Using React Fragments increases React (unstable_runWithPriority) render consumption from 4% to 26%. - // It'd be interesting to root cause why at some point. - // ReactDOM.render(<>{Array.from({ length: iterations }, () => (scenarios[scenario]))}, div); - ReactDOM.render( - - {Array.from({ length: iterations }, () => ( - - ))} - , - div, - () => div.appendChild(renderFinishedMarker), - ); - } -} else { - // No PerfTest scenario to render -> done - div.appendChild(renderFinishedMarker); -} diff --git a/apps/perf-test/src/scenarios/scenarioList.ts b/apps/perf-test/src/scenarios/scenarioList.ts deleted file mode 100644 index 0da3b397d2c00f..00000000000000 --- a/apps/perf-test/src/scenarios/scenarioList.ts +++ /dev/null @@ -1,14 +0,0 @@ -const reqContext = require.context('./', false, /^\.\/(?!scenarioList)[^\.]*\.(j|t)sx?$/); - -const scenarios: { [scenarioName: string]: string } = {}; - -reqContext.keys().forEach((key: string) => { - const pathSplit = key.replace(/^\.\//, '').split(/\\\//); - const basename = pathSplit[pathSplit.length - 1]; - const scenarioName = basename.indexOf('.') > -1 ? basename.split('.')[0] : basename; - const scenarioModule = reqContext(key); - - scenarios[scenarioName] = scenarioModule.default || scenarioModule; -}); - -module.exports = scenarios; diff --git a/apps/perf-test/tasks/perf-test.ts b/apps/perf-test/tasks/perf-test.ts deleted file mode 100644 index 8ece8d87fb919e..00000000000000 --- a/apps/perf-test/tasks/perf-test.ts +++ /dev/null @@ -1,327 +0,0 @@ -import fs from 'fs'; -import path from 'path'; -import flamegrill, { CookResults, Scenarios, ScenarioConfig, CookResult } from 'flamegrill'; -import { scenarioIterations } from '../src/scenarioIterations'; -import { scenarioRenderTypes, DefaultRenderTypes } from '../src/scenarioRenderTypes'; -import { argv } from '@fluentui/scripts-tasks'; - -type ScenarioSetting = Record; -// TODO: consolidate with newer version of fluent perf-test - -// A high number of iterations are needed to get visualization of lower level calls that are infrequently hit by ticks. -// Wiki: https://github.com/microsoft/fluentui/wiki/Perf-Testing -const iterationsDefault = 5000; - -/* eslint-disable @fluentui/max-len */ -// TODO: -// - Results Analysis -// - If System/Framework is cutting out over half of overall time.. what is consuming the rest? How can that be identified for users? -// - Is the case for Toggle.. but not SplitButton. Maybe it's normal for "ok" perf components? -// - Text is not nearly as bad as Toggle with overall lower samples, though, so something in Toggle is more expensive in Framework. -// - Even so, rationalize the time and what's consuming it, even if it's expected. -// - Could compare percentage differences rather than absolute to negate variance. (see variance examples) -// - Would also have to account for new or missing call hierarchies, which will affect overall percentages. -// - Production vs. Debug Build Results -// - Differences? -// - System Calls -// - Appear in CI but just appear as DLLs locally on Windows -// - V8 bug? -// - Ways to demonstrate improvement/regression: -// - How could perf results of https://github.com/microsoft/fluentui/pull/9622 be more succintly seen and summarized? -// - Some way of differing parts of the call graph that differ, from the root function (in this case filteredAssign) -// - https://github.com/microsoft/fluentui/pull/9516 -// - https://github.com/microsoft/fluentui/pull/9548 -// - https://github.com/microsoft/fluentui/pull/9580 -// - https://github.com/microsoft/fluentui/pull/9432 -// - How will pass/fail be determined? -// - What role should React measurements play in results? -// - Tick Processing -// - Flags: "https://github.com/v8/v8/blob/master/tools/tickprocessor.js" -// - Use same version of V8 in Puppeteer to process ticks, somehow -// - If not, need to remove "Testing v8 version different from logging version" from processed logs -// - Results Presentation -// - Use debug version of React to make results more readable? (Where time in React is being spent?) -// - Add links to scenario implementations? -// - Master trends for scenario results -// - Perf -// - Figure out what is causing huge PROCESSED log file size differences between Windows and Mac. (mac perf is pretty bad) -// - Mac files have many thousands more platform functions defined. -// - Way to remove? Any benefit to filtering out while streaming output? (Probably still as time consuming.) -// - Single CPU usage -// - Both perf testing and log processing seem to only use one CPU. -// - Ways to scale / parallelize processing? Node limitation? -// - Is already taking 10 minutes on CI. If users add scenarios it could get out of control. -// - Options: -// - Don't test master, just use posted results. -// - If master has a "bad" variance, this result will be frozen. May be ok since it can happen on PRs too. -// - Reduce default number iterations -// - Allow varying iterations by scenario (for "problem" components like DocumentCardTitle) -// - This may not be good if these components don't "stand out" as much with high samples. -// - Modularize: -// - Standard method for scenario implementation. Storybook? -// - Would require way of delineating scenario execution, if separate logfiles can't be used for each. -// - Options -// - Options to run in development mode to see React stack? -// - If nothing else should document ways that users can do it locally on wiki. -// - Ways to test changes to packages that doesn't require rebuilding everything to perf-test? -// - Add notes to wiki regarding requirements for changing other packages under test. -// - Add webpack serve option with aliasing? -// - Reference selection (local file, OUFR version, etc?) -// - Watch mode for flamegraphs. -// - Would require going back to webserve config mode? -// - Variance -// - Characterize variance -// - Verify results are repeatable and consistent -// - 1 tab vs. 100 tabs simulateneously -// - Eliminate or account for variance! -// - Minimize scenarios. -// - Further ideas: -// - Resizing page to determine reflow -// - React cascading updates on initial component render. -// - Monomorphic vs. Megamorphic Analysis: -// - Sean Larkin said that switching from polymorphic to monomorphic was a webpack optimization. -// - https://mrale.ph/blog/2015/01/11/whats-up-with-monomorphism.html -// - https://dzone.com/articles/impact-of-polymorphism-on-component-based-framewor - -// TODO: other args? -// https://github.com/v8/v8/blob/master/src/flags/flag-definitions.h -// --log-timer-events -// --log-source-code - -// Analysis -// - Why is BaseComponent warnMutuallyExclusive appearing in flamegraphs? -// - It appears the CPU is being consumed simply by calling warnMututallyExclusive. -// - warnMutuallyExlusive impl is neutered but there still perf hit in setting up the args to call it. -// - The "get" in flamegraphs is caused by "this.className" arg. -// - makeAllSafe also consumes time just by having any component extend BaseComponent. -// - Puppeteer.tracing -// - Similar to using profiler in Chrome, does not show bottom-up analysis well -// - Seems to break V8 profile logging output. -// await page.tracing.start({ path: path.join(logPath, testLogFile[0] + '.trace') }); -// await page.goto(testUrl); -// await page.tracing.stop(); - -// Hardcoded PR deploy URL for local testing -const DEPLOY_URL = 'fluentuipr.z22.web.core.windows.net'; - -const urlForDeployPath = process.env.DEPLOYURL - ? `${process.env.DEPLOYURL}/perf-test` - : 'file://' + path.resolve(__dirname, '../dist/'); - -// Temporarily comment out deploy site usage to speed up CI build time and support parallelization. -// At some point perf test should be broken out from CI default pipeline entirely and then can go back to using deploy site. -// For now, use local perf-test bundle so that perf-test job can run ASAP instead of waiting for the perf-test bundle to be deployed. -// const urlForDeploy = urlForDeployPath + '/index.html'; -const urlForDeploy = 'file://' + path.resolve(__dirname, '../dist/') + '/index.html'; - -const targetPath = `heads/${process.env.SYSTEM_PULLREQUEST_TARGETBRANCH || 'master'}`; -const urlForMaster = `https://${process.env.DEPLOYHOST || DEPLOY_URL}/${targetPath}/perf-test/index.html`; - -const outDir = path.join(__dirname, '../dist'); -const tempDir = path.join(__dirname, '../logfiles'); - -export async function getPerfRegressions() { - // For debugging, in case the environment variables used to generate these have unexpected values - console.log(`urlForDeployPath: "${urlForDeployPath}"`); - console.log(`urlForMaster: "${urlForMaster}"`); - - const iterationsArgv: number = argv().iterations; - const iterationsArg = Number.isInteger(iterationsArgv) && iterationsArgv; - - const scenariosAvailable = fs - .readdirSync(path.join(__dirname, '../src/scenarios')) - .filter(name => name.indexOf('scenarioList') < 0) - .map(name => path.basename(name, '.tsx')); - - const scenariosArgv: string = argv().scenarios; - const scenariosArg = scenariosArgv?.split?.(',') || []; - scenariosArg.forEach(scenario => { - if (!scenariosAvailable.includes(scenario)) { - throw new Error(`Invalid scenario: ${scenario}.`); - } - }); - - let scenarioList = scenariosArg.length > 0 ? scenariosArg : scenariosAvailable; - // TeachingBubble perf test is hanging after puppeteer pin to v19. Disabling for now to unblock SWC migration work. - scenarioList = scenarioList.filter(scenario => scenario !== 'TeachingBubble'); - - const scenarios: Scenarios = {}; - const scenarioSettings: ScenarioSetting = {}; - scenarioList.forEach(scenarioName => { - if (!scenariosAvailable.includes(scenarioName)) { - throw new Error(`Invalid scenario: ${scenarioName}.`); - } - const iterations: number = - iterationsArg || scenarioIterations[scenarioName as keyof typeof scenarioIterations] || iterationsDefault; - const renderTypes: string[] = - scenarioRenderTypes[scenarioName as keyof typeof scenarioRenderTypes] || DefaultRenderTypes; - - renderTypes.forEach(renderType => { - const scenarioKey = `${scenarioName}-${renderType}`; - const testUrlParams = `?scenario=${scenarioName}&iterations=${iterations}&renderType=${renderType}`; - - scenarios[scenarioKey] = { - baseline: `${urlForMaster}${testUrlParams}`, - scenario: `${urlForDeploy}${testUrlParams}`, - }; - - scenarioSettings[scenarioKey] = { - scenarioName, - iterations, - renderType, - }; - }); - }); - - console.log(`\nRunning scenarios:`); - console.dir(scenarios); - - if (fs.existsSync(tempDir)) { - const tempContents = fs.readdirSync(tempDir); - - if (tempContents.length > 0) { - console.log(`Unexpected files already present in ${tempDir}`); - tempContents.forEach(logFile => { - const logFilePath = path.join(tempDir, logFile); - console.log(`Deleting ${logFilePath}`); - fs.unlinkSync(logFilePath); - }); - } - } - - const scenarioConfig: ScenarioConfig = { - outDir, - tempDir, - pageActions: async (page, options) => { - // Occasionally during our CI, page takes unexpected amount of time to navigate (unsure about the root cause). - // Removing the timeout to avoid perf-test failures but be cautious about long test runs. - page.setDefaultTimeout(0); - - await page.goto(options.url); - await page.waitForSelector('#render-done'); - }, - }; - - const scenarioResults: CookResults = await flamegrill.cook(scenarios, scenarioConfig); - - const comment = createReport(scenarioSettings, scenarioResults); - - // TODO: determine status according to perf numbers - const status = 'success'; - - console.log(`Perf evaluation status: ${status}`); - console.log(`Writing comment to file:\n${comment}`); - - // Write results to file - fs.writeFileSync(path.join(outDir, 'perfCounts.html'), comment); - - console.log(`##vso[task.setvariable variable=PerfCommentFilePath;]apps/perf-test/dist/perfCounts.html`); - console.log(`##vso[task.setvariable variable=PerfCommentStatus;]${status}`); -} - -/** - * Create test summary based on test results. - */ -function createReport(scenarioSettings: ScenarioSetting, testResults: CookResults) { - const report = '## [Perf Analysis (`@fluentui/react`)](https://github.com/microsoft/fluentui/wiki/Perf-Testing)\n' - - // Show only significant changes by default. - .concat(createScenarioTable(scenarioSettings, testResults, false)) - - // Show all results in a collapsible table. - .concat('
All results

') - .concat(createScenarioTable(scenarioSettings, testResults, true)) - .concat('

\n\n'); - - return report; -} - -/** - * Create a table of scenario results. - * @param showAll Show only significant results by default. - */ -function createScenarioTable(scenarioSettings: ScenarioSetting, testResults: CookResults, showAll: boolean) { - const resultsToDisplay = Object.keys(testResults).filter( - key => showAll || testResults[key].analysis?.regression?.isRegression, - ); - - if (resultsToDisplay.length === 0) { - return '

No significant results to display.

'; - } - - const result = ` - - - - - - - - - `.concat( - resultsToDisplay - .map(key => { - const testResult = testResults[key]; - const { scenarioName, iterations, renderType } = scenarioSettings[key] || {}; - - return ` - - - ${getCell(testResult, true)} - ${getCell(testResult, false)} - - ${getRegression(testResult)} - `; - }) - .join('\n') - .concat(`
ScenarioRender type - Master Ticks - - PR Ticks - IterationsStatus
${scenarioName}${renderType}${iterations}
`), - ); - - console.log('result: ' + result); - - return result; -} - -/** - * Helper that renders an output cell based on a test result. - */ -function getCell(testResult: CookResult, getBaseline: boolean) { - let flamegraphFile = testResult.processed.output && testResult.processed.output.flamegraphFile; - let errorFile = testResult.processed.error && testResult.processed.error.errorFile; - let numTicks = testResult.analysis && testResult.analysis.numTicks; - - if (getBaseline) { - const processedBaseline = testResult.processed.baseline; - flamegraphFile = processedBaseline && processedBaseline.output && processedBaseline.output.flamegraphFile; - errorFile = processedBaseline && processedBaseline.error && processedBaseline.error.errorFile; - numTicks = testResult.analysis && testResult.analysis.baseline && testResult.analysis.baseline.numTicks; - } - - const cell = errorFile - ? `err` - : flamegraphFile - ? `${numTicks}` - : `n/a`; - - return `${cell}`; -} - -/** - * Helper that renders an output cell based on a test result. - */ -function getRegression(testResult: CookResult) { - const cell = - testResult.analysis && testResult.analysis.regression && testResult.analysis.regression.isRegression - ? testResult.analysis.regression.regressionFile - ? `Possible regression` - : '' - : ''; - - return `${cell}`; -} diff --git a/apps/perf-test/webpack.config.js b/apps/perf-test/webpack.config.js index 545d23fe7806e5..e0bbbaa8322cae 100644 --- a/apps/perf-test/webpack.config.js +++ b/apps/perf-test/webpack.config.js @@ -8,7 +8,7 @@ const { resources } = require('@fluentui/scripts-webpack'); // TODO: Should root cause why this only works as a serve config. module.exports = resources.createServeConfig( { - entry: './src/index.scenarios.tsx', + entry: './src/app.tsx', mode: 'production', output: { filename: 'perf-test.js', diff --git a/azure-pipelines.perf-test.yml b/azure-pipelines.perf-test.yml index 49fdd4137697f1..08682210e8c386 100644 --- a/azure-pipelines.perf-test.yml +++ b/azure-pipelines.perf-test.yml @@ -56,7 +56,7 @@ jobs: displayName: Build to Perf Test (Northstar) - script: | - yarn perf:test:base + yarn perf:test --base condition: eq(variables.isPR, false) workingDirectory: packages/fluentui/perf-test-northstar displayName: Run Perf Test Base (Northstar) @@ -83,8 +83,8 @@ jobs: inputs: githubOwner: microsoft githubRepo: 'fluentui' - blobFilePath: '$(Build.SourcesDirectory)/$(PerfCommentFilePathNorthstar)' - status: '$(PerfCommentStatusNorthstar)' + blobFilePath: '$(Build.SourcesDirectory)/$(PerfCommentFilePathReactNorthstar)' + status: '$(PerfCommentStatusReactNorthstar)' uniqueId: 'perfComment9424' - script: | @@ -147,8 +147,8 @@ jobs: inputs: githubOwner: microsoft githubRepo: 'fluentui' - blobFilePath: '$(Build.SourcesDirectory)/$(PerfCommentFilePath)' - status: '$(PerfCommentStatus)' + blobFilePath: '$(Build.SourcesDirectory)/$(PerfCommentFilePathReact)' + status: '$(PerfCommentStatusReact)' uniqueId: 'perfComment9423' - template: .devops/templates/cleanup.yml diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 989520511d0283..3a4a437264cd75 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -34,8 +34,10 @@ jobs: displayName: NX workspace lint - script: | - # @fluentui/api-docs is used within apps/public-docsite-resources/just.config.ts thus it needs to be build in advance + # @fluentui/api-docs is used within apps/public-docsite-resources/just.config.ts, thus it needs to be build in advance yarn workspace @fluentui/api-docs build + # @fluentui/digest is used within packages/fluentui/perf-test-northstar/just.config.ts, thus it needs to be build in advance + yarn workspace @fluentui/digest build yarn tsc -p ./tsconfig.json displayName: Type-check just.config.ts files diff --git a/change/@fluentui-monaco-editor-ddb09896-2b72-4b5a-a337-3f5cc2597ae9.json b/change/@fluentui-monaco-editor-ddb09896-2b72-4b5a-a337-3f5cc2597ae9.json new file mode 100644 index 00000000000000..0dfb945db3ce40 --- /dev/null +++ b/change/@fluentui-monaco-editor-ddb09896-2b72-4b5a-a337-3f5cc2597ae9.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "fix(scripts-tasks): format and update API usage which was modified by eslint export * rule unwrap autofix", + "packageName": "@fluentui/monaco-editor", + "email": "martinhochel@microsoft.com", + "dependentChangeType": "none" +} diff --git a/package.json b/package.json index d72a03c4f95a49..bfab1faa390f91 100644 --- a/package.json +++ b/package.json @@ -241,6 +241,7 @@ "extract-comments": "1.1.0", "file-loader": "6.2.0", "find-free-port": "2.0.0", + "flamegrill": "0.2.0", "fork-ts-checker-webpack-plugin": "6.1.0", "fs-extra": "8.1.0", "geckodriver": "3.0.2", diff --git a/packages/fluentui/perf-test-northstar/just.config.ts b/packages/fluentui/perf-test-northstar/just.config.ts index 0636533c213632..c9885ea8db84c2 100644 --- a/packages/fluentui/perf-test-northstar/just.config.ts +++ b/packages/fluentui/perf-test-northstar/just.config.ts @@ -1,5 +1,6 @@ import path from 'path'; import { series, task, argv, taskPresets } from 'just-scripts'; +import { config } from './tasks/perf-test.config'; taskPresets.lib(); @@ -26,9 +27,9 @@ task('perf-test:bundle', bundleStories()); task('perf-test:run', () => { // delay require in case digest isn't built yet - const runPerfTest = require('./tasks/perf-test').default; + const { getPerfRegressions } = require('./tasks/perf-test') as typeof import('./tasks/perf-test'); - return runPerfTest(argv().base); + return getPerfRegressions(config, (argv() as { base?: boolean }).base); }); // TOOD: is build doing anything meaningful? only if there's source that's not a just script? diff --git a/packages/fluentui/perf-test-northstar/package.json b/packages/fluentui/perf-test-northstar/package.json index ce5b42cf4dfb43..1e82b92462e492 100644 --- a/packages/fluentui/perf-test-northstar/package.json +++ b/packages/fluentui/perf-test-northstar/package.json @@ -8,15 +8,17 @@ "bundle": "just-scripts perf-test:bundle", "clean": "just-scripts clean", "test": "just-scripts test", - "perf:test": "just-scripts perf-test", - "perf:test:base": "just-scripts perf-test --base" + "perf:test": "just-scripts perf-test" }, - "devDependencies": { - "@fluentui/digest": "^0.66.4", + "dependencies": { "@fluentui/react-northstar": "^0.66.4", "@fluentui/react-northstar-prototypes": "^0.66.4", + "@fluentui/digest": "^0.66.4", "flamegrill": "0.2.0" }, + "devDependencies": { + "@fluentui/scripts-tasks": "*" + }, "publishConfig": { "access": "public" } diff --git a/packages/fluentui/perf-test-northstar/tasks/fluentPerfRegressions.ts b/packages/fluentui/perf-test-northstar/tasks/fluentPerfRegressions.ts index 7e9bc60223d233..461a962712a0b6 100644 --- a/packages/fluentui/perf-test-northstar/tasks/fluentPerfRegressions.ts +++ b/packages/fluentui/perf-test-northstar/tasks/fluentPerfRegressions.ts @@ -1,6 +1,9 @@ import * as _ from 'lodash'; import * as path from 'path'; import { workspaceRoot } from 'nx/src/utils/app-root'; +import { perfTestEnv } from '@fluentui/scripts-tasks'; + +import { config } from './perf-test.config'; // TODO: check false positive potential regression reports in fluent ui repo and fix @@ -26,9 +29,10 @@ export function getFluentPerfRegressions() { } function linkToFlamegraph(value: string, filename: string) { - const urlForDeployPath = process.env.DEPLOYURL - ? `${process.env.DEPLOYURL}/perf-test-northstar` - : 'file://' + path.resolve(workspaceRoot, 'packages/fluentui/perf-test/dist'); + const projectRootDirectoryName = path.basename(config.projectRootPath); + const urlForDeployPath = perfTestEnv.DEPLOYURL + ? `${perfTestEnv.DEPLOYURL}/${projectRootDirectoryName}` + : 'file://' + path.resolve(workspaceRoot, `${config.projectRootPath}/dist`); return `[${value}](${urlForDeployPath}/${path.basename(filename)})`; } @@ -103,12 +107,12 @@ function reportResults(perfCounts: any, reporter: Reporter) { } const checkPerfRegressions = (reporter: Reporter) => { - let perfCounts; + let perfCounts: any; - reporter.markdown('## Perf Analysis (`@fluentui/react-northstar`)'); + reporter.markdown(`## Perf Analysis (\`${config.projectName}\`)`); try { - perfCounts = require(path.resolve(workspaceRoot, 'packages/perf-test-northstar/dist/perfCounts.json')); + perfCounts = require(path.resolve(workspaceRoot, `${config.projectRootPath}/dist/perfCounts.json`)); } catch { reporter.warn('No perf measurements available'); return; diff --git a/packages/fluentui/perf-test-northstar/tasks/perf-test.config.ts b/packages/fluentui/perf-test-northstar/tasks/perf-test.config.ts new file mode 100644 index 00000000000000..26b48fc8565826 --- /dev/null +++ b/packages/fluentui/perf-test-northstar/tasks/perf-test.config.ts @@ -0,0 +1,10 @@ +import * as path from 'path'; +import type { PerfRegressionConfig } from '@fluentui/scripts-tasks'; + +export const config: PerfRegressionConfig = { + projectRootPath: 'packages/fluentui/perf-test-northstar', + projectName: '@fluentui/react-northstar', + outDir: path.join(__dirname, '../dist'), + tempDir: path.join(__dirname, '../logfiles'), + scenariosSrcDirPath: '../dist/stories.js', +}; diff --git a/packages/fluentui/perf-test-northstar/tasks/perf-test.ts b/packages/fluentui/perf-test-northstar/tasks/perf-test.ts index b8c77abfb9a808..e3059183e69282 100644 --- a/packages/fluentui/perf-test-northstar/tasks/perf-test.ts +++ b/packages/fluentui/perf-test-northstar/tasks/perf-test.ts @@ -3,10 +3,9 @@ import path from 'path'; import _ from 'lodash'; import flamegrill, { CookResult, CookResults, ScenarioConfig, Scenarios } from 'flamegrill'; import { generateUrl } from '@fluentui/digest'; -import { getFluentPerfRegressions } from './fluentPerfRegressions'; +import { perfTestEnv, PerfRegressionConfig } from '@fluentui/scripts-tasks'; -// Hardcoded PR deploy URL for local testing -const DEPLOY_URL = 'fluentuipr.z22.web.core.windows.net'; +import { getFluentPerfRegressions } from './fluentPerfRegressions'; type ExtendedCookResult = CookResult & { extended: { @@ -35,18 +34,17 @@ const urlForDeployPath = `file://${path.resolve(__dirname, '../dist/')}`; const urlForDeploy = `${urlForDeployPath}/index.html`; const defaultIterations = 1; -const outDir = path.join(__dirname, '../dist'); -const tempDir = path.join(__dirname, '../logfiles'); - console.log(`__dirname: ${__dirname}`); -export default async function getPerfRegressions(baselineOnly: boolean = false) { - let urlForMaster: string | undefined; +export async function getPerfRegressions(config: PerfRegressionConfig, baselineOnly = false) { + const { outDir, tempDir, scenariosSrcDirPath, projectName, projectRootPath } = config; + const projectRootDirectoryName = path.basename(projectRootPath); + const projectEnvVars = perfTestEnv.EnvVariablesByProject[projectName]; - if (!baselineOnly) { - const targetPath = `heads/${process.env.SYSTEM_PULLREQUEST_TARGETBRANCH || 'master'}`; - urlForMaster = `https://${process.env.DEPLOYHOST || DEPLOY_URL}/${targetPath}/perf-test-northstar/index.html`; - } + const targetPath = `heads/${perfTestEnv.SYSTEM_PULLREQUEST_TARGETBRANCH || 'master'}`; + const urlForMaster = baselineOnly + ? undefined + : `https://${perfTestEnv.DEPLOYHOST}/${targetPath}/${projectRootDirectoryName}/index.html`; // For debugging, in case the environment variables used to generate these have unexpected values console.log(`urlForDeployPath: "${urlForDeployPath}"`); @@ -59,7 +57,7 @@ export default async function getPerfRegressions(baselineOnly: boolean = false) const scenarioList: string[] = []; // TODO: can this get typing somehow? can't be imported since file is only available after build - const test = require('../dist/stories.js'); + const test = require(scenariosSrcDirPath); const { stories } = test.default; console.log('stories:'); @@ -75,8 +73,13 @@ export default async function getPerfRegressions(baselineOnly: boolean = false) scenario: generateUrl(urlForDeploy, kindKey, storyKey, getIterations(stories, kindKey, storyKey)), ...(!baselineOnly && storyKey !== 'Fabric' && { - // Optimization: skip baseline comparision for Fabric - baseline: generateUrl(urlForMaster, kindKey, storyKey, getIterations(stories, kindKey, storyKey)), + // Optimization: skip baseline comparison for Fabric + baseline: generateUrl( + urlForMaster as string, + kindKey, + storyKey, + getIterations(stories, kindKey, storyKey), + ), }), }; }); @@ -126,13 +129,14 @@ export default async function getPerfRegressions(baselineOnly: boolean = false) // Write results to file fs.writeFileSync(path.join(outDir, 'perfCounts.html'), comment); - console.log( - `##vso[task.setvariable variable=PerfCommentFilePathNorthstar;]packages/fluentui/perf-test-northstar/dist/perfCounts.html`, - ); - console.log(`##vso[task.setvariable variable=PerfCommentStatusNorthstar;]${status}`); + console.log(`##vso[task.setvariable variable=${projectEnvVars.filePath};]${projectRootPath}/dist/perfCounts.html`); + console.log(`##vso[task.setvariable variable=${projectEnvVars.status};]${status}`); } -function extendCookResults(stories, testResults: CookResults): ExtendedCookResults { +function extendCookResults( + stories: { [x: string]: { [x: string]: any } }, + testResults: CookResults, +): ExtendedCookResults { return _.mapValues(testResults, (testResult, resultKey) => { const kind = getKindKey(resultKey); const story = getStoryKey(resultKey); @@ -158,7 +162,7 @@ function extendCookResults(stories, testResults: CookResults): ExtendedCookResul * @param {CookResults} testResults * @returns {string} */ -function createReport(stories, testResults: ExtendedCookResults): string { +function createReport(stories: any, testResults: ExtendedCookResults): string { // TODO: We can't do CI, measure baseline or do regression analysis until master & PR files are deployed and publicly accessible. // TODO: Fluent reporting is outside of this script so this code will probably be moved entirely on perf-test consolidation. // // Show only significant changes by default. @@ -179,15 +183,9 @@ function createReport(stories, testResults: ExtendedCookResults): string { * @param {boolean} showAll Show only significant results by default. * @returns {string} */ -function createScenarioTable(stories, testResults: ExtendedCookResults, showAll: boolean): string { +function createScenarioTable(stories: any, testResults: ExtendedCookResults, showAll: boolean): string { const resultsToDisplay = Object.keys(testResults) - .filter( - key => - showAll || - (testResults[key].analysis && - testResults[key].analysis.regression && - testResults[key].analysis.regression.isRegression), - ) + .filter(key => showAll || testResults[key]?.analysis?.regression?.isRegression) .filter(testResultKey => getStoryKey(testResultKey) !== 'Fabric') .sort(); @@ -272,8 +270,13 @@ function getStoryKey(resultKey: string): string { return story; } -function getTpiResult(testResults, stories, kind, story): number | undefined { - let tpi = undefined; +function getTpiResult( + testResults: CookResults, + stories: { [x: string]: { [x: string]: any } }, + kind: string, + story: string, +): number | undefined { + let tpi: number | undefined; if (stories[kind][story]) { const resultKey = `${kind}.${story}`; const testResult = testResults[resultKey]; @@ -284,18 +287,25 @@ function getTpiResult(testResults, stories, kind, story): number | undefined { return tpi; } -function getIterations(stories, kind, story): number { +function getIterations( + stories: { + [x: string]: Partial<{ + default: { iterations?: number }; + [x: string]: { iterations?: number }; + }>; + }, + kind: string, + story: string, +): number { // Give highest priority to most localized definition of iterations. Story => kind => default. - return ( - stories[kind][story].iterations || (stories[kind].default && stories[kind].default.iterations) || defaultIterations - ); + return stories[kind][story]?.iterations || stories[kind].default?.iterations || defaultIterations; } function getTicks(testResult: CookResult): number | undefined { return testResult.analysis && testResult.analysis.numTicks; } -function linkifyResult(testResult, resultContent, getBaseline) { +function linkifyResult(testResult: CookResult, resultContent: string | number | undefined, getBaseline: boolean) { let flamegraphFile = testResult.processed.output && testResult.processed.output.flamegraphFile; let errorFile = testResult.processed.error && testResult.processed.error.errorFile; diff --git a/scripts/perf-test-flamegrill/.eslintrc.json b/scripts/perf-test-flamegrill/.eslintrc.json new file mode 100644 index 00000000000000..4f5307c1c0b5ba --- /dev/null +++ b/scripts/perf-test-flamegrill/.eslintrc.json @@ -0,0 +1,21 @@ +{ + "extends": ["plugin:@fluentui/eslint-plugin/node", "plugin:@fluentui/eslint-plugin/imports"], + "rules": { + "@fluentui/max-len": "off", + "import/no-extraneous-dependencies": [ + "error", + { + "packageDir": [".", "../../"] + } + ] + }, + "overrides": [ + { + "files": "index.d.ts", + "rules": { + "import/no-self-import": "off" + } + } + ], + "root": true +} diff --git a/scripts/perf-test-flamegrill/jest.config.js b/scripts/perf-test-flamegrill/jest.config.js new file mode 100644 index 00000000000000..e6ac1cdb5dff41 --- /dev/null +++ b/scripts/perf-test-flamegrill/jest.config.js @@ -0,0 +1,14 @@ +// @ts-check + +/** + * @type {import('@jest/types').Config.InitialOptions} + */ +module.exports = { + displayName: 'scripts-perf-test-flamegrill', + preset: '../../jest.preset.js', + transform: { + '^.+\\.tsx?$': 'ts-jest', + }, + coverageDirectory: './coverage', + testEnvironment: 'node', +}; diff --git a/scripts/perf-test-flamegrill/package.json b/scripts/perf-test-flamegrill/package.json new file mode 100644 index 00000000000000..1edc441192a4f4 --- /dev/null +++ b/scripts/perf-test-flamegrill/package.json @@ -0,0 +1,20 @@ +{ + "name": "@fluentui/scripts-perf-test-flamegrill", + "version": "0.0.1", + "private": true, + "main": "src/index.ts", + "scripts": { + "format": "prettier -w --ignore-path ../../.prettierignore .", + "format:check": "yarn format -c", + "lint": "eslint --ext .ts,.js .", + "test": "jest --passWithNoTests", + "type-check": "tsc -b tsconfig.json" + }, + "peerDependencies": { + "@types/react": ">=16.8.0 <19.0.0", + "@types/react-dom": ">=16.8.0 <19.0.0", + "react": ">=16.8.0 <19.0.0", + "react-dom": ">=16.8.0 <19.0.0", + "webpack": "^5.75.0" + } +} diff --git a/scripts/perf-test-flamegrill/src/index.ts b/scripts/perf-test-flamegrill/src/index.ts new file mode 100644 index 00000000000000..ee0b3bc41ab867 --- /dev/null +++ b/scripts/perf-test-flamegrill/src/index.ts @@ -0,0 +1,2 @@ +export { render } from './renderer'; +export { loadScenarios } from './load-scenarios'; diff --git a/scripts/perf-test-flamegrill/src/load-scenarios.ts b/scripts/perf-test-flamegrill/src/load-scenarios.ts new file mode 100644 index 00000000000000..37a596cf4c838f --- /dev/null +++ b/scripts/perf-test-flamegrill/src/load-scenarios.ts @@ -0,0 +1,20 @@ +import type { Scenarios } from './types'; + +/** + * + * //TODO this uses proprietary webpack require.context which is not future-proof - use standard ESM + */ +export function loadScenarios(context: __WebpackModuleApi.RequireContext): Scenarios { + const scenarios: Scenarios = {}; + + context.keys().forEach((key: string) => { + const pathSplit = key.replace(/^\.\//, '').split(/\\\//); + const basename = pathSplit[pathSplit.length - 1]; + const scenarioName = basename.indexOf('.') > -1 ? basename.split('.')[0] : basename; + const scenarioModule = context(key); + + scenarios[scenarioName] = scenarioModule.default || scenarioModule; + }); + + return scenarios; +} diff --git a/scripts/perf-test-flamegrill/src/renderer.tsx b/scripts/perf-test-flamegrill/src/renderer.tsx new file mode 100644 index 00000000000000..fa6940393a01d6 --- /dev/null +++ b/scripts/perf-test-flamegrill/src/renderer.tsx @@ -0,0 +1,57 @@ +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; + +import type { Scenarios } from './types'; + +export function render(scenarios: Scenarios) { + const div = document.createElement('div'); + document.body.appendChild(div); + + const renderFinishedMarkerId = 'render-done'; + const renderFinishedMarker = document.createElement('div'); + renderFinishedMarker.id = renderFinishedMarkerId; + + // TODO: could default to displaying list of scenarios if param is not provided. + const defaultScenarioName = Object.keys(scenarios)[0]; + const defaultIterations = 10; + const queryParams = new URLSearchParams(window.location.search); + + const iterations = queryParams.get('iterations') ? Number(queryParams.get('iterations')) : defaultIterations; + const scenario = queryParams.get('scenario') ?? defaultScenarioName; + const renderType = queryParams.get('renderType'); + + const PerfTestScenario = scenarios[scenario]; + + if (PerfTestScenario) { + const PerfTestDecorator = PerfTestScenario.decorator || 'div'; + + if (renderType === 'virtual-rerender' || renderType === 'virtual-rerender-with-unmount') { + for (let i = 0; i < iterations - 1; i++) { + ReactDOM.render(, div); + if (renderType === 'virtual-rerender-with-unmount') { + ReactDOM.unmountComponentAtNode(div); + } + } + ReactDOM.render(, div, () => div.appendChild(renderFinishedMarker)); + } else { + // TODO: This seems to increase React (unstable_runWithPriority) render consumption from 4% to 72%! + // const ScenarioContent = Array.from({ length: iterations }, () => scenarios[scenario]); + + // TODO: Using React Fragments increases React (unstable_runWithPriority) render consumption from 4% to 26%. + // It'd be interesting to root cause why at some point. + // ReactDOM.render(<>{Array.from({ length: iterations }, () => (scenarios[scenario]))}, div); + ReactDOM.render( + + {Array.from({ length: iterations }, () => ( + + ))} + , + div, + () => div.appendChild(renderFinishedMarker), + ); + } + } else { + // No PerfTest scenario to render -> done + div.appendChild(renderFinishedMarker); + } +} diff --git a/scripts/perf-test-flamegrill/src/types.ts b/scripts/perf-test-flamegrill/src/types.ts new file mode 100644 index 00000000000000..dea4ce7101aed6 --- /dev/null +++ b/scripts/perf-test-flamegrill/src/types.ts @@ -0,0 +1,7 @@ +import * as React from 'react'; +export interface Scenario { + (): JSX.Element; + decorator?: (props: { children: React.ReactNode }) => JSX.Element; +} + +export type Scenarios = { [scenarioExportName: string]: Scenario }; diff --git a/scripts/perf-test-flamegrill/tsconfig.json b/scripts/perf-test-flamegrill/tsconfig.json new file mode 100644 index 00000000000000..b289e657bc0e53 --- /dev/null +++ b/scripts/perf-test-flamegrill/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "@tsconfig/node14/tsconfig.json", + "compilerOptions": { + "target": "ES2019", + "pretty": true, + "noEmit": true, + "allowJs": true, + "checkJs": true, + "sourceMap": true, + "noUnusedLocals": true + }, + "include": [], + "files": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/scripts/perf-test-flamegrill/tsconfig.lib.json b/scripts/perf-test-flamegrill/tsconfig.lib.json new file mode 100644 index 00000000000000..5354d98dbfac2c --- /dev/null +++ b/scripts/perf-test-flamegrill/tsconfig.lib.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "lib": ["ES2019", "DOM"], + "jsx": "react", + "outDir": "../../dist/out-tsc", + "types": ["webpack-env"] + }, + "exclude": ["**/*.spec.ts", "**/*.spec.tsx", "**/*.test.ts", "**/*.test.tsx"], + "include": ["./**/*.ts", "./**/*.tsx", "./**/*.js"] +} diff --git a/scripts/perf-test-flamegrill/tsconfig.spec.json b/scripts/perf-test-flamegrill/tsconfig.spec.json new file mode 100644 index 00000000000000..bc196f0514f5b1 --- /dev/null +++ b/scripts/perf-test-flamegrill/tsconfig.spec.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "CommonJS", + "outDir": "dist", + "types": ["jest", "node"] + }, + "include": ["**/*.spec.ts", "**/*.test.ts", "**/*.spec.tsx", "**/*.test.tsx", "**/*.d.ts"] +} diff --git a/scripts/tasks/package.json b/scripts/tasks/package.json index a1e93c82db61b4..a994fd70eb5501 100644 --- a/scripts/tasks/package.json +++ b/scripts/tasks/package.json @@ -13,6 +13,7 @@ "dependencies": { "@fluentui/scripts-monorepo": "*", "@fluentui/scripts-utils": "*", - "@fluentui/scripts-prettier": "*" + "@fluentui/scripts-prettier": "*", + "flamegrill": "0.2.0" } } diff --git a/scripts/tasks/src/index.ts b/scripts/tasks/src/index.ts index a06699dde7e7de..b8525731e60e8a 100644 --- a/scripts/tasks/src/index.ts +++ b/scripts/tasks/src/index.ts @@ -87,3 +87,13 @@ export type { export { preset } from './presets'; export { expandSourcePath } from './copy'; export { postprocessTask } from './postprocess'; +export { + getPerfRegressions, + AllRenderTypes, + DefaultRenderTypes, + ScenarioIterations, + ScenarioRenderTypes, + ScenarioNames, + perfTestEnv, + PerfRegressionConfig, +} from './perf-task'; diff --git a/scripts/tasks/src/perf-task/env.ts b/scripts/tasks/src/perf-task/env.ts new file mode 100644 index 00000000000000..f149b283727f6e --- /dev/null +++ b/scripts/tasks/src/perf-task/env.ts @@ -0,0 +1,22 @@ +/** + * Hardcoded PR deploy URL for local testing + */ +const DEPLOY_URL = 'fluentuipr.z22.web.core.windows.net'; + +export const DEPLOYHOST = process.env.DEPLOYHOST ?? DEPLOY_URL; +export const DEPLOYURL = process.env.DEPLOYURL; +export const SYSTEM_PULLREQUEST_TARGETBRANCH = process.env.SYSTEM_PULLREQUEST_TARGETBRANCH; + +const envPrefix = { filePath: 'PerfCommentFilePath', status: 'PerfCommentStatus' }; + +export const EnvVariablesByProject: { [projectName: string]: { filePath: string; status: string } } = { + '@fluentui/react': { filePath: `${envPrefix.filePath}React`, status: `${envPrefix.status}React` }, + '@fluentui/react-components': { + filePath: `${envPrefix.filePath}ReactComponents`, + status: `${envPrefix.status}ReactComponents`, + }, + '@fluentui/react-northstar': { + filePath: `${envPrefix.filePath}ReactNorthstar`, + status: `${envPrefix.status}ReactNorthstar`, + }, +}; diff --git a/scripts/tasks/src/perf-task/index.ts b/scripts/tasks/src/perf-task/index.ts new file mode 100644 index 00000000000000..a10e805c1196eb --- /dev/null +++ b/scripts/tasks/src/perf-task/index.ts @@ -0,0 +1,11 @@ +import { DEPLOYHOST, DEPLOYURL, EnvVariablesByProject, SYSTEM_PULLREQUEST_TARGETBRANCH } from './env'; +export { getPerfRegressions } from './perf-test'; +export { + RenderTypes as AllRenderTypes, + RenderTypesDefault as DefaultRenderTypes, + ScenarioIterations, + ScenarioRenderTypes, + ScenarioNames, + PerfRegressionConfig, +} from './settings'; +export const perfTestEnv = { EnvVariablesByProject, DEPLOYHOST, DEPLOYURL, SYSTEM_PULLREQUEST_TARGETBRANCH }; diff --git a/apps/perf-test-react-components/tasks/perf-test.ts b/scripts/tasks/src/perf-task/perf-test.ts similarity index 75% rename from apps/perf-test-react-components/tasks/perf-test.ts rename to scripts/tasks/src/perf-task/perf-test.ts index f54e00573fa998..6fa725fbbcd484 100644 --- a/apps/perf-test-react-components/tasks/perf-test.ts +++ b/scripts/tasks/src/perf-task/perf-test.ts @@ -1,19 +1,17 @@ import fs from 'fs'; import path from 'path'; -import flamegrill, { CookResults, Scenarios, ScenarioConfig, CookResult } from 'flamegrill'; -import { scenarioIterations } from '../src/scenarioIterations'; -import { scenarioRenderTypes, DefaultRenderTypes } from '../src/scenarioRenderTypes'; -import { argv } from '@fluentui/scripts-tasks'; -type ScenarioSetting = Record; +import { workspaceRoot } from '@nrwl/devkit'; +import flamegrill, { CookResult, CookResults, ScenarioConfig, Scenarios } from 'flamegrill'; -// TODO: consolidate with newer version of fluent perf-test +import { getJustArgv as argv } from '../argv'; -// A high number of iterations are needed to get visualization of lower level calls that are infrequently hit by ticks. -// Wiki: https://github.com/microsoft/fluentui/wiki/Perf-Testing -const iterationsDefault = 5000; +import { DEPLOYHOST, DEPLOYURL, EnvVariablesByProject, SYSTEM_PULLREQUEST_TARGETBRANCH } from './env'; +import { IterationsDefault, PerfRegressionConfig, RenderTypesDefault } from './settings'; + +type ScenarioSetting = Record; +// TODO: consolidate with newer version of fluent perf-test -/* eslint-disable @fluentui/max-len */ // TODO: // - Results Analysis // - If System/Framework is cutting out over half of overall time.. what is consuming the rest? How can that be identified for users? @@ -102,28 +100,33 @@ const iterationsDefault = 5000; // await page.goto(testUrl); // await page.tracing.stop(); -// Hardcoded PR deploy URL for local testing -const DEPLOY_URL = 'fluentuipr.z22.web.core.windows.net'; +export async function getPerfRegressions(options: PerfRegressionConfig) { + validatePerfOptions(options); -const urlForDeployPath = process.env.DEPLOYURL - ? `${process.env.DEPLOYURL}/perf-test-react-components` - : 'file://' + path.resolve(__dirname, '../dist/'); + const { + scenarioIterations, + scenarioRenderTypes, + outDir, + tempDir, + projectName, + projectRootPath, + scenariosSrcDirPath, + excludeScenarios, + } = options; + const projectRootDirectoryName = path.basename(projectRootPath); -// Temporarily comment out deploy site usage to speed up CI build time and support parallelization. -// At some point perf test should be broken out from CI default pipeline entirely and then can go back to using deploy site. -// For now, use local perf-test bundle so that perf-test job can run ASAP instead of waiting for the perf-test bundle to be deployed. -// const urlForDeploy = urlForDeployPath + '/index.html'; -const urlForDeploy = 'file://' + path.resolve(__dirname, '../dist/') + '/index.html'; + const projectEnvVars = EnvVariablesByProject[projectName]; + const urlForDeployPath = DEPLOYURL ? `${DEPLOYURL}/${projectRootDirectoryName}` : 'file://' + outDir; -const targetPath = `heads/${process.env.SYSTEM_PULLREQUEST_TARGETBRANCH || 'master'}`; -const urlForMaster = `https://${ - process.env.DEPLOYHOST || DEPLOY_URL -}/${targetPath}/perf-test-react-components/index.html`; + // Temporarily comment out deploy site usage to speed up CI build time and support parallelization. + // At some point perf test should be broken out from CI default pipeline entirely and then can go back to using deploy site. + // For now, use local perf-test bundle so that perf-test job can run ASAP instead of waiting for the perf-test bundle to be deployed. + // const urlForDeploy = urlForDeployPath + '/index.html'; + const urlForDeploy = 'file://' + outDir + '/index.html'; -const outDir = path.join(__dirname, '../dist'); -const tempDir = path.join(__dirname, '../logfiles'); + const targetPath = `heads/${SYSTEM_PULLREQUEST_TARGETBRANCH || 'master'}`; + const urlForMaster = `https://${DEPLOYHOST}/${targetPath}/${projectRootDirectoryName}/index.html`; -export async function getPerfRegressions() { // For debugging, in case the environment variables used to generate these have unexpected values console.log(`urlForDeployPath: "${urlForDeployPath}"`); console.log(`urlForMaster: "${urlForMaster}"`); @@ -132,9 +135,21 @@ export async function getPerfRegressions() { const iterationsArg = Number.isInteger(iterationsArgv) && iterationsArgv; const scenariosAvailable = fs - .readdirSync(path.join(__dirname, '../src/scenarios')) - .filter(name => name.indexOf('scenarioList') < 0) - .map(name => path.basename(name, '.tsx')); + .readdirSync(scenariosSrcDirPath) + .filter(fileName => { + if (excludeScenarios) { + const shouldExclude = excludeScenarios.some(scenarioName => { + return fileName.indexOf(scenarioName) !== -1; + }); + + if (shouldExclude) { + return false; + } + } + + return true; + }) + .map(fileName => path.basename(fileName, '.tsx')); const scenariosArgv: string = argv().scenarios; const scenariosArg = scenariosArgv?.split?.(',') || []; @@ -152,10 +167,10 @@ export async function getPerfRegressions() { if (!scenariosAvailable.includes(scenarioName)) { throw new Error(`Invalid scenario: ${scenarioName}.`); } - const iterations = - iterationsArg || scenarioIterations[scenarioName as keyof typeof scenarioIterations] || iterationsDefault; - const renderTypes: string[] = - scenarioRenderTypes[scenarioName as keyof typeof scenarioRenderTypes] || DefaultRenderTypes; + const iterations: number = + iterationsArg || (scenarioIterations && scenarioIterations[scenarioName]) || IterationsDefault; + + const renderTypes: string[] = (scenarioRenderTypes && scenarioRenderTypes[scenarioName]) || RenderTypesDefault; renderTypes.forEach(renderType => { const scenarioKey = `${scenarioName}-${renderType}`; @@ -193,6 +208,7 @@ export async function getPerfRegressions() { const scenarioConfig: ScenarioConfig = { outDir, tempDir, + // eslint-disable-next-line @typescript-eslint/no-shadow pageActions: async (page, options) => { // Occasionally during our CI, page takes unexpected amount of time to navigate (unsure about the root cause). // Removing the timeout to avoid perf-test failures but be cautious about long test runs. @@ -205,7 +221,7 @@ export async function getPerfRegressions() { const scenarioResults: CookResults = await flamegrill.cook(scenarios, scenarioConfig); - const comment = createReport(scenarioSettings, scenarioResults); + const comment = createReport(scenarioSettings, scenarioResults, { projectName, urlForDeployPath }); // TODO: determine status according to perf numbers const status = 'success'; @@ -216,25 +232,27 @@ export async function getPerfRegressions() { // Write results to file fs.writeFileSync(path.join(outDir, 'perfCounts.html'), comment); - console.log( - `##vso[task.setvariable variable=PerfCommentFilePathReactComponents;]apps/perf-test-react-components/dist/perfCounts.html`, - ); - console.log(`##vso[task.setvariable variable=PerfCommentStatusReactComponents;]${status}`); + console.log(`##vso[task.setvariable variable=${projectEnvVars.filePath};]${projectRootPath}/dist/perfCounts.html`); + console.log(`##vso[task.setvariable variable=${projectEnvVars.status};]${status}`); } +interface ReportOptions { + projectName: string; + urlForDeployPath: string; +} /** * Create test summary based on test results. */ -function createReport(scenarioSettings: ScenarioSetting, testResults: CookResults) { +function createReport(scenarioSettings: ScenarioSetting, testResults: CookResults, options: ReportOptions) { const report = - '## [Perf Analysis (`@fluentui/react-components`)](https://github.com/microsoft/fluentui/wiki/Perf-Testing)\n' + `## [Perf Analysis (\`${options.projectName}\`)](https://github.com/microsoft/fluentui/wiki/Perf-Testing)\n` // Show only significant changes by default. - .concat(createScenarioTable(scenarioSettings, testResults, false)) + .concat(createScenarioTable(scenarioSettings, testResults, false, options)) // Show all results in a collapsible table. .concat('
All results

') - .concat(createScenarioTable(scenarioSettings, testResults, true)) + .concat(createScenarioTable(scenarioSettings, testResults, true, options)) .concat('

\n\n'); return report; @@ -244,7 +262,12 @@ function createReport(scenarioSettings: ScenarioSetting, testResults: CookResult * Create a table of scenario results. * @param showAll Show only significant results by default. */ -function createScenarioTable(scenarioSettings: ScenarioSetting, testResults: CookResults, showAll: boolean) { +function createScenarioTable( + scenarioSettings: ScenarioSetting, + testResults: CookResults, + showAll: boolean, + options: ReportOptions, +) { const resultsToDisplay = Object.keys(testResults).filter( key => showAll || testResults[key].analysis?.regression?.isRegression, ); @@ -275,10 +298,10 @@ function createScenarioTable(scenarioSettings: ScenarioSetting, testResults: Coo return ` ${scenarioName} ${renderType} - ${getCell(testResult, true)} - ${getCell(testResult, false)} + ${getCell(testResult, true, options)} + ${getCell(testResult, false, options)} ${iterations} - ${getRegression(testResult)} + ${getRegression(testResult, options)} `; }) .join('\n') @@ -293,7 +316,8 @@ function createScenarioTable(scenarioSettings: ScenarioSetting, testResults: Coo /** * Helper that renders an output cell based on a test result. */ -function getCell(testResult: CookResult, getBaseline: boolean) { +function getCell(testResult: CookResult, getBaseline: boolean, options: ReportOptions) { + const { urlForDeployPath } = options; let flamegraphFile = testResult.processed.output && testResult.processed.output.flamegraphFile; let errorFile = testResult.processed.error && testResult.processed.error.errorFile; let numTicks = testResult.analysis && testResult.analysis.numTicks; @@ -317,7 +341,8 @@ function getCell(testResult: CookResult, getBaseline: boolean) { /** * Helper that renders an output cell based on a test result. */ -function getRegression(testResult: CookResult) { +function getRegression(testResult: CookResult, options: ReportOptions) { + const { urlForDeployPath } = options; const cell = testResult.analysis && testResult.analysis.regression && testResult.analysis.regression.isRegression ? testResult.analysis.regression.regressionFile @@ -329,3 +354,15 @@ function getRegression(testResult: CookResult) { return `${cell}`; } + +function validatePerfOptions(options: PerfRegressionConfig) { + if (!fs.existsSync(path.join(workspaceRoot, options.projectRootPath))) { + throw new Error(`Invalid ProjectRootPath. ${options.projectRootPath} doesn't exists`); + } + + if (!fs.existsSync(options.outDir)) { + throw new Error( + `${options.outDir} doesn't exist. Make sure to run bundling process to be able to generate perf suite.`, + ); + } +} diff --git a/scripts/tasks/src/perf-task/settings.ts b/scripts/tasks/src/perf-task/settings.ts new file mode 100644 index 00000000000000..92ca4b26dd01dc --- /dev/null +++ b/scripts/tasks/src/perf-task/settings.ts @@ -0,0 +1,44 @@ +/** + * You don't have to add scenarios to this structure unless + * you want their render types to differ from the default (mount only). + * + * Note: + * You should not need to have virtual-rerender tests in most cases because mount test provides enough coverage. + * It is mostly usefual for cases where component has memoization logics. And in case of re-rendering, + * memoization logic help avoid certain code paths. + */ + +// A high number of iterations are needed to get visualization of lower level calls that are infrequently hit by ticks. +// Wiki: https://github.com/microsoft/fluentui/wiki/Perf-Testing +export const IterationsDefault = 5000; + +export const RenderTypes = ['mount', 'virtual-rerender', 'virtual-rerender-with-unmount']; +export const RenderTypesDefault = ['mount']; + +export type ScenarioNames = { [scenarioName: string]: string }; + +export type ScenarioRenderTypes = { [scenarioName: string]: string[] }; + +export type ScenarioIterations = { [scenarioName: string]: number }; + +export type PerfRegressionConfig = { + /** + * path from workspace root -> example `apps/my-app` + */ + projectRootPath: string; + /** + * name used within package.json#name + */ + projectName: string; + outDir: string; + tempDir: string; + scenariosSrcDirPath: string; + scenarioNames?: ScenarioNames; + scenarioIterations?: ScenarioIterations; + scenarioRenderTypes?: ScenarioRenderTypes; + /** + * array of scenario names to be excluded. + * NOTE: array item needs to match scenario filename without extension. So to exclude `Foo.tsx` , you need to define `['Foo']` etc. + */ + excludeScenarios?: string[]; +}; diff --git a/workspace.json b/workspace.json index 58b32ef0140e32..fc7796bf79378e 100644 --- a/workspace.json +++ b/workspace.json @@ -1020,6 +1020,12 @@ "projectType": "library", "tags": ["tools"] }, + "@fluentui/scripts-perf-test-flamegrill": { + "root": "scripts/perf-test-flamegrill", + "sourceRoot": "scripts/perf-test-flamegrill/src", + "projectType": "library", + "tags": ["tools", "platform:any"] + }, "@fluentui/set-version": { "root": "packages/set-version", "projectType": "library",