UNPKG

playwright-performance-reporter

Version:

Measure and publish performance metrics from browser dev-tools when running playwright

246 lines (245 loc) 8.08 kB
import CDP from 'chrome-remote-interface'; import { Lock, Logger, } from '../../helpers/index.js'; import { AllPerformanceMetrics, HeapDump, TotalJsHeapSize, UsedJsHeapSize, } from './observers/index.js'; export class ChromiumDevelopmentTools { options; /** * Chrome dev tools protocol client */ clients = {}; /** * Chrome dev tools target for metadata extraction */ targets = {}; /** * Lock to indicate if connection request is going on */ connectLock = new Lock(); /** * Indicates if all options are correctly set for connection */ areClientOptionsValid = false; /** * @inheritdoc */ constructor(options) { this.options = options; } /** * @inheritdoc */ async connect() { let unlockCallback; while (!unlockCallback) { unlockCallback = this.connectLock.lock(); } try { const customOptions = this.buildOptions(); const targetList = await CDP.List(customOptions); await Promise.allSettled(targetList.map(async (target) => this.connectToTarget(target))); } catch { } unlockCallback(); } /** * @inheritdoc */ async getMetric(metric, hookOrder) { return new Promise(async (resolve) => { let newConnectionRequests; if (this.connectLock.isLocked()) { await this.connectLock.notifyOnUnlock(); } else { newConnectionRequests = this.connect(); } const currentAvailableTargets = Object.keys(this.clients); const targetMetric = {}; const metricRequests = []; // Get metrics from available targets metricRequests.push(...currentAvailableTargets.map(async (targetId) => this.runPredefinedMetricFetch(targetId, targetMetric, metric, hookOrder))); // Check if new targets were yielded from connection await newConnectionRequests; const newTargets = Object.keys(this.clients).filter(targetId => !currentAvailableTargets.includes(targetId)); if (newTargets.length > 0) { metricRequests.push(...newTargets.map(async (targetId) => this.runPredefinedMetricFetch(targetId, targetMetric, metric, hookOrder))); } // Wait for metric request to be done and fill targets with metadata await Promise.allSettled(metricRequests); for (const targetId of Object.keys(targetMetric)) { Object.assign(targetMetric[targetId], { ...this.targets[targetId] }); } resolve(Object.values(targetMetric)); }); } /** * @inheritdoc */ async runCustomObserver(customMetric, hookOrder) { return new Promise(async (resolve) => { const targetMetric = []; await this.connect(); await Promise.allSettled(Object.keys(this.clients).map(async (targetId) => { const newTargetMetric = { ...this.targets[targetId], metric: {}, }; const client = this.clients[targetId]; if (!client) { return; } await this.runPlugins(targetId, customMetric); await customMetric[hookOrder](newTargetMetric.metric, client); targetMetric.push(newTargetMetric); })); resolve(targetMetric); }); } /** * Cleans up client connection. * If `targetId` is not provided, then all clients are destroyed. * * @param {string=} targetId id to indicate which client to destroy */ async destroy(targetId) { const listOfClients = []; if (targetId) { listOfClients.push(targetId); } else { listOfClients.push(...Object.keys(this.clients)); } for (const id of listOfClients) { const client = this.clients[id]; if (!client) { continue; } try { client.send('IO.close', () => { }); } catch { } // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete this.clients[id]; // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete this.targets[id]; } } /** * @inheritdoc */ getBrowserName() { return 'chromium'; } /** * Fetch metric for a target * * @param targetId target to fill * @param targetMetric mapping of all targets * @param metric type of metric * @param hookOrder hook order */ async runPredefinedMetricFetch(targetId, targetMetric, metric, hookOrder) { const newTargetMetric = { metric: {}, }; const mapping = this.mapMetric(metric); const client = this.clients[targetId]; if (!mapping || !client) { return; } await this.runPlugins(targetId, mapping); await mapping[hookOrder](newTargetMetric.metric, client); targetMetric[targetId] = newTargetMetric; } /** * Builds options object to use for CDP * * @param {string=} targetId id to specify target to run CDP on */ buildOptions(targetId) { const options = { host: 'localhost', port: 9222, }; let foundPort = false; for (const argument of (this.options.launchOptions?.args ?? [])) { if (argument.includes('--remote-debugging-port')) { const port = argument.split('=')[1].trim(); options.port = Number(port); foundPort = true; } } if (!this.areClientOptionsValid && foundPort) { Logger.info('Port for Chromium found', options.port); this.areClientOptionsValid = true; } else if (!this.areClientOptionsValid && !foundPort) { Logger.error('Port for Chromium not found. Metrics fetch will not work!'); } if (targetId) { options.target = targetId; } return options; } /** * Check client to target and create new connection if not available * * @param target reference to run CDP commands on */ async connectToTarget(target) { if (this.clients[target.id]) { this.targets[target.id] = target; return; } try { const customOptions = this.buildOptions(target.id); this.clients[target.id] = await CDP(customOptions); this.targets[target.id] = target; this.clients[target.id].on('disconnect', async () => { await this.destroy(target.id); }); } catch { } } /** * Runs every plugin for a metric. * * @param targetId target to fill * @param metric */ async runPlugins(targetId, metric) { const client = this.clients[targetId]; if (!client) { return; } for (const plugin of metric.plugins) { try { // Plugins can depend on the execution order // eslint-disable-next-line no-await-in-loop await plugin(client); } catch { } } } /** * Provide observer to collect metric * * @param metric observer to create */ mapMetric(metric) { switch (metric) { case 'usedJsHeapSize': { return new UsedJsHeapSize(); } case 'totalJsHeapSize': { return new TotalJsHeapSize(); } case 'allPerformanceMetrics': { return new AllPerformanceMetrics(); } case 'heapDump': { return new HeapDump(); } } } }