@memlab/core
Advanced tools
Comparing version 1.1.16 to 1.1.17
@@ -74,2 +74,3 @@ /** | ||
webSourceDir: string; | ||
debugDataDir: string; | ||
runMetaFile: string; | ||
@@ -94,2 +95,3 @@ snapshotSequenceFile: string; | ||
metricsOutDir: string; | ||
heapAnalysisLogDir: string; | ||
reportScreenshotFile: string; | ||
@@ -96,0 +98,0 @@ newUniqueClusterDir: string; |
@@ -26,2 +26,3 @@ /** | ||
private log; | ||
private logFileSet; | ||
private styles; | ||
@@ -44,2 +45,4 @@ private static singleton; | ||
private printStr; | ||
registerLogFile(logFile: string): void; | ||
unregisterLogFile(logFile: string): void; | ||
beginSection(name: string): void; | ||
@@ -46,0 +49,0 @@ endSection(name: string): void; |
@@ -17,2 +17,3 @@ /** | ||
const fs_1 = __importDefault(require("fs")); | ||
const path_1 = __importDefault(require("path")); | ||
const readline_1 = __importDefault(require("readline")); | ||
@@ -22,2 +23,3 @@ const string_width_1 = __importDefault(require("string-width")); | ||
const TABLE_MAX_WIDTH = 50; | ||
const LOG_BUFFER_LENGTH = 100; | ||
const prevLine = '\x1b[F'; | ||
@@ -47,2 +49,12 @@ const eraseLine = '\x1b[K'; | ||
} | ||
function registerExitCleanup(inst, exitHandler) { | ||
const p = process; | ||
// normal exit | ||
p.on('exit', exitHandler.bind(null, { cleanup: true })); | ||
// ctrl + c event | ||
p.on('SIGINT', exitHandler.bind(null, { exit: true })); | ||
// kill pid | ||
p.on('SIGUSR1', exitHandler.bind(null, { exit: true })); | ||
p.on('SIGUSR2', exitHandler.bind(null, { exit: true })); | ||
} | ||
class MemLabConsole { | ||
@@ -52,2 +64,3 @@ constructor() { | ||
this.log = []; | ||
this.logFileSet = new Set(); | ||
this.styles = { | ||
@@ -74,8 +87,11 @@ top: (msg) => msg, | ||
MemLabConsole.singleton = inst; | ||
// clean up output | ||
const exitHandler = ( | ||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
process.on('exit', (_code) => { | ||
inst.flushLog(); | ||
_options, | ||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
_exitCode) => { | ||
inst.flushLog({ sync: true }); | ||
inst.clearPrevOverwriteMsg(); | ||
}); | ||
}; | ||
registerExitCleanup(inst, exitHandler); | ||
return inst; | ||
@@ -106,24 +122,45 @@ } | ||
// remove control characters | ||
const rawMsg = msg | ||
const lines = msg.split('\n').map(line => line | ||
// eslint-disable-next-line no-control-regex | ||
.replace(/[\u0000-\u001F\u007F-\u009F]/g, '') | ||
.replace(/\[\d{1,3}m/g, ''); | ||
this.log.push(rawMsg); | ||
if (this.log.length > 20) { | ||
this.flushLog(); | ||
.replace(/\[\d{1,3}m/g, '')); | ||
this.log.push(...lines); | ||
if (this.log.length > LOG_BUFFER_LENGTH) { | ||
this.flushLog({ sync: true }); | ||
} | ||
} | ||
flushLog() { | ||
flushLog(options = {}) { | ||
const str = this.log.join('\n'); | ||
if (str.length > 0) { | ||
const file = this.config.consoleLogFile; | ||
fs_1.default.appendFile(file, str + '\n', 'UTF-8', () => { | ||
// NOOP | ||
}); | ||
this.log = []; | ||
if (str.length === 0) { | ||
return; | ||
} | ||
this.log = []; | ||
// synchronous logging | ||
if (options.sync) { | ||
for (const logFile of this.logFileSet) { | ||
try { | ||
fs_1.default.appendFileSync(logFile, str + '\n', 'UTF-8'); | ||
} | ||
catch (_a) { | ||
// fail silently | ||
} | ||
} | ||
} | ||
else { | ||
// async logging | ||
const emptyCallback = () => { | ||
// no op | ||
}; | ||
for (const logFile of this.logFileSet) { | ||
try { | ||
fs_1.default.appendFile(logFile, str + '\n', 'UTF-8', emptyCallback); | ||
} | ||
catch (_b) { | ||
// fail silently | ||
} | ||
} | ||
} | ||
} | ||
pushMsg(msg, options = {}) { | ||
const len = this.sections.arr.length; | ||
if (this.config.isContinuousTest || len === 0) { | ||
if (this.sections.arr.length === 0) { | ||
return; | ||
@@ -149,3 +186,5 @@ } | ||
} | ||
stdout.write(eraseLine); | ||
if (!this.config.muteConsole) { | ||
stdout.write(eraseLine); | ||
} | ||
const msg = section.msgs.pop(); | ||
@@ -160,4 +199,6 @@ if (!msg) { | ||
let n = line === 0 ? 1 : Math.ceil(line / width); | ||
while (n-- > 0) { | ||
stdout.write(prevLine + eraseLine); | ||
if (!this.config.muteConsole && !this.config.isTest) { | ||
while (n-- > 0) { | ||
stdout.write(prevLine + eraseLine); | ||
} | ||
} | ||
@@ -200,8 +241,18 @@ } | ||
printStr(msg, options = {}) { | ||
if (this.config.isTest || this.config.muteConsole) { | ||
this.pushMsg(msg, options); | ||
if (this.config.isTest) { | ||
return; | ||
} | ||
console.log(msg); | ||
this.pushMsg(msg, options); | ||
if (this.config.isContinuousTest || !this.config.muteConsole) { | ||
console.log(msg); | ||
} | ||
} | ||
registerLogFile(logFile) { | ||
this.flushLog({ sync: true }); | ||
this.logFileSet.add(path_1.default.resolve(logFile)); | ||
} | ||
unregisterLogFile(logFile) { | ||
this.flushLog({ sync: true }); | ||
this.logFileSet.delete(path_1.default.resolve(logFile)); | ||
} | ||
beginSection(name) { | ||
@@ -312,10 +363,12 @@ if (this.config.isContinuousTest) { | ||
overwrite(msg, options = {}) { | ||
const str = this.style(msg, options.level || 'low'); | ||
if (this.config.isContinuousTest) { | ||
this.printStr(msg, { isOverwrite: false }); | ||
return; | ||
} | ||
if (this.config.isTest || this.config.muteConsole) { | ||
this.printStr(str, { isOverwrite: true }); | ||
return; | ||
} | ||
if (this.config.isContinuousTest) { | ||
return console.log(msg); | ||
} | ||
this.clearPrevOverwriteMsg(); | ||
const str = this.style(msg, options.level || 'low'); | ||
this.printStr(str, { isOverwrite: true }); | ||
@@ -322,0 +375,0 @@ } |
@@ -35,4 +35,7 @@ /** | ||
getWebSourceMetaFile(options?: FileOption): string; | ||
getDebugDataDir(options?: FileOption): string; | ||
getDebugSourceFile(options?: FileOption): string; | ||
getPersistDataDir(options: FileOption): string; | ||
getLoggerOutDir(options?: FileOption): string; | ||
getHeapAnalysisLogDir(options?: FileOption): string; | ||
getTraceClustersDir(options?: FileOption): string; | ||
@@ -65,2 +68,3 @@ getTraceJSONDir(options?: FileOption): string; | ||
}; | ||
initNewHeapAnalysisLogFile(options?: FileOption): string; | ||
getAndInitTSCompileIntermediateDir(): string; | ||
@@ -67,0 +71,0 @@ clearDataDirs(options?: FileOption): void; |
@@ -101,2 +101,8 @@ "use strict"; | ||
} | ||
getDebugDataDir(options = {}) { | ||
return path_1.default.join(this.getDataBaseDir(options), 'debug'); | ||
} | ||
getDebugSourceFile(options = {}) { | ||
return path_1.default.join(this.getDebugDataDir(options), 'file.js'); | ||
} | ||
getPersistDataDir(options) { | ||
@@ -108,2 +114,6 @@ return path_1.default.join(this.getDataBaseDir(options), 'persist'); | ||
} | ||
// all heap analysis results generated | ||
getHeapAnalysisLogDir(options = {}) { | ||
return path_1.default.join(this.getLoggerOutDir(options), 'heap-analysis'); | ||
} | ||
// all trace clusters generated from the current run | ||
@@ -205,2 +215,11 @@ getTraceClustersDir(options = {}) { | ||
} | ||
// create a unique log file created for heap analysis output | ||
initNewHeapAnalysisLogFile(options = {}) { | ||
const dir = this.getHeapAnalysisLogDir(options); | ||
const file = path_1.default.join(dir, `analysis-${Utils_1.default.getUniqueID()}-out.log`); | ||
if (!fs_extra_1.default.existsSync(file)) { | ||
fs_extra_1.default.createFileSync(file); | ||
} | ||
return file; | ||
} | ||
getAndInitTSCompileIntermediateDir() { | ||
@@ -218,2 +237,3 @@ const dir = path_1.default.join(this.getTmpDir(), 'memlab-code'); | ||
this.emptyDirIfExists(this.getWebSourceDir(options)); | ||
this.emptyDirIfExists(this.getDebugDataDir(options)); | ||
const dataSuffix = ['.heapsnapshot', '.json', '.png']; | ||
@@ -245,2 +265,4 @@ const files = fs_extra_1.default.readdirSync(curDataDir); | ||
this.emptyDirIfExists(this.getUniqueTraceClusterDir(options)); | ||
// all heap analysis results | ||
this.emptyDirIfExists(this.getHeapAnalysisLogDir(options)); | ||
} | ||
@@ -302,5 +324,8 @@ resetBrowserDir() { | ||
config.webSourceDir = joinAndProcessDir(options, this.getWebSourceDir(options)); | ||
config.debugDataDir = joinAndProcessDir(options, this.getDebugDataDir(options)); | ||
config.dataBuilderDataDir = joinAndProcessDir(options, config.dataBaseDir, 'dataBuilder'); | ||
config.persistentDataDir = joinAndProcessDir(options, this.getPersistDataDir(options)); | ||
// register the default log file | ||
config.consoleLogFile = path_1.default.join(config.curDataDir, 'console-log.txt'); | ||
Console_1.default.registerLogFile(config.consoleLogFile); | ||
config.runMetaFile = this.getRunMetaFile(options); | ||
@@ -323,2 +348,4 @@ config.snapshotSequenceFile = this.getSnapshotSequenceMetaFile(options); | ||
config.traceJsonOutDir = joinAndProcessDir(options, this.getTraceJSONDir(options)); | ||
// heap analysis results | ||
config.heapAnalysisLogDir = joinAndProcessDir(options, this.getHeapAnalysisLogDir(options)); | ||
config.metricsOutDir = joinAndProcessDir(options, loggerOutDir, 'metrics'); | ||
@@ -325,0 +352,0 @@ config.reportScreenshotFile = path_1.default.join(outDir, 'report.png'); |
@@ -260,6 +260,5 @@ /** | ||
const locationIdx = heapSnapshot._nodeIdx2LocationIdx[this.idx]; | ||
if (locationIdx == null) { | ||
return null; | ||
} | ||
return new HeapLocation_1.default(heapSnapshot, locationIdx); | ||
return locationIdx == null | ||
? null | ||
: new HeapLocation_1.default(heapSnapshot, locationIdx); | ||
} | ||
@@ -266,0 +265,0 @@ // search reference by edge name and edge type |
@@ -209,4 +209,4 @@ /** | ||
while (locationIdx < this._locationCount) { | ||
const id = locations[locationIdx * locationFieldsCount + this._locationObjectIndexOffset]; | ||
this._nodeIdx2LocationIdx[id] = locationIdx; | ||
const nodeIndex = locations[locationIdx * locationFieldsCount + this._locationObjectIndexOffset]; | ||
this._nodeIdx2LocationIdx[nodeIndex] = locationIdx; | ||
++locationIdx; | ||
@@ -213,0 +213,0 @@ } |
@@ -10,11 +10,6 @@ /** | ||
*/ | ||
import type { E2EStepInfo, HeapNodeIdSet, IHeapNode, IHeapSnapshot, IMemoryAnalystOptions, IMemoryAnalystSnapshotDiff, LeakTracePathItem, Optional, IOveralLeakInfo, TraceCluster, ISerializedInfo } from './Types'; | ||
import type { E2EStepInfo, HeapNodeIdSet, IHeapSnapshot, IMemoryAnalystOptions, IMemoryAnalystSnapshotDiff, IOveralHeapInfo, LeakTracePathItem, Optional, IOveralLeakInfo, TraceCluster, ISerializedInfo } from './Types'; | ||
import TraceFinder from '../paths/TraceFinder'; | ||
declare class MemoryAnalyst { | ||
checkLeak(): Promise<ISerializedInfo[]>; | ||
checkUnbound(options?: IMemoryAnalystOptions): Promise<void>; | ||
breakDownMemoryByShapes(options?: { | ||
file?: string; | ||
}): Promise<void>; | ||
detectUnboundGrowth(options?: IMemoryAnalystOptions): Promise<void>; | ||
detectMemoryLeaks(): Promise<ISerializedInfo[]>; | ||
@@ -27,3 +22,2 @@ visualizeMemoryUsage(options?: IMemoryAnalystOptions): void; | ||
diffSnapshots(loadAll?: boolean): Promise<IMemoryAnalystSnapshotDiff>; | ||
private calculateRetainedSizes; | ||
preparePathFinder(snapshot: IHeapSnapshot): TraceFinder; | ||
@@ -33,10 +27,7 @@ private dumpPageInteractionSummary; | ||
private filterLeakedObjects; | ||
aggregateDominatorMetrics(ids: HeapNodeIdSet, snapshot: IHeapSnapshot, checkNodeCb: (node: IHeapNode) => boolean, nodeMetricsCb: (node: IHeapNode) => number): number; | ||
private getOverallHeapInfo; | ||
getRetainedSize(node: IHeapNode): number; | ||
getOverallHeapInfo(snapshot: IHeapSnapshot, options?: { | ||
force?: boolean; | ||
}): Optional<IOveralHeapInfo>; | ||
getOverallLeakInfo(leakedNodeIds: HeapNodeIdSet, snapshot: IHeapSnapshot): Optional<IOveralLeakInfo>; | ||
private printHeapInfo; | ||
private breakDownSnapshotByShapes; | ||
private isTrivialEdgeForBreakDown; | ||
private breakDownByReferrers; | ||
printHeapInfo(leakInfo: IOveralHeapInfo): void; | ||
private printHeapAndLeakInfo; | ||
@@ -43,0 +34,0 @@ private logLeakTraceSummary; |
@@ -26,3 +26,2 @@ /** | ||
const babar_1 = __importDefault(require("babar")); | ||
const chalk_1 = __importDefault(require("chalk")); | ||
const LeakClusterLogger_1 = __importDefault(require("../logger/LeakClusterLogger")); | ||
@@ -46,152 +45,2 @@ const LeakTraceDetailsLogger_1 = __importDefault(require("../logger/LeakTraceDetailsLogger")); | ||
} | ||
checkUnbound(options = {}) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
this.visualizeMemoryUsage(options); | ||
Utils_1.default.checkSnapshots(options); | ||
yield this.detectUnboundGrowth(options); | ||
}); | ||
} | ||
breakDownMemoryByShapes(options = {}) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const opt = { buildNodeIdIndex: true, verbose: true }; | ||
const file = options.file || | ||
Utils_1.default.getSnapshotFilePathWithTabType(/.*/) || | ||
'<EMPTY_FILE_PATH>'; | ||
const snapshot = yield Utils_1.default.getSnapshotFromFile(file, opt); | ||
this.preparePathFinder(snapshot); | ||
const heapInfo = this.getOverallHeapInfo(snapshot, { force: true }); | ||
if (heapInfo) { | ||
this.printHeapInfo(heapInfo); | ||
} | ||
this.breakDownSnapshotByShapes(snapshot); | ||
}); | ||
} | ||
// find any objects that keeps growing | ||
detectUnboundGrowth(options = {}) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const nodeInfo = Object.create(null); | ||
let hasCheckedFirstSnapshot = false; | ||
let snapshot = null; | ||
const isValidNode = (node) => node.type === 'object' || | ||
node.type === 'closure' || | ||
node.type === 'regexp'; | ||
const initNodeInfo = (node) => { | ||
if (!isValidNode(node)) { | ||
return; | ||
} | ||
const n = node.retainedSize; | ||
nodeInfo[node.id] = { | ||
type: node.type, | ||
name: node.name, | ||
min: n, | ||
max: n, | ||
history: [n], | ||
node, | ||
}; | ||
}; | ||
const updateNodeInfo = (node) => { | ||
const item = nodeInfo[node.id]; | ||
if (!item) { | ||
return; | ||
} | ||
if (node.name !== item.name || node.type !== item.type) { | ||
nodeInfo[node.id] = null; | ||
return; | ||
} | ||
const n = node.retainedSize; | ||
// only monotonic increase? | ||
if (Config_1.default.monotonicUnboundGrowthOnly && n < item.max) { | ||
nodeInfo[node.id] = null; | ||
return; | ||
} | ||
item.history.push(n); | ||
item.max = Math.max(item.max, n); | ||
item.min = Math.min(item.min, n); | ||
}; | ||
// summarize the heap objects info in current heap snapshot | ||
// this is mainly used for better understanding of the % of | ||
// objects released and allocated over time | ||
const maybeSummarizeNodeInfo = () => { | ||
if (!Config_1.default.verbose) { | ||
return; | ||
} | ||
let n = 0; | ||
for (const k in nodeInfo) { | ||
if (nodeInfo[k]) { | ||
++n; | ||
} | ||
} | ||
Console_1.default.lowLevel(`Objects tracked: ${n}`); | ||
}; | ||
Console_1.default.overwrite('Checking unbounded objects...'); | ||
const snapshotFiles = options.snapshotDir | ||
? // load snapshots from a directory | ||
Utils_1.default.getSnapshotFilesInDir(options.snapshotDir) | ||
: // load snapshots based on the visit sequence meta data | ||
Utils_1.default.getSnapshotFilesFromTabsOrder(); | ||
for (const file of snapshotFiles) { | ||
// force GC before loading each snapshot | ||
if (global.gc) { | ||
global.gc(); | ||
} | ||
// load and preprocess heap snapshot | ||
const opt = { buildNodeIdIndex: true, verbose: true }; | ||
snapshot = yield Utils_1.default.getSnapshotFromFile(file, opt); | ||
this.calculateRetainedSizes(snapshot); | ||
// keep track of heap objects | ||
if (!hasCheckedFirstSnapshot) { | ||
// record Ids in the snapshot | ||
snapshot.nodes.forEach(initNodeInfo); | ||
hasCheckedFirstSnapshot = true; | ||
} | ||
else { | ||
snapshot.nodes.forEach(updateNodeInfo); | ||
maybeSummarizeNodeInfo(); | ||
} | ||
} | ||
// exit if no heap snapshot found | ||
if (!hasCheckedFirstSnapshot) { | ||
return; | ||
} | ||
// post process and print the unbounded objects | ||
const idsInLastSnapshot = new Set(); | ||
snapshot === null || snapshot === void 0 ? void 0 : snapshot.nodes.forEach(node => { | ||
idsInLastSnapshot.add(node.id); | ||
}); | ||
let ids = []; | ||
for (const key in nodeInfo) { | ||
const id = parseInt(key, 10); | ||
const item = nodeInfo[id]; | ||
if (!item) { | ||
continue; | ||
} | ||
if (!idsInLastSnapshot.has(id)) { | ||
continue; | ||
} | ||
if (item.min === item.max) { | ||
continue; | ||
} | ||
// filter out non-significant leaks | ||
if (item.history[item.history.length - 1] < Config_1.default.unboundSizeThreshold) { | ||
continue; | ||
} | ||
ids.push(Object.assign({ id }, item)); | ||
} | ||
if (ids.length === 0) { | ||
Console_1.default.midLevel('No increasing objects found.'); | ||
return; | ||
} | ||
ids = ids | ||
.sort((o1, o2) => o2.history[o2.history.length - 1] - o1.history[o1.history.length - 1]) | ||
.slice(0, 20); | ||
// print on terminal | ||
const str = Serializer_1.default.summarizeUnboundedObjects(ids, { color: true }); | ||
Console_1.default.topLevel('Top growing objects in sizes:'); | ||
Console_1.default.lowLevel(' (Use `memlab trace --node-id=@ID` to get trace)'); | ||
Console_1.default.topLevel('\n' + str); | ||
// save results to file | ||
const csv = Serializer_1.default.summarizeUnboundedObjectsToCSV(ids); | ||
fs_1.default.writeFileSync(Config_1.default.unboundObjectCSV, csv, 'UTF-8'); | ||
}); | ||
} | ||
// find all unique pattern of leaks | ||
@@ -373,7 +222,2 @@ detectMemoryLeaks() { | ||
} | ||
calculateRetainedSizes(snapshot) { | ||
const finder = new TraceFinder_1.default(); | ||
// dominator and retained size | ||
finder.calculateAllNodesRetainedSizes(snapshot); | ||
} | ||
// initialize the path finder | ||
@@ -448,10 +292,2 @@ preparePathFinder(snapshot) { | ||
} | ||
aggregateDominatorMetrics(ids, snapshot, checkNodeCb, nodeMetricsCb) { | ||
let ret = 0; | ||
const dominators = Utils_1.default.getConditionalDominatorIds(ids, snapshot, checkNodeCb); | ||
Utils_1.default.applyToNodes(dominators, snapshot, node => { | ||
ret += nodeMetricsCb(node); | ||
}); | ||
return ret; | ||
} | ||
getOverallHeapInfo(snapshot, options = {}) { | ||
@@ -464,13 +300,10 @@ if (!Config_1.default.verbose && !options.force) { | ||
const heapInfo = { | ||
fiberNodeSize: this.aggregateDominatorMetrics(allIds, snapshot, Utils_1.default.isFiberNode, this.getRetainedSize), | ||
regularFiberNodeSize: this.aggregateDominatorMetrics(allIds, snapshot, Utils_1.default.isRegularFiberNode, this.getRetainedSize), | ||
detachedFiberNodeSize: this.aggregateDominatorMetrics(allIds, snapshot, Utils_1.default.isDetachedFiberNode, this.getRetainedSize), | ||
alternateFiberNodeSize: this.aggregateDominatorMetrics(allIds, snapshot, Utils_1.default.isAlternateNode, this.getRetainedSize), | ||
error: this.aggregateDominatorMetrics(allIds, snapshot, node => node.name === 'Error', this.getRetainedSize), | ||
fiberNodeSize: Utils_1.default.aggregateDominatorMetrics(allIds, snapshot, Utils_1.default.isFiberNode, Utils_1.default.getRetainedSize), | ||
regularFiberNodeSize: Utils_1.default.aggregateDominatorMetrics(allIds, snapshot, Utils_1.default.isRegularFiberNode, Utils_1.default.getRetainedSize), | ||
detachedFiberNodeSize: Utils_1.default.aggregateDominatorMetrics(allIds, snapshot, Utils_1.default.isDetachedFiberNode, Utils_1.default.getRetainedSize), | ||
alternateFiberNodeSize: Utils_1.default.aggregateDominatorMetrics(allIds, snapshot, Utils_1.default.isAlternateNode, Utils_1.default.getRetainedSize), | ||
error: Utils_1.default.aggregateDominatorMetrics(allIds, snapshot, node => node.name === 'Error', Utils_1.default.getRetainedSize), | ||
}; | ||
return heapInfo; | ||
} | ||
getRetainedSize(node) { | ||
return node.retainedSize; | ||
} | ||
getOverallLeakInfo(leakedNodeIds, snapshot) { | ||
@@ -480,3 +313,3 @@ if (!Config_1.default.verbose) { | ||
} | ||
const leakInfo = Object.assign(Object.assign({}, this.getOverallHeapInfo(snapshot)), { leakedSize: this.aggregateDominatorMetrics(leakedNodeIds, snapshot, () => true, this.getRetainedSize), leakedFiberNodeSize: this.aggregateDominatorMetrics(leakedNodeIds, snapshot, Utils_1.default.isFiberNode, this.getRetainedSize), leakedAlternateFiberNodeSize: this.aggregateDominatorMetrics(leakedNodeIds, snapshot, Utils_1.default.isAlternateNode, this.getRetainedSize) }); | ||
const leakInfo = Object.assign(Object.assign({}, this.getOverallHeapInfo(snapshot)), { leakedSize: Utils_1.default.aggregateDominatorMetrics(leakedNodeIds, snapshot, () => true, Utils_1.default.getRetainedSize), leakedFiberNodeSize: Utils_1.default.aggregateDominatorMetrics(leakedNodeIds, snapshot, Utils_1.default.isFiberNode, Utils_1.default.getRetainedSize), leakedAlternateFiberNodeSize: Utils_1.default.aggregateDominatorMetrics(leakedNodeIds, snapshot, Utils_1.default.isAlternateNode, Utils_1.default.getRetainedSize) }); | ||
return leakInfo; | ||
@@ -494,107 +327,2 @@ } | ||
} | ||
breakDownSnapshotByShapes(snapshot) { | ||
Console_1.default.overwrite('Breaking down memory by shapes...'); | ||
const breakdown = Object.create(null); | ||
const population = Object.create(null); | ||
// group objects based on their shapes | ||
snapshot.nodes.forEach(node => { | ||
if ((node.type !== 'object' && !Utils_1.default.isStringNode(node)) || | ||
Config_1.default.nodeIgnoreSetInShape.has(node.name)) { | ||
return; | ||
} | ||
const key = Serializer_1.default.summarizeNodeShape(node); | ||
breakdown[key] = breakdown[key] || new Set(); | ||
breakdown[key].add(node.id); | ||
if (population[key] === undefined) { | ||
population[key] = { examples: [], n: 0 }; | ||
} | ||
++population[key].n; | ||
// retain the top 5 examples | ||
const examples = population[key].examples; | ||
examples.push(node); | ||
examples.sort((n1, n2) => n2.retainedSize - n1.retainedSize); | ||
if (examples.length > 5) { | ||
examples.pop(); | ||
} | ||
}); | ||
// calculate and sort based on retained sizes | ||
const ret = []; | ||
for (const key in breakdown) { | ||
const size = this.aggregateDominatorMetrics(breakdown[key], snapshot, () => true, this.getRetainedSize); | ||
ret.push({ key, retainedSize: size }); | ||
} | ||
ret.sort((o1, o2) => o2.retainedSize - o1.retainedSize); | ||
Console_1.default.topLevel('Object shapes with top retained sizes:'); | ||
Console_1.default.lowLevel(' (Use `memlab trace --node-id=@ID` to get trace)\n'); | ||
const topList = ret.slice(0, 40); | ||
// print settings | ||
const opt = { color: true, compact: true }; | ||
const dot = chalk_1.default.grey('· '); | ||
const colon = chalk_1.default.grey(': '); | ||
// print the shapes with the biggest retained size | ||
for (const o of topList) { | ||
const referrerInfo = this.breakDownByReferrers(breakdown[o.key], snapshot); | ||
const { examples, n } = population[o.key]; | ||
const shapeStr = Serializer_1.default.summarizeNodeShape(examples[0], opt); | ||
const bytes = Utils_1.default.getReadableBytes(o.retainedSize); | ||
const examplesStr = examples | ||
.map(e => `@${e.id} [${Utils_1.default.getReadableBytes(e.retainedSize)}]`) | ||
.join(' | '); | ||
const meta = chalk_1.default.grey(` (N: ${n}, Examples: ${examplesStr})`); | ||
Console_1.default.topLevel(`${dot}${shapeStr}${colon}${bytes}${meta}`); | ||
Console_1.default.lowLevel(referrerInfo + '\n'); | ||
} | ||
} | ||
isTrivialEdgeForBreakDown(edge) { | ||
const source = edge.fromNode; | ||
return (source.type === 'array' || | ||
source.name === '(object elements)' || | ||
source.name === 'system' || | ||
edge.name_or_index === '__proto__' || | ||
edge.name_or_index === 'prototype'); | ||
} | ||
breakDownByReferrers(ids, snapshot) { | ||
const edgeNames = Object.create(null); | ||
for (const id of ids) { | ||
const node = snapshot.getNodeById(id); | ||
for (const edge of (node === null || node === void 0 ? void 0 : node.referrers) || []) { | ||
const source = edge.fromNode; | ||
if (!Utils_1.default.isMeaningfulEdge(edge) || | ||
this.isTrivialEdgeForBreakDown(edge)) { | ||
continue; | ||
} | ||
const sourceName = Serializer_1.default.summarizeNodeName(source, { | ||
color: false, | ||
}); | ||
const edgeName = Serializer_1.default.summarizeEdgeName(edge, { | ||
color: false, | ||
abstract: true, | ||
}); | ||
const edgeKey = `[${sourceName}] --${edgeName}--> `; | ||
edgeNames[edgeKey] = edgeNames[edgeKey] || { | ||
numberOfEdgesToNode: 0, | ||
source, | ||
edge, | ||
}; | ||
++edgeNames[edgeKey].numberOfEdgesToNode; | ||
} | ||
} | ||
const referrerInfo = Object.entries(edgeNames) | ||
.sort((i1, i2) => i2[1].numberOfEdgesToNode - i1[1].numberOfEdgesToNode) | ||
.slice(0, 4) | ||
.map(i => { | ||
const meta = i[1]; | ||
const source = Serializer_1.default.summarizeNodeName(meta.source, { | ||
color: true, | ||
}); | ||
const edgeName = Serializer_1.default.summarizeEdgeName(meta.edge, { | ||
color: true, | ||
abstract: true, | ||
}); | ||
const edgeSummary = `${source} --${edgeName}-->`; | ||
return ` · ${edgeSummary}: ${meta.numberOfEdgesToNode}`; | ||
}) | ||
.join('\n'); | ||
return referrerInfo; | ||
} | ||
printHeapAndLeakInfo(leakedNodeIds, snapshot) { | ||
@@ -626,6 +354,2 @@ // write page interaction summary to the leaks text file | ||
this.filterLeakedObjects(leakedNodeIds, snapshot); | ||
if (Config_1.default.verbose) { | ||
// show a breakdown of different object structures | ||
this.breakDownSnapshotByShapes(snapshot); | ||
} | ||
const nodeIdInPaths = new Set(); | ||
@@ -653,3 +377,3 @@ const paths = []; | ||
// cluster traces from the current run | ||
const clusters = TraceBucket_1.default.clusterPaths(paths, snapshot, this.aggregateDominatorMetrics, { | ||
const clusters = TraceBucket_1.default.clusterPaths(paths, snapshot, Utils_1.default.aggregateDominatorMetrics, { | ||
strategy: Config_1.default.isMLClustering | ||
@@ -663,3 +387,3 @@ ? new MLTraceSimilarityStrategy_1.default() | ||
// cluster traces from the current run | ||
const clustersUnclassified = TraceBucket_1.default.generateUnClassifiedClusters(paths, snapshot, this.aggregateDominatorMetrics); | ||
const clustersUnclassified = TraceBucket_1.default.generateUnClassifiedClusters(paths, snapshot, Utils_1.default.aggregateDominatorMetrics); | ||
LeakClusterLogger_1.default.logUnclassifiedClusters(clustersUnclassified); | ||
@@ -695,3 +419,3 @@ } | ||
// cluster traces from the current run | ||
const clusters = TraceBucket_1.default.clusterPaths(paths, snapshot, this.aggregateDominatorMetrics, { | ||
const clusters = TraceBucket_1.default.clusterPaths(paths, snapshot, Utils_1.default.aggregateDominatorMetrics, { | ||
strategy: Config_1.default.isMLClustering | ||
@@ -698,0 +422,0 @@ ? new MLTraceSimilarityStrategy_1.default() |
@@ -10,3 +10,3 @@ /** | ||
*/ | ||
import type { HaltOrThrowOptions } from './Types'; | ||
import type { HaltOrThrowOptions, HeapNodeIdSet, ShellOptions } from './Types'; | ||
import type { Browser, Page } from 'puppeteer'; | ||
@@ -126,3 +126,7 @@ import type { AnyAyncFunction, AnyOptions, E2EStepInfo, IHeapSnapshot, IHeapNode, IHeapEdge, IScenario, ILeakFilter, LeakTracePathItem, RunMetaInfo, RawHeapSnapshot, Nullable, Optional } from './Types'; | ||
declare function getClosureSourceUrl(node: IHeapNode): Nullable<string>; | ||
export declare function runShell(command: string, options?: ShellOptions): string; | ||
export declare function getRetainedSize(node: IHeapNode): number; | ||
export declare function aggregateDominatorMetrics(ids: HeapNodeIdSet, snapshot: IHeapSnapshot, checkNodeCb: (node: IHeapNode) => boolean, nodeMetricsCb: (node: IHeapNode) => number): number; | ||
declare const _default: { | ||
aggregateDominatorMetrics: typeof aggregateDominatorMetrics; | ||
applyToNodes: typeof applyToNodes; | ||
@@ -153,2 +157,3 @@ callAsync: typeof callAsync; | ||
getReadableTime: typeof getReadableTime; | ||
getRetainedSize: typeof getRetainedSize; | ||
getRunMetaFilePath: typeof getRunMetaFilePath; | ||
@@ -221,2 +226,3 @@ getScenarioName: typeof getScenarioName; | ||
resolveSnapshotFilePath: typeof resolveSnapshotFilePath; | ||
runShell: typeof runShell; | ||
setIsAlternateNode: typeof setIsAlternateNode; | ||
@@ -223,0 +229,0 @@ setIsRegularFiberNode: typeof setIsRegularFiberNode; |
@@ -47,5 +47,6 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.resolveSnapshotFilePath = void 0; | ||
exports.aggregateDominatorMetrics = exports.getRetainedSize = exports.runShell = exports.resolveSnapshotFilePath = void 0; | ||
const fs_1 = __importDefault(require("fs")); | ||
const path_1 = __importDefault(require("path")); | ||
const child_process_1 = __importDefault(require("child_process")); | ||
const process_1 = __importDefault(require("process")); | ||
@@ -1755,3 +1756,48 @@ const Config_1 = __importStar(require("./Config")); | ||
} | ||
function runShell(command, options = {}) { | ||
var _a, _b, _c; | ||
const runningDir = (_b = (_a = options.dir) !== null && _a !== void 0 ? _a : Config_1.default.workDir) !== null && _b !== void 0 ? _b : FileManager_1.default.getTmpDir(); | ||
const execOptions = { | ||
cwd: runningDir, | ||
stdio: options.disconnectStdio | ||
? [] | ||
: [process_1.default.stdin, process_1.default.stdout, process_1.default.stderr], | ||
}; | ||
if (process_1.default.platform !== 'win32') { | ||
execOptions.shell = '/bin/bash'; | ||
} | ||
let ret = ''; | ||
try { | ||
ret = child_process_1.default.execSync(command, execOptions); | ||
} | ||
catch (ex) { | ||
if (Config_1.default.verbose) { | ||
if (ex instanceof Error) { | ||
Console_1.default.lowLevel(ex.message); | ||
Console_1.default.lowLevel((_c = ex.stack) !== null && _c !== void 0 ? _c : ''); | ||
} | ||
} | ||
if (options.ignoreError === true) { | ||
return ''; | ||
} | ||
__1.utils.haltOrThrow(`Error when executing command: ${command}`); | ||
} | ||
return ret && ret.toString('UTF-8'); | ||
} | ||
exports.runShell = runShell; | ||
function getRetainedSize(node) { | ||
return node.retainedSize; | ||
} | ||
exports.getRetainedSize = getRetainedSize; | ||
function aggregateDominatorMetrics(ids, snapshot, checkNodeCb, nodeMetricsCb) { | ||
let ret = 0; | ||
const dominators = __1.utils.getConditionalDominatorIds(ids, snapshot, checkNodeCb); | ||
__1.utils.applyToNodes(dominators, snapshot, node => { | ||
ret += nodeMetricsCb(node); | ||
}); | ||
return ret; | ||
} | ||
exports.aggregateDominatorMetrics = aggregateDominatorMetrics; | ||
exports.default = { | ||
aggregateDominatorMetrics, | ||
applyToNodes, | ||
@@ -1782,2 +1828,3 @@ callAsync, | ||
getReadableTime, | ||
getRetainedSize, | ||
getRunMetaFilePath, | ||
@@ -1850,2 +1897,3 @@ getScenarioName, | ||
resolveSnapshotFilePath, | ||
runShell, | ||
setIsAlternateNode, | ||
@@ -1852,0 +1900,0 @@ setIsRegularFiberNode, |
{ | ||
"name": "@memlab/core", | ||
"version": "1.1.16", | ||
"version": "1.1.17", | ||
"license": "MIT", | ||
@@ -62,3 +62,3 @@ "description": "memlab core libraries", | ||
"test-pkg": "jest .", | ||
"publish-patch": "npm version patch --force && npm publish", | ||
"publish-patch": "npm publish", | ||
"clean-pkg": "rm -rf ./dist && rm -rf ./node_modules && rm -f ./tsconfig.tsbuildinfo" | ||
@@ -65,0 +65,0 @@ }, |
Sorry, the diff of this file is too big to display
547118
14172
3