|
|
@@ -0,0 +1,205 @@ |
|
|
/** |
|
|
* @author ebidel@ (Eric Bidelman) |
|
|
* License Apache-2.0 |
|
|
* |
|
|
* Shows how to use Puppeeteer's code coverage API to measure CSS/JS coverage across |
|
|
* different points of time during loading. Great for determining if a lazy loading strategy |
|
|
* is paying off or working correctly. |
|
|
* |
|
|
* Install: |
|
|
* npm i puppeteer chalk cli-table |
|
|
* Run: |
|
|
* URL=https://example.com node coverage.js |
|
|
*/ |
|
|
|
|
|
const puppeteer = require('puppeteer'); |
|
|
const chalk = require('chalk'); |
|
|
const Table = require('cli-table'); |
|
|
|
|
|
const URL = process.env.URL || 'https://www.chromestatus.com/features'; |
|
|
|
|
|
const EVENTS = [ |
|
|
'domcontentloaded', |
|
|
'load', |
|
|
'networkidle0', |
|
|
]; |
|
|
|
|
|
function formatBytesToKB(bytes) { |
|
|
if (bytes > 1024) { |
|
|
const formattedNum = new Intl.NumberFormat('en-US', {maximumFractionDigits: 1}).format(bytes / 1024); |
|
|
return `${formattedNum}KB`; |
|
|
} |
|
|
return `${bytes} bytes`; |
|
|
} |
|
|
|
|
|
class UsageFormatter { |
|
|
constructor(stats) { |
|
|
this.stats = stats; |
|
|
} |
|
|
|
|
|
static eventLabel(event) { |
|
|
return chalk.magenta(event); |
|
|
} |
|
|
|
|
|
summary(used = this.stats.usedBytes, total = this.stats.totalBytes) { |
|
|
const percent = Math.round((used / total) * 100); |
|
|
return `${formatBytesToKB(used)}/${formatBytesToKB(total)} (${percent}%)`; |
|
|
} |
|
|
|
|
|
shortSummary(used, total = this.stats.totalBytes) { |
|
|
const percent = Math.round((used / total) * 100); |
|
|
return used ? `${formatBytesToKB(used)} (${percent}%)` : 0; |
|
|
} |
|
|
|
|
|
/** |
|
|
* Constructors a bar chart for the % usage of each value. |
|
|
* @param {!{jsUsed: number, cssUsed: number, totalBytes: number}=} stats Usage stats. |
|
|
* @return {string} |
|
|
*/ |
|
|
barGraph(stats = this.stats) { |
|
|
const maxBarWidth = 30; |
|
|
|
|
|
const jsSegment = ' '.repeat((stats.jsUsed / stats.totalBytes) * maxBarWidth); |
|
|
const cssSegment = ' '.repeat((stats.cssUsed / stats.totalBytes) * maxBarWidth); |
|
|
const unusedSegment = ' '.repeat(maxBarWidth - jsSegment.length - cssSegment.length); |
|
|
|
|
|
return chalk.bgRedBright(jsSegment) + chalk.bgBlueBright(cssSegment) + |
|
|
chalk.bgBlackBright(unusedSegment); |
|
|
} |
|
|
} |
|
|
|
|
|
const stats = new Map(); |
|
|
|
|
|
/** |
|
|
* @param {!Object} coverage |
|
|
* @param {string} type Either "css" or "js" to indicate which type of coverage. |
|
|
* @param {string} eventType The page event when the coverage was captured. |
|
|
*/ |
|
|
function addUsage(coverage, type, eventType) { |
|
|
for (const entry of coverage) { |
|
|
if (!stats.has(entry.url)) { |
|
|
stats.set(entry.url, []); |
|
|
} |
|
|
|
|
|
const urlStats = stats.get(entry.url); |
|
|
|
|
|
let eventStats = urlStats.find(item => item.eventType === eventType); |
|
|
if (!eventStats) { |
|
|
eventStats = { |
|
|
cssUsed: 0, |
|
|
jsUsed: 0, |
|
|
get usedBytes() { return this.cssUsed + this.jsUsed; }, |
|
|
totalBytes: 0, |
|
|
get percentUsed() { |
|
|
return this.totalBytes ? Math.round(this.usedBytes / this.totalBytes * 100) : 0; |
|
|
}, |
|
|
eventType, |
|
|
url: entry.url, |
|
|
}; |
|
|
urlStats.push(eventStats); |
|
|
} |
|
|
|
|
|
eventStats.totalBytes += entry.text.length; |
|
|
|
|
|
for (const range of entry.ranges) { |
|
|
eventStats[`${type}Used`] += range.end - range.start - 1; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
async function collectCoverage() { |
|
|
const browser = await puppeteer.launch({headless: true}); |
|
|
|
|
|
// Do separate load for each event. See |
|
|
// https://github.com/GoogleChrome/puppeteer/issues/1887 |
|
|
const collectPromises = EVENTS.map(async event => { |
|
|
console.log(`Collecting coverage @ ${UsageFormatter.eventLabel(event)}...`); |
|
|
|
|
|
const page = await browser.newPage(); |
|
|
|
|
|
await Promise.all([ |
|
|
page.coverage.startJSCoverage(), |
|
|
page.coverage.startCSSCoverage() |
|
|
]); |
|
|
|
|
|
await page.goto(URL, {waitUntil: event}); |
|
|
|
|
|
const [jsCoverage, cssCoverage] = await Promise.all([ |
|
|
page.coverage.stopJSCoverage(), |
|
|
page.coverage.stopCSSCoverage() |
|
|
]); |
|
|
|
|
|
addUsage(cssCoverage, 'css', event); |
|
|
addUsage(jsCoverage, 'js', event); |
|
|
|
|
|
await page.close(); |
|
|
}); |
|
|
|
|
|
await Promise.all(collectPromises); |
|
|
|
|
|
return browser.close(); |
|
|
} |
|
|
|
|
|
(async() => { |
|
|
|
|
|
await collectCoverage(); |
|
|
|
|
|
for (const [url, vals] of stats) { |
|
|
console.log('\n' + chalk.cyan(url)); |
|
|
|
|
|
const table = new Table({ |
|
|
head: [ |
|
|
'Event', |
|
|
`${chalk.bgRedBright(' JS ')} ${chalk.bgBlueBright(' CSS ')} % used`, |
|
|
'JS used', |
|
|
'CSS used', |
|
|
'Total bytes used' |
|
|
], |
|
|
style: {head: ['white'], border: ['grey']} |
|
|
}); |
|
|
|
|
|
EVENTS.forEach(event => { |
|
|
const usageForEvent = vals.filter(val => val.eventType === event); |
|
|
|
|
|
if (usageForEvent.length) { |
|
|
for (const stats of usageForEvent) { |
|
|
const formatter = new UsageFormatter(stats); |
|
|
table.push([ |
|
|
UsageFormatter.eventLabel(stats.eventType), |
|
|
formatter.barGraph(), |
|
|
formatter.shortSummary(stats.jsUsed), // !== 0 ? `${formatBytesToKB(stats.jsUsed)}KB` : 0, |
|
|
formatter.shortSummary(stats.cssUsed), |
|
|
formatter.summary() |
|
|
]); |
|
|
} |
|
|
} else { |
|
|
table.push([UsageFormatter.eventLabel(event), 'no usage found', '-', '-', '-']); |
|
|
} |
|
|
}); |
|
|
|
|
|
console.log(table.toString()); |
|
|
} |
|
|
|
|
|
// Print total usage for each event. |
|
|
// console.log('\n'); |
|
|
EVENTS.forEach(event => { |
|
|
let totalBytes = 0; |
|
|
let totalUsedBytes = 0; |
|
|
|
|
|
const metrics = Array.from(stats.values()); |
|
|
const statsForEvent = metrics.map(eventStatsForUrl => { |
|
|
const statsForEvent = eventStatsForUrl.filter(stat => stat.eventType === event)[0]; |
|
|
// TODO: need to sum max totalBytes. Currently ignores stats if event didn't |
|
|
// have an entry. IOW, all total numerators should be max totalBytes seen for that event. |
|
|
if (statsForEvent) { |
|
|
totalBytes += statsForEvent.totalBytes; |
|
|
totalUsedBytes += statsForEvent.usedBytes; |
|
|
} |
|
|
}); |
|
|
|
|
|
const percentUsed = Math.round(totalUsedBytes / totalBytes * 100); |
|
|
|
|
|
console.log(`Total used @ ${chalk.magenta(event)}: ${formatBytesToKB(totalUsedBytes)}/${formatBytesToKB(totalBytes)} (${percentUsed}%)`); |
|
|
}); |
|
|
|
|
|
})(); |