Skip to content

Commit

Permalink
feat: added a new CLI arg --merge-async to asynchronously and incre…
Browse files Browse the repository at this point in the history
…mentally merge process coverage files to avoid OOM due to heap exhaustion
  • Loading branch information
bizob2828 committed May 23, 2023
1 parent 5d56252 commit 0978bdf
Show file tree
Hide file tree
Showing 6 changed files with 1,307 additions and 525 deletions.
3 changes: 2 additions & 1 deletion lib/commands/report.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ exports.outputReport = async function (argv) {
allowExternal: argv.allowExternal,
src: argv.src,
skipFull: argv.skipFull,
excludeNodeModules: argv.excludeNodeModules
excludeNodeModules: argv.excludeNodeModules,
mergeAsync: argv.mergeAsync
})
await report.run()
if (argv.checkCoverage) await checkCoverages(argv, report)
Expand Down
6 changes: 6 additions & 0 deletions lib/parse-args.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,12 @@ function buildYargs (withCommands = false) {
describe: 'supplying --allowExternal will cause c8 to allow files from outside of your cwd. This applies both to ' +
'files discovered in coverage temp files and also src files discovered if using the --all flag.'
})
.options('merge-async', {
default: false,
type: 'boolean',
describe: 'supplying --merge-async will merge all v8 coverage reports asynchronously and incrementally. ' +
'This is to avoid OOM issues with Node.js runtime.'
})
.pkgConf('c8')
.demandCommand(1)
.check((argv) => {
Expand Down
136 changes: 101 additions & 35 deletions lib/report.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ try {
} catch (err) {
;({ readFile } = require('fs').promises)
}
const { readdirSync, statSync } = require('fs')
const { readdirSync, readFileSync, statSync } = require('fs')
const { isAbsolute, resolve, extname } = require('path')
const { pathToFileURL, fileURLToPath } = require('url')
const getSourceMapFromFile = require('./source-map-from-file')
Expand Down Expand Up @@ -36,7 +36,8 @@ class Report {
src,
allowExternal = false,
skipFull,
excludeNodeModules
excludeNodeModules,
mergeAsync
}) {
this.reporter = reporter
this.reporterOptions = reporterOptions || {}
Expand All @@ -59,6 +60,7 @@ class Report {
this.all = all
this.src = this._getSrc(src)
this.skipFull = skipFull
this.mergeAsync = mergeAsync
}

_getSrc (src) {
Expand Down Expand Up @@ -96,7 +98,13 @@ class Report {
if (this._allCoverageFiles) return this._allCoverageFiles

const map = libCoverage.createCoverageMap()
const v8ProcessCov = await this._getMergedProcessCov()
let v8ProcessCov

if (this.mergeAsync) {
v8ProcessCov = await this._getMergedProcessCovAsync()
} else {
v8ProcessCov = this._getMergedProcessCov()
}
const resultCountPerPath = new Map()
const possibleCjsEsmBridges = new Map()

Expand Down Expand Up @@ -180,7 +188,30 @@ class Report {
* @return {ProcessCov} Merged V8 process coverage.
* @private
*/
async _getMergedProcessCov () {
_getMergedProcessCov () {
const { mergeProcessCovs } = require('@bcoe/v8-coverage')
const v8ProcessCovs = []
const fileIndex = new Set() // Set<string>
for (const v8ProcessCov of this._loadReports()) {
if (this._isCoverageObject(v8ProcessCov)) {
if (v8ProcessCov['source-map-cache']) {
Object.assign(this.sourceMapCache, this._normalizeSourceMapCache(v8ProcessCov['source-map-cache']))
}
v8ProcessCovs.push(this._normalizeProcessCov(v8ProcessCov, fileIndex))
}
}

if (this.all) {
const emptyReports = this._includeUncoveredFiles(fileIndex)
v8ProcessCovs.unshift({
result: emptyReports
})
}

return mergeProcessCovs(v8ProcessCovs)
}

async _getMergedProcessCovAsync () {
const { mergeProcessCovs } = require('@bcoe/v8-coverage')
const fileIndex = new Set() // Set<string>
let mergedCov = null
Expand Down Expand Up @@ -209,47 +240,60 @@ class Report {
}

if (this.all) {
const emptyReports = []
const emptyReports = this._includeUncoveredFiles(fileIndex)
const emptyReport = {
result: emptyReports
}
const workingDirs = this.src
const { extension } = this.exclude
for (const workingDir of workingDirs) {
this.exclude.globSync(workingDir).forEach((f) => {
const fullPath = resolve(workingDir, f)
if (!fileIndex.has(fullPath)) {
const ext = extname(fullPath)
if (extension.includes(ext)) {
const stat = statSync(fullPath)
const sourceMap = getSourceMapFromFile(fullPath)
if (sourceMap) {
this.sourceMapCache[pathToFileURL(fullPath)] = { data: sourceMap }
}
emptyReports.push({
scriptId: 0,
url: resolve(fullPath),
functions: [{
functionName: '(empty-report)',
ranges: [{
startOffset: 0,
endOffset: stat.size,
count: 0
}],
isBlockCoverage: true
}]
})
}
}
})
}

mergedCov = mergeProcessCovs([emptyReport, mergedCov])
}

return mergedCov
}

/**
* Adds empty coverage reports to account for uncovered/untested code.
* This is only done when the `--all` flag is present.
*
* @param {Set} fileIndex list of files that have coverage
* @returns {Array} list of empty coverage reports
*/
_includeUncoveredFiles(fileIndex) {
const emptyReports = []
const workingDirs = this.src
const { extension } = this.exclude
for (const workingDir of workingDirs) {
this.exclude.globSync(workingDir).forEach((f) => {
const fullPath = resolve(workingDir, f)
if (!fileIndex.has(fullPath)) {
const ext = extname(fullPath)
if (extension.includes(ext)) {
const stat = statSync(fullPath)
const sourceMap = getSourceMapFromFile(fullPath)
if (sourceMap) {
this.sourceMapCache[pathToFileURL(fullPath)] = { data: sourceMap }
}
emptyReports.push({
scriptId: 0,
url: resolve(fullPath),
functions: [{
functionName: '(empty-report)',
ranges: [{
startOffset: 0,
endOffset: stat.size,
count: 0
}],
isBlockCoverage: true
}]
})
}
}
})
}

return emptyReports
}

/**
* Make sure v8ProcessCov actually contains coverage information.
*
Expand All @@ -260,6 +304,28 @@ class Report {
return maybeV8ProcessCov && Array.isArray(maybeV8ProcessCov.result)
}

/**
* Returns the list of V8 process coverages generated by Node.
*
* @return {ProcessCov[]} Process coverages generated by Node.
* @private
*/
_loadReports () {
const reports = []
for (const file of readdirSync(this.tempDirectory)) {
try {
reports.push(JSON.parse(readFileSync(
resolve(this.tempDirectory, file),
'utf8'
)))
} catch (err) {
debuglog(`${err.stack}`)
}
}
return reports
}


/**
* Normalizes a process coverage.
*
Expand Down
Loading

0 comments on commit 0978bdf

Please sign in to comment.