// @ts-check
const fs = require('fs');
const path = require('path');
const flamegrill = require('flamegrill');
const scenarioIterations = require('../src/scenarioIterations');
const { scenarioRenderTypes, DefaultRenderTypes } = require('../src/scenarioRenderTypes');
const { argv } = require('@uifabric/build').just;
import { getFluentPerfRegressions } from './fluentPerfRegressions';
// TODO: consolidate with newer version of fluent perf-test
// Flamegrill Types
/**
* @typedef {{
* scenario: string;
* baseline?: string;
* }} Scenario
*
* @typedef {{
* [scenarioName: string]: Scenario;
* }} Scenarios
*
* @typedef {{
* outDir?: string;
* tempDir?: string;
* }} ScenarioConfig
*
* @typedef {{
* Timestamp: number;
* Documents: number;
* Frames: number;
* JSEventListeners: number;
* Nodes: number;
* LayoutCount: number;
* RecalcStyleCount: number;
* LayoutDuration: number;
* RecalcStyleDuration: number;
* ScriptDuration: number;
* TaskDuration: number;
* JSHeapUsedSize: number;
* JSHeapTotalSize: number;
* }} Metrics
*
* @typedef {{
* logFile: string;
* metrics: Metrics;
* baseline?: {
* logFile: string;
* metrics: Metrics;
* }
* }} ScenarioProfile
*
* @typedef {{
* dataFile: string;
* flamegraphFile: string;
* }} ProcessedOutput
*
* @typedef {{
* errorFile: string;
* }} ProcessedError
*
* @typedef {{
* output?: ProcessedOutput;
* error?: ProcessedError;
* baseline?: {
* output?: ProcessedOutput;
* error?: ProcessedError;
* }
* }} ProcessedScenario
*
* @typedef {{
* numTicks: number;
* }} Analysis
*
* @typedef {{
* summary: string;
* isRegression: boolean;
* regressionFile?: string;
* }} RegressionAnalysis
*
* @typedef {{
* numTicks: number;
* baseline?: Analysis;
* regression?: RegressionAnalysis;
* }} ScenarioAnalysis
*
* @typedef {{
* profile: ScenarioProfile;
* processed: ProcessedScenario;
* analysis?: ScenarioAnalysis;
* }} CookResult
*
* @typedef {{
* [scenarioName: string]: CookResult;
* }} CookResults
*/
// 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;
// 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();
const urlForDeployPath = process.env.BUILD_SOURCEBRANCH
? `http://fabricweb.z5.web.core.windows.net/pr-deploy-site/${process.env.BUILD_SOURCEBRANCH}/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 urlForMaster = process.env.SYSTEM_PULLREQUEST_TARGETBRANCH
? `http://fabricweb.z5.web.core.windows.net/pr-deploy-site/refs/heads/${process.env.SYSTEM_PULLREQUEST_TARGETBRANCH}/perf-test/index.html`
: 'http://fabricweb.z5.web.core.windows.net/pr-deploy-site/refs/heads/master/perf-test/index.html';
const outDir = path.join(__dirname, '../dist');
const tempDir = path.join(__dirname, '../logfiles');
module.exports = async function getPerfRegressions() {
const iterationsArgv = /** @type {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 = /** @type {string} */ argv().scenarios;
const scenariosArg = (scenariosArgv && scenariosArgv.split && scenariosArgv.split(',')) || [];
scenariosArg.forEach(scenario => {
if (!scenariosAvailable.includes(scenario)) {
throw new Error(`Invalid scenario: ${scenario}.`);
}
});
const scenarioList = scenariosArg.length > 0 ? scenariosArg : scenariosAvailable;
/** @type {Scenarios} */
const scenarios = {};
const scenarioSettings = {};
scenarioList.forEach(scenarioName => {
if (!scenariosAvailable.includes(scenarioName)) {
throw new Error(`Invalid scenario: ${scenarioName}.`);
}
const iterations = iterationsArg || scenarioIterations[scenarioName] || iterationsDefault;
const renderTypes = scenarioRenderTypes[scenarioName] || 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);
});
}
}
/** @type {ScenarioConfig} */
const 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');
},
};
/** @type {CookResults} */
const scenarioResults = await flamegrill.cook(scenarios, scenarioConfig);
let comment = createReport(scenarioSettings, scenarioResults);
comment = comment.concat(getFluentPerfRegressions());
// 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.
*
* @param {CookResults} testResults
* @returns {string}
*/
function createReport(scenarioSettings, testResults) {
const report = '## [Perf Analysis](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(' ')
.concat(createScenarioTable(scenarioSettings, testResults, true))
.concat('All results
No significant results to display.
'; } const result = `| Scenario | Render type | Master Ticks | PR Ticks | Iterations | Status |
|---|---|---|---|---|---|
| ${scenarioName} | ${renderType} | ${getCell(testResult, true)} ${getCell(testResult, false)}${iterations} | ${getRegression(testResult)}