This commit is contained in:
Xu Gao 2020-04-24 12:53:55 -07:00
Родитель a94bb7d517
Коммит ac5bae4877
5 изменённых файлов: 102 добавлений и 38 удалений

36
.vscode/launch.json поставляемый Normal file
Просмотреть файл

@ -0,0 +1,36 @@
{
// Use IntelliSense to learn about possible Node.js debug attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug Unit Jest Tests",
"cwd": "${workspaceFolder}",
"args": [
"--inspect-brk",
"${workspaceRoot}/node_modules/.bin/jest",
"--runInBand",
"--watch",
"--config",
"${workspaceRoot}/packages/flamegrill/jest.config.js",
"${fileBasenameNoExtension}"
],
"windows": {
"args": [
"--inspect-brk",
"${workspaceRoot}/node_modules/jest/bin/jest.js",
"--runInBand",
"--watch",
"--config",
"${workspaceRoot}/packages/flamegrill/jest.config.js",
"${fileBasenameNoExtension}"
]
},
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
}
]
}

Просмотреть файл

@ -1,6 +1,6 @@
{ {
"type": "minor", "type": "minor",
"comment": "support executing page operations before taking measures/profiling", "comment": "Support customized page actions and reset default timeout to 30s instead of no timeout",
"packageName": "flamegrill", "packageName": "flamegrill",
"email": "xgao@microsoft.com", "email": "xgao@microsoft.com",
"commit": "355b574566a201ce7c8e31be9c55472651c85310", "commit": "355b574566a201ce7c8e31be9c55472651c85310",

Просмотреть файл

@ -16,13 +16,22 @@ export interface Scenarios {
[scenarioName: string]: Scenario; [scenarioName: string]: Scenario;
}; };
export type ScenarioConfig = { export interface PageActionOptions {
/** URL the page will navigate to. */
url: string;
}
/**
* Async page operations which will be execute before taking metrics.
* This will override default page operations done by flamegrill before page.metrics().
*/
export type PageActions = (page: ProfilePage, options: PageActionOptions) => Promise<void>;
export interface ScenarioConfig {
outDir?: string; outDir?: string;
tempDir?: string; tempDir?: string;
pageActions?: PageActions;
/** Any async operation which will be execute before taking metrics for the profiling page. */ }
executeBeforeMeasurement?(page: ProfilePage): Promise<void>;
};
export interface CookResult { export interface CookResult {
profile: ScenarioProfile; profile: ScenarioProfile;
@ -58,7 +67,7 @@ export async function cook(scenarios: Scenarios, userConfig?: ScenarioConfig): P
const config = { const config = {
outDir: userConfig && userConfig.outDir ? resolveDir(userConfig.outDir) : process.cwd(), outDir: userConfig && userConfig.outDir ? resolveDir(userConfig.outDir) : process.cwd(),
tempDir: userConfig && userConfig.tempDir ? resolveDir(userConfig.tempDir) : process.cwd(), tempDir: userConfig && userConfig.tempDir ? resolveDir(userConfig.tempDir) : process.cwd(),
executeBeforeMeasurement: userConfig && userConfig.executeBeforeMeasurement, pageActions: userConfig && userConfig.pageActions,
}; };
const profiles = await profile(scenarios, config); const profiles = await profile(scenarios, config);

Просмотреть файл

@ -1,21 +1,14 @@
import * as tmp from 'tmp'; import * as tmp from 'tmp';
import { Browser, Page } from 'puppeteer'; import { Browser, Page } from 'puppeteer';
import { __unitTestHooks, ProfilePage } from '../profile'; import { __unitTestHooks, ProfilePage, Profile } from '../profile';
import { PageActions, PageActionOptions } from '../../flamegrill';
describe('profileUrl', () => { describe('profileUrl', () => {
const { profileUrl } = __unitTestHooks; const { profileUrl } = __unitTestHooks;
const testUrl = 'testUrl'; const testUrl = 'testUrl';
const testMetrics = { metric1: 1, metric2: 2 }; const testMetrics = { metric1: 1, metric2: 2 };
const testPage: Page = { let testPage: Page;
close: jest.fn(() => Promise.resolve()),
goto: jest.fn(() => {
return Promise.resolve(null);
}),
metrics: jest.fn(() => Promise.resolve(testMetrics)),
setDefaultTimeout: jest.fn(() => {}),
waitForSelector: jest.fn(() => Promise.resolve()),
} as unknown as Page;
const testBrowser: Browser = { const testBrowser: Browser = {
newPage: jest.fn(() => { newPage: jest.fn(() => {
@ -32,15 +25,47 @@ describe('profileUrl', () => {
beforeAll(() => { beforeAll(() => {
outdir = tmp.dirSync({ unsafeCleanup: true }); outdir = tmp.dirSync({ unsafeCleanup: true });
}); });
afterAll(() => { afterAll(() => {
outdir.removeCallback(); outdir.removeCallback();
}) });
beforeEach(() => {
testPage = {
close: jest.fn(() => Promise.resolve()),
goto: jest.fn(() => {
return Promise.resolve(null);
}),
metrics: jest.fn(() => Promise.resolve(testMetrics)),
setDefaultTimeout: jest.fn(() => {}),
waitForSelector: jest.fn(() => Promise.resolve()),
} as unknown as Page;
});
afterEach(() => {
jest.clearAllMocks();
});
it('performs expected operations', async () => { it('performs expected operations', async () => {
const executeBeforeMeasurement = async (page: ProfilePage) => { await page.waitForSelector(testSelector); } const result = await profileUrl(testBrowser, testUrl, 'testScenario', outdir.name);
const result = await profileUrl(testBrowser, testUrl, 'testScenario', outdir.name, executeBeforeMeasurement); expect((testPage.goto as jest.Mock).mock.calls.length).toEqual(1);
expect((testPage.goto as jest.Mock).mock.calls[0][0]).toEqual(testUrl);
expect((testPage.close as jest.Mock).mock.calls.length).toEqual(1);
expect(logfile).toBeDefined();
expect(result.logFile).toEqual(logfile.name);
expect(result.metrics).toEqual(testMetrics);
});
it('performs expected operations when user defined page actions', async () => {
const pageActions: PageActions = async (page: ProfilePage, options: PageActionOptions) => {
await page.setDefaultTimeout(0);
await page.goto(options.url);
await page.waitForSelector(testSelector);
};
const result = await profileUrl(testBrowser, testUrl, 'testScenario', outdir.name, pageActions);
expect((testPage.setDefaultTimeout as jest.Mock).mock.calls.length).toEqual(1); expect((testPage.setDefaultTimeout as jest.Mock).mock.calls.length).toEqual(1);
expect((testPage.setDefaultTimeout as jest.Mock).mock.calls[0][0]).toEqual(0); expect((testPage.setDefaultTimeout as jest.Mock).mock.calls[0][0]).toEqual(0);

Просмотреть файл

@ -3,7 +3,7 @@ import path from 'path';
import puppeteer from 'puppeteer'; import puppeteer from 'puppeteer';
import { Browser, Metrics } from 'puppeteer'; import { Browser, Metrics } from 'puppeteer';
import { Scenarios, ScenarioConfig } from '../flamegrill'; import { Scenarios, ScenarioConfig, PageActions } from '../flamegrill';
import { arr_diff } from '../util'; import { arr_diff } from '../util';
@ -24,7 +24,7 @@ export type ProfilePage = puppeteer.Page;
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<T>; type Optional<T, K extends keyof T> = Omit<T, K> & Partial<T>;
export type ScenarioProfileConfig = Optional<Required<ScenarioConfig>, 'executeBeforeMeasurement'>; export type ScenarioProfileConfig = Optional<Required<ScenarioConfig>, 'pageActions'>;
// const extraV8Flags = '--log-source-code --log-timer-events'; // const extraV8Flags = '--log-source-code --log-timer-events';
// const extraV8Flags = '--log-source-code'; // const extraV8Flags = '--log-source-code';
@ -37,7 +37,7 @@ const extraV8Flags = '';
* @param scenarios * @param scenarios
*/ */
export async function profile(scenarios: Scenarios, config: ScenarioProfileConfig): Promise<ScenarioProfiles> { export async function profile(scenarios: Scenarios, config: ScenarioProfileConfig): Promise<ScenarioProfiles> {
const { tempDir, executeBeforeMeasurement } = config; const { tempDir, pageActions } = config;
const logFile = path.join(tempDir, '/puppeteer.log'); const logFile = path.join(tempDir, '/puppeteer.log');
console.log(`profile logFile: ${logFile}`); console.log(`profile logFile: ${logFile}`);
@ -65,10 +65,10 @@ export async function profile(scenarios: Scenarios, config: ScenarioProfileConfi
for (const scenarioName of Object.keys(scenarios)) { for (const scenarioName of Object.keys(scenarios)) {
const scenario = scenarios[scenarioName]; const scenario = scenarios[scenarioName];
let profileResults: ScenarioProfile = await profileUrl(browser, scenario.scenario, scenarioName, tempDir, executeBeforeMeasurement); let profileResults: ScenarioProfile = await profileUrl(browser, scenario.scenario, scenarioName, tempDir, pageActions);
if (scenario.baseline) { if (scenario.baseline) {
profileResults.baseline = await profileUrl(browser, scenario.baseline, scenarioName, tempDir, executeBeforeMeasurement); profileResults.baseline = await profileUrl(browser, scenario.baseline, scenarioName, tempDir, pageActions);
} }
profiles[scenarioName] = profileResults; profiles[scenarioName] = profileResults;
@ -87,7 +87,7 @@ export async function profile(scenarios: Scenarios, config: ScenarioProfileConfi
* @param {string} testUrl Base URL supporting 'scenario' and 'iterations' query parameters. * @param {string} testUrl Base URL supporting 'scenario' and 'iterations' query parameters.
* @param {string} profileName Name of scenario that will be used with baseUrl. * @param {string} profileName Name of scenario that will be used with baseUrl.
* @param {string} logDir Absolute path to output log profiles. * @param {string} logDir Absolute path to output log profiles.
* @param onPageLoad Async opertaion that is executed after page is loaded. * @param {PageActions} pageActions Async opertaion that is executed before taking metrics.
* @returns {string} Log file path associated with test. * @returns {string} Log file path associated with test.
*/ */
async function profileUrl( async function profileUrl(
@ -95,17 +95,12 @@ async function profileUrl(
testUrl: string, testUrl: string,
profileName: string, profileName: string,
logDir: string, logDir: string,
onPageLoad?: (page: ProfilePage) => Promise<void> pageActions?: PageActions
): Promise<Profile> { ): Promise<Profile> {
const logFilesBefore = fs.readdirSync(logDir); const logFilesBefore = fs.readdirSync(logDir);
const page = await browser.newPage(); const page = await browser.newPage();
// Default timeout is 30 seconds. This is good for most tests except for problematic components like DocumentCardTitle.
// Disable timeout for now and tweak to a maximum setting once server conditions are better known.
// TODO: argument? should probably default to 30 seconds
page.setDefaultTimeout(0);
const logFilesAfter = fs.readdirSync(logDir); const logFilesAfter = fs.readdirSync(logDir);
const testLogFile = arr_diff(logFilesBefore, logFilesAfter); const testLogFile = arr_diff(logFilesBefore, logFilesAfter);
@ -121,14 +116,13 @@ async function profileUrl(
console.log(`Starting test for ${profileName} at ${testUrl}`); console.log(`Starting test for ${profileName} at ${testUrl}`);
console.time('Ran profile in'); console.time('Ran profile in');
// TODO: consider using or exposing other load finished options:
// https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#pagegotourl-options
await page.goto(testUrl);
if (onPageLoad) { if (pageActions) {
console.log("Started executing user-defined page operations."); console.log("Started executing user-defined page operations.");
await onPageLoad(page); await pageActions(page, { url: testUrl });
console.log("Finished executing user-defined page operations."); console.log("Finished executing user-defined page operations.");
} else {
await page.goto(testUrl);
} }
console.timeEnd('Ran profile in'); console.timeEnd('Ran profile in');