leakage
Version:
Memory leak testing for node. Javascript memory footprinting using your favorite test runner.
87 lines (69 loc) • 2.93 kB
JavaScript
const ExtendableError = require('es6-error')
const leftPad = require('left-pad')
const prettyBytes = require('pretty-bytes')
class MemoryLeakError extends ExtendableError { }
module.exports = {
MemoryLeakError,
testConstantHeapSize
}
function testConstantHeapSize (heapDiffs, { iterations, gcollections, sensitivity = 1024 }) {
const subsequentHeapGrowths = getSubsequentHeapGrowths(heapDiffs, sensitivity)
const throwOnSubsequentHeapGrowths = Math.floor(heapDiffs.length * 2 / 3)
if (subsequentHeapGrowths.length > throwOnSubsequentHeapGrowths) {
const lastHeapDiff = subsequentHeapGrowths[ subsequentHeapGrowths.length - 1 ]
const heapGrowthIterations = Math.round(subsequentHeapGrowths.length * iterations)
const growthInBytes = subsequentHeapGrowths
.map(heapDiff => heapDiff.change.size_bytes)
.reduce((total, heapGrowth) => (total + heapGrowth), 0)
return new MemoryLeakError(
`Heap grew on ${subsequentHeapGrowths.length} subsequent garbage collections ` +
`(${formatInteger(heapGrowthIterations)} of ${iterations * gcollections} iterations) ` +
`by ${prettyBytes(growthInBytes)}.\n\n` +
` Iterations between GCs: ${formatInteger(iterations)}\n\n` +
` Final GC details:\n` +
` ${prettyHeapContents(lastHeapDiff).trimLeft()}\n`
)
} else {
return null
}
}
function getSubsequentHeapGrowths (heapDiffs, sensitivity) {
const growthSeriesSets = []
let subsequentGrowths = []
heapDiffs.forEach(heapDiff => {
if (heapDiff.change.size_bytes > sensitivity) {
subsequentGrowths.push(heapDiff)
} else {
if (subsequentGrowths.length > 0) {
growthSeriesSets.push(subsequentGrowths)
}
subsequentGrowths = []
}
})
if (subsequentGrowths.length > 0) {
growthSeriesSets.push(subsequentGrowths)
}
return getLongestItem(growthSeriesSets, [])
}
function getLongestItem (array, defaultValue) {
return array.reduce((longestItem, currentItem) => (
currentItem.length > longestItem.length ? currentItem : longestItem
), defaultValue)
}
function prettyHeapContents (lastHeapDiff) {
const byGrowth = (a, b) => (a.size_bytes < b.size_bytes ? 1 : -1)
const formatHeapContent = (item) => (
`[${leftPad(prettyBytes(item.size_bytes), 10)}] [+ ${leftPad(item['+'], 3)}x] [- ${leftPad(item['-'], 3)}x] ${item.what}`
)
const sortedDetails = [].concat(lastHeapDiff.change.details).sort(byGrowth)
const formattedHeapContents = sortedDetails.map((heapContentItem) => formatHeapContent(heapContentItem))
const heapContentLines = formattedHeapContents.length > 4
? formattedHeapContents.slice(0, 4).concat(`... (${formattedHeapContents.length - 4} more)`)
: formattedHeapContents
return heapContentLines
.map((line) => ` ${line}`)
.join('\n')
}
function formatInteger (value) {
return Math.round(value) !== value ? '~' + Math.round(value) : value
}