225 lines
5.6 KiB
JavaScript
225 lines
5.6 KiB
JavaScript
/**
|
|
* Memory Profiler - Track memory usage during installation
|
|
* Helps identify memory leaks and optimize resource usage
|
|
*/
|
|
|
|
const v8 = require('v8')
|
|
|
|
class MemoryProfiler {
|
|
constructor () {
|
|
this.checkpoints = []
|
|
this.startTime = Date.now()
|
|
this.peakMemory = 0
|
|
}
|
|
|
|
/**
|
|
* Create a memory checkpoint
|
|
* @param {string} label - Label for this checkpoint
|
|
*/
|
|
checkpoint (label) {
|
|
const memUsage = process.memoryUsage()
|
|
const heapStats = v8.getHeapStatistics()
|
|
|
|
const checkpoint = {
|
|
label,
|
|
timestamp: Date.now() - this.startTime,
|
|
memory: {
|
|
rss: this.formatBytes(memUsage.rss),
|
|
heapTotal: this.formatBytes(memUsage.heapTotal),
|
|
heapUsed: this.formatBytes(memUsage.heapUsed),
|
|
external: this.formatBytes(memUsage.external),
|
|
arrayBuffers: this.formatBytes(memUsage.arrayBuffers || 0)
|
|
},
|
|
heap: {
|
|
totalHeapSize: this.formatBytes(heapStats.total_heap_size),
|
|
usedHeapSize: this.formatBytes(heapStats.used_heap_size),
|
|
heapSizeLimit: this.formatBytes(heapStats.heap_size_limit),
|
|
mallocedMemory: this.formatBytes(heapStats.malloced_memory),
|
|
externalMemory: this.formatBytes(heapStats.external_memory)
|
|
},
|
|
raw: {
|
|
heapUsed: memUsage.heapUsed
|
|
}
|
|
}
|
|
|
|
// Track peak memory
|
|
if (memUsage.heapUsed > this.peakMemory) {
|
|
this.peakMemory = memUsage.heapUsed
|
|
}
|
|
|
|
this.checkpoints.push(checkpoint)
|
|
return checkpoint
|
|
}
|
|
|
|
/**
|
|
* Force garbage collection (requires --expose-gc flag)
|
|
*/
|
|
forceGC () {
|
|
if (global.gc) {
|
|
global.gc()
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
/**
|
|
* Get memory usage summary
|
|
*/
|
|
getSummary () {
|
|
const currentMemory = process.memoryUsage()
|
|
|
|
return {
|
|
currentUsage: {
|
|
rss: this.formatBytes(currentMemory.rss),
|
|
heapTotal: this.formatBytes(currentMemory.heapTotal),
|
|
heapUsed: this.formatBytes(currentMemory.heapUsed)
|
|
},
|
|
peakMemory: this.formatBytes(this.peakMemory),
|
|
totalCheckpoints: this.checkpoints.length,
|
|
runTime: `${((Date.now() - this.startTime) / 1000).toFixed(2)}s`
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get detailed report of memory usage
|
|
*/
|
|
getDetailedReport () {
|
|
const summary = this.getSummary()
|
|
const memoryGrowth = this.calculateMemoryGrowth()
|
|
|
|
return {
|
|
summary,
|
|
memoryGrowth,
|
|
checkpoints: this.checkpoints,
|
|
recommendations: this.getRecommendations(memoryGrowth)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calculate memory growth between checkpoints
|
|
*/
|
|
calculateMemoryGrowth () {
|
|
if (this.checkpoints.length < 2) return []
|
|
|
|
const growth = []
|
|
for (let i = 1; i < this.checkpoints.length; i++) {
|
|
const prev = this.checkpoints[i - 1]
|
|
const curr = this.checkpoints[i]
|
|
|
|
const heapDiff = curr.raw.heapUsed - prev.raw.heapUsed
|
|
|
|
growth.push({
|
|
from: prev.label,
|
|
to: curr.label,
|
|
heapGrowth: this.formatBytes(Math.abs(heapDiff)),
|
|
isIncrease: heapDiff > 0,
|
|
timeDiff: `${((curr.timestamp - prev.timestamp) / 1000).toFixed(2)}s`
|
|
})
|
|
}
|
|
|
|
return growth
|
|
}
|
|
|
|
/**
|
|
* Get recommendations based on memory usage
|
|
*/
|
|
getRecommendations (memoryGrowth) {
|
|
const recommendations = []
|
|
|
|
// Check for large memory growth
|
|
const largeGrowths = memoryGrowth.filter(g => {
|
|
const bytes = this.parseBytes(g.heapGrowth)
|
|
return bytes > 50 * 1024 * 1024 // 50MB
|
|
})
|
|
|
|
if (largeGrowths.length > 0) {
|
|
recommendations.push({
|
|
type: 'warning',
|
|
message: `Large memory growth detected in ${largeGrowths.length} operations`,
|
|
details: largeGrowths.map(g => `${g.from} → ${g.to}: ${g.heapGrowth}`)
|
|
})
|
|
}
|
|
|
|
// Check peak memory
|
|
if (this.peakMemory > 500 * 1024 * 1024) { // 500MB
|
|
recommendations.push({
|
|
type: 'warning',
|
|
message: `High peak memory usage: ${this.formatBytes(this.peakMemory)}`,
|
|
suggestion: 'Consider processing files in smaller batches'
|
|
})
|
|
}
|
|
|
|
// Check for potential memory leaks
|
|
const continuousGrowth = this.checkContinuousGrowth()
|
|
if (continuousGrowth) {
|
|
recommendations.push({
|
|
type: 'error',
|
|
message: 'Potential memory leak detected',
|
|
details: 'Memory usage continuously increases without significant decreases'
|
|
})
|
|
}
|
|
|
|
return recommendations
|
|
}
|
|
|
|
/**
|
|
* Check for continuous memory growth (potential leak)
|
|
*/
|
|
checkContinuousGrowth () {
|
|
if (this.checkpoints.length < 5) return false
|
|
|
|
let increasingCount = 0
|
|
for (let i = 1; i < this.checkpoints.length; i++) {
|
|
if (this.checkpoints[i].raw.heapUsed > this.checkpoints[i - 1].raw.heapUsed) {
|
|
increasingCount++
|
|
}
|
|
}
|
|
|
|
// If memory increases in more than 80% of checkpoints, might be a leak
|
|
return increasingCount / (this.checkpoints.length - 1) > 0.8
|
|
}
|
|
|
|
/**
|
|
* Format bytes to human-readable string
|
|
*/
|
|
formatBytes (bytes) {
|
|
if (bytes === 0) return '0 B'
|
|
|
|
const k = 1024
|
|
const sizes = ['B', 'KB', 'MB', 'GB']
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
|
}
|
|
|
|
/**
|
|
* Parse human-readable bytes back to number
|
|
*/
|
|
parseBytes (str) {
|
|
const match = str.match(/^([\d.]+)\s*([KMGT]?B?)$/i)
|
|
if (!match) return 0
|
|
|
|
const value = parseFloat(match[1])
|
|
const unit = match[2].toUpperCase()
|
|
|
|
const multipliers = {
|
|
B: 1,
|
|
KB: 1024,
|
|
MB: 1024 * 1024,
|
|
GB: 1024 * 1024 * 1024
|
|
}
|
|
|
|
return value * (multipliers[unit] || 1)
|
|
}
|
|
|
|
/**
|
|
* Clear checkpoints to free memory
|
|
*/
|
|
clear () {
|
|
this.checkpoints = []
|
|
}
|
|
}
|
|
|
|
// Export singleton instance
|
|
module.exports = new MemoryProfiler()
|