react-automation-profiler
Version:
Automated React profiling and data visualization using React's Profiler API, Puppeteer, and D3.
296 lines • 12.3 kB
JavaScript
import express from 'express';
import fs from 'fs/promises';
import { minify } from 'html-minifier-terser';
import jsdom from 'jsdom';
import yaml from 'js-yaml';
import puppeteer from 'puppeteer';
import { getFileName, MessageTypes, printMessage } from './util.js';
export var OutputType;
(function (OutputType) {
OutputType["CHART"] = "chart";
OutputType["JSON"] = "json";
})(OutputType || (OutputType = {}));
var Actions;
(function (Actions) {
Actions["CLICK"] = "click";
Actions["FOCUS"] = "focus";
Actions["HOVER"] = "hover";
Actions["GOTO"] = "goto";
Actions["WAIT"] = "wait";
})(Actions || (Actions = {}));
const { ERROR, NOTICE } = MessageTypes;
export default async function automate({ automationCount, averageOf, cwd, includeMount, isServerReady, packagePath, serverPort, url, headless, output, }) {
const MOUNT = 'Mount';
const browser = await puppeteer.launch({ headless });
const page = await browser.newPage();
let errorMessage = '';
async function exportResults() {
switch (output) {
case OutputType.CHART: {
await appendJsonToHTML();
return null;
}
case OutputType.JSON: {
return await exportJsonBundle();
}
}
}
async function getRawResults() {
let files = await fs.readdir(packagePath);
files = files.filter((file) => file.includes('.json'));
let results = {};
for (const file of files) {
const jsonContents = await fs.readFile(`${packagePath}/${file}`, 'utf8');
results[file] = jsonContents;
}
return results;
}
async function appendJsonToHTML() {
var _a;
const { JSDOM } = jsdom;
try {
const contents = await fs.readFile(`${packagePath}/index.html`, 'utf8');
const { document } = new JSDOM(`${contents}`).window;
(_a = document.querySelectorAll('.json')) === null || _a === void 0 ? void 0 : _a.forEach((item) => item.remove());
const rawResults = await getRawResults();
Object.keys(rawResults).forEach((fileName) => {
const jsonScript = document.createElement('script');
const idArr = fileName.split('-');
jsonScript.id =
averageOf > 1 ? `${idArr[1]}-${idArr[2]}` : `${idArr[0]}-${idArr[1]}`;
jsonScript.classList.add('json');
jsonScript.type = 'application/json';
jsonScript.innerHTML = rawResults[fileName];
document.body.appendChild(jsonScript);
});
await fs.writeFile(`${packagePath}/index.html`, document.documentElement.outerHTML);
await generateExport(document);
}
catch (e) {
errorMessage = 'Could not append JSON data to HTML file.';
printMessage(ERROR, { e: e, log: errorMessage });
}
}
async function exportJsonBundle() {
const rawResults = await getRawResults();
const result = {};
Object.keys(rawResults).forEach((fileName) => {
const parsedResult = JSON.parse(rawResults[fileName]);
result[fileName] = parsedResult;
});
const pathName = `${process.cwd()}/${getFileName('react_profile')}`;
await fs.writeFile(pathName, JSON.stringify(result));
printMessage(NOTICE, { log: `Results saved to ${pathName}` });
return result;
}
async function calculateAverage() {
try {
const files = await fs.readdir(packagePath);
const flows = new Set(files
.filter((file) => file.includes('.json'))
.map((file) => file.split('-')[0])
.filter((name) => name !== 'average'));
for (const [i, flow] of [...flows].entries()) {
const flowFiles = files.filter((file) => !file.includes('average-') && file.includes(`${flow}-`));
const sumLogs = [];
let sumNumberOfInteractions = 0;
for (const file of flowFiles) {
const contents = JSON.parse(await fs.readFile(`${packagePath}/${file}`, 'utf8'));
const { logs } = contents;
for (const [index, log] of logs.entries()) {
if (typeof sumLogs[index] !== 'object' || sumLogs[index] === null)
sumLogs[index] = {
actualDuration: 0,
baseDuration: 0,
commitTime: 0,
id: '',
interactions: {},
phase: '',
startTime: 0,
};
sumLogs[index].actualDuration += log.actualDuration;
sumLogs[index].baseDuration += log.baseDuration;
sumLogs[index].commitTime += log.commitTime;
sumLogs[index].startTime += log.startTime;
if (!sumLogs[index].id)
sumLogs[index].id = log.id;
if (!sumLogs[index].phase)
sumLogs[index].phase = log.phase;
}
sumNumberOfInteractions += contents.numberOfInteractions;
await fs.unlink(`${packagePath}/${file}`);
}
const averagedData = {
logs: sumLogs.map((log) => ({
actualDuration: log.actualDuration / averageOf,
baseDuration: log.baseDuration / averageOf,
commitTime: log.commitTime / averageOf,
id: log.id,
interactions: log.interactions,
phase: log.phase,
startTime: log.startTime / averageOf,
})),
numberOfInteractions: sumNumberOfInteractions / averageOf,
};
await fs.writeFile(`${packagePath}/average-${flow}${getFileName()}`, JSON.stringify(averagedData));
if (averageOf === automationCount && i === flows.size - 1) {
return await exportResults();
}
}
return null;
}
catch (e) {
errorMessage = 'An error occurred while calculating averages.';
printMessage(ERROR, { e: e, log: errorMessage });
return null;
}
}
async function collectLogs({ label, numberOfInteractions = 0, }) {
if (label !== MOUNT || (label === MOUNT && includeMount)) {
const logs = await page.evaluate(() => window.profiler);
const fileName = getFileName(label);
if (logs.length === 0)
return false;
try {
await fs.writeFile(`${packagePath}/${fileName}`, JSON.stringify({ logs, numberOfInteractions }));
}
catch (e) {
errorMessage =
'An error occurred while collecting automation log data.';
printMessage(ERROR, { e: e, log: errorMessage });
}
}
await page.evaluate(() => {
window.profiler = [];
});
return true;
}
async function generateExport(document) {
var _a;
(_a = document.querySelector('#export')) === null || _a === void 0 ? void 0 : _a.remove();
try {
await fs.writeFile(`${packagePath}/export.html`, minify(document.documentElement.outerHTML, {
collapseBooleanAttributes: true,
collapseWhitespace: true,
minifyCSS: true,
minifyJS: true,
removeAttributeQuotes: true,
}));
}
catch (e) {
errorMessage = 'An error occurred while generating a new export file.';
printMessage(ERROR, { e: e, log: errorMessage });
}
}
async function handleActions(actions) {
for (const action of actions) {
const [actionType, ...selector] = action.split(' ');
if (Object.values(Actions).includes(actionType)) {
const selectorStr = selector.join(' ');
printMessage(NOTICE, { log: `${actionType}: ${selectorStr}` });
switch (actionType) {
case Actions.CLICK:
await page.click(selectorStr);
break;
case Actions.FOCUS:
await page.focus(selectorStr);
break;
case Actions.HOVER:
await page.hover(selectorStr);
break;
case Actions.GOTO:
await page.goto(selectorStr);
break;
case Actions.WAIT:
await page.waitForTimeout(parseInt(selectorStr));
break;
}
}
else {
errorMessage = 'One or more action types provided was not valid.';
throw printMessage(ERROR, { log: errorMessage });
}
}
}
async function readAutomationFile() {
let flows;
try {
flows = (yaml.load(await fs.readFile(`${cwd}/react.automation.yml`, 'utf8')));
return flows;
}
catch (_a) {
flows = (yaml.load(await fs.readFile(`${cwd}/react.automation.yaml`, 'utf8')));
return flows;
}
}
async function runFlows() {
try {
const flows = await readAutomationFile();
if (!flows) {
return;
}
const keys = Object.keys(flows);
let attempts = 0;
for (let i = 0; i < keys.length; i++) {
const actions = flows[keys[i]];
await handleActions(actions);
const success = await collectLogs({
label: keys[i],
numberOfInteractions: actions.length,
});
if (!success) {
if (attempts++ < 3)
i -= 1;
else
printMessage(NOTICE, {
log: `Automation flow "${keys[i]}" did not produce any renders.\n`,
});
}
}
}
catch (e) {
const isErrorObjectEmpty = Object.keys(e).length === 0;
const description = isErrorObjectEmpty &&
` This was likely caused by one of these issues:
- The react.automation YAML file could not be found at the root of your repo.
- The react.automation file is using a selector that does not exist.`;
errorMessage = `An error occurred while trying to run automation flows.${description ? description : ''}`;
printMessage(ERROR, {
e: isErrorObjectEmpty ? undefined : e,
log: errorMessage,
});
}
}
async function startServer() {
const app = express();
app.use(express.static(packagePath));
app.get('/', (_, res) => res.sendFile(`${packagePath}/index.html`));
app.listen(serverPort);
}
await page.goto(url);
await page.setViewport({
deviceScaleFactor: 1,
height: 1080,
width: 1920,
});
await collectLogs({ label: MOUNT });
await runFlows();
await browser.close();
let results = {};
if (averageOf > 1 && automationCount === averageOf) {
results = await calculateAverage();
}
else if (averageOf === 1) {
results = await exportResults();
}
if (!isServerReady &&
automationCount === averageOf &&
output === OutputType.CHART)
await startServer();
if (errorMessage)
throw printMessage(ERROR, {
log: 'Automation could not complete because of the above errors.',
});
return results;
}
//# sourceMappingURL=automation.js.map