playwright-performance-reporter
Version:
Measure and publish performance metrics from browser dev-tools when running playwright
150 lines (149 loc) • 4.41 kB
JavaScript
import path from 'node:path';
import fs from 'node:fs';
import { Logger, } from '../../helpers/index.js';
/**
* Base presenter that collects timeline data points and writes to file
*/
export class TimelineDataPresenter {
/**
* Status whether writer is usable
*/
isClosed = false;
/**
* Timeline data collected from writes
*/
timelineData = [];
/**
* Locator where to store the output
*/
filePath;
/**
* File writer
*/
fileStream;
/**
* Maps unique test ids to the computed name
*/
testIdToParentNameMap = new Map();
/**
* Initialize writer
*
* @param options defines target output
*/
constructor(options) {
this.filePath = path.join(options.outputDir, options.outputFile);
this.fileStream = fs.createWriteStream(this.filePath, { flags: 'w' });
}
/**
* Finish data collection and generate output
*/
async close() {
this.isClosed = true;
if (!this.fileStream || !this.filePath) {
return false;
}
try {
const content = this.generate();
this.fileStream.write(content);
this.fileStream.end();
return await new Promise(resolve => {
this.fileStream?.once('finish', () => {
resolve(true);
});
});
}
catch (error) {
Logger.error(String(error));
return false;
}
}
/**
* Delete created file
*/
async delete() {
const closeResult = await this.close();
if (!closeResult) {
return false;
}
if (this.filePath) {
fs.rmSync(this.filePath, { maxRetries: 5, retryDelay: 500 });
return true;
}
return false;
}
/**
* Create new entry by filtering numeric data for the timeline.
*
* @param content The result accumulator containing test performance data
*/
async write(content) {
if (this.isClosed) {
return false;
}
try {
for (const [caseId, steps] of Object.entries(content)) {
for (const [stepId, testPerformance] of Object.entries(steps)) {
if (stepId === 'TEST_CASE_PARENT') {
this.testIdToParentNameMap.set(caseId, testPerformance.name);
}
const datapoint = this.extractDatapoint(caseId, testPerformance);
if (datapoint) {
this.timelineData.push(datapoint);
}
}
}
return true;
}
catch (error) {
Logger.error(`Failed to parse timeline data: ${String(error)}`);
return false;
}
}
/**
* Extract a TimelineDataPoint from TestPerformance data
*
* @param caseId The case identifier
* @param testPerformance The test performance data
* @returns TimelineDataPoint or undefined if no numeric data found
*/
extractDatapoint(caseId, testPerformance) {
const labels = [];
const values = [];
const metricSources = [
testPerformance.startMetrics,
testPerformance.stopMetrics,
testPerformance.samplingMetrics,
];
for (const metrics of metricSources) {
if (!Array.isArray(metrics)) {
continue;
}
for (const item of metrics) {
if (!item?.metric) {
continue;
}
for (const [metricName, metricValue] of Object.entries(item.metric)) {
if (typeof metricValue === 'number' && !Number.isNaN(metricValue)) {
labels.push(metricName);
values.push(metricValue);
}
}
}
}
if (labels.length === 0) {
return undefined;
}
return {
labels,
name: this.testIdToParentNameMap.get(caseId) ?? caseId,
timestamp: testPerformance.endMeasurement,
values,
};
}
/**
* Generate output from collected timeline data
*/
generate() {
return JSON.stringify(this.timelineData, null, 2);
}
}