blob: 57ec78c2f64441aa71a61af3d1d09a4ed1bd1fc1 [file] [log] [blame] [edit]
var emscriptenMemoryProfiler = {
// If true, walks all allocated pointers at graphing time to print a detailed memory fragmentation map. If false, used
// memory is only graphed in one block (at the bottom of DYNAMIC memory space). Set this to false to improve performance at the expense of
// accuracy.
detailedHeapUsage: true,
// Allocations of memory blocks larger than this threshold will get their detailed callstack captured and logged at runtime.
// Warning: This can be extremely slow. Set to a very very large value like 1024*1024*1024*4 to disable.
// Disabled if stack information is not available in this browser
trackedCallstackMinSizeBytes: (typeof new Error().stack === 'undefined') ? 1024*1024*1024*4 : 16*1024*1024,
// Controls whether all outstanding allocation are printed to html page by callstack.
allocateStatistics: false,
// If allocateStatistics = true, then all callstacks that have recorded more than the following number of allocations will be printed to html page.
allocateStatisticsMinReported: 100,
// If true, we hook into Runtime.stackAlloc to be able to catch better estimate of the maximum used STACK space.
// You might only ever want to set this to false for performance reasons. Since stack allocations may occur often, this might impact performance.
hookStackAlloc: true,
// How often the log page is refreshed.
uiUpdateIntervalMsecs: 2000,
// Tracks data for the allocation statistics.
allocationSiteStatistics: {},
allocationSitePtrs: {},
// Stores an associative array of records HEAP ptr -> size so that we can retrieve how much memory was freed in calls to
// _free() and decrement the tracked usage accordingly.
// E.g. allocatedPtrSizes[address] returns the size of the heap pointer starting at 'address'.
allocatedPtrSizes: {},
// Conceptually same as the above array, except this one tracks only pointers that were allocated during the application preRun step, which
// corresponds to the data added to the VFS with --preload-file.
preRunMallocs: {},
// Once set to true, preRun is finished and the above array is not touched anymore.
pagePreRunIsFinished: false,
// Stores an associative array of records HEAP ptr -> function string name so that we can identify each allocated pointer
// by the location in code the allocation occurred in.
callstackOfAllocatedPtr: {},
// Stores an associative array of accumulated amount of memory allocated per location.
// E.g. allocatedBytesPerCallstack[callstack_function_name_string] returns the total number of allocated bytes performed from 'callstack_function_name_string'.
allocatedBytesPerCallstack: {},
// Grand total of memory currently allocated via malloc(). Decremented on free()s.
totalMemoryAllocated: 0,
// The running count of the number of times malloc() and free() have been called in the app. Used to keep track of # of currently alive pointers.
// TODO: Perhaps in the future give a statistic of allocations per second to see how trashing memory usage is.
totalTimesMallocCalled: 0,
totalTimesFreeCalled: 0,
// Tracks the highest seen location of the STACKTOP variable.
stackTopWatermark: 0,
// The canvas DOM element to which to draw the allocation map.
canvas: null,
// The 2D drawing context on the canvas.
drawContext: null,
// Converts number f to string with at most two decimals, without redundant trailing zeros.
truncDec: function truncDec(f) {
var str = f.toFixed(2);
if (str.indexOf('.00', str.length-3) !== -1) return str.substr(0, str.length-3);
else if (str.indexOf('0', str.length-1) !== -1) return str.substr(0, str.length-1);
else return str;
},
// Converts a number of bytes pretty-formatted as a string.
formatBytes: function formatBytes(bytes) {
if (bytes >= 1000*1024*1024) return this.truncDec(bytes/(1024*1024*1024)) + ' GB';
else if (bytes >= 1000*1024) return this.truncDec(bytes/(1024*1024)) + ' MB';
else if (bytes >= 1000) return this.truncDec(bytes/1024) + ' KB';
else return this.truncDec(bytes) + ' B';
},
onMalloc: function onMalloc(ptr, size) {
// Gather global stats.
this.totalMemoryAllocated += size;
++this.totalTimesMallocCalled;
this.stackTopWatermark = Math.max(this.stackTopWatermark, STACKTOP);
// Remember the size of the allocated block to know how much will be _free()d later.
this.allocatedPtrSizes[ptr] = size;
// Also track if this was a _malloc performed at preRun time.
if (!this.pagePreRunIsFinished) this.preRunMallocs[ptr] = size;
if (this.allocateStatistics) {
var loc = new Error().stack.toString();
var str = loc;
if (!this.allocationSiteStatistics[str]) this.allocationSiteStatistics[str] = 1;
else ++this.allocationSiteStatistics[str];
this.allocationSitePtrs[ptr] = loc;
}
// If this is a large enough allocation, track its detailed callstack info.
if (size > this.trackedCallstackMinSizeBytes) {
// Get the caller function as string.
var loc = new Error().stack.toString();
var nl = loc.indexOf('\n')+1;
loc = loc.substr(nl);
loc = loc.replace(/\n/g, '<br />');
var caller = loc;
this.callstackOfAllocatedPtr[ptr] = caller;
if (this.allocatedBytesPerCallstack[caller] > 0) this.allocatedBytesPerCallstack[caller] += size;
else this.allocatedBytesPerCallstack[caller] = size;
}
},
onFree: function onFree(ptr) {
if (!ptr) return;
// Decrement global stats.
var sz = this.allocatedPtrSizes[ptr];
if (!isNaN(sz)) {
this.totalMemoryAllocated -= sz;
}
delete this.allocatedPtrSizes[ptr];
delete this.preRunMallocs[ptr]; // Also free if this happened to be a _malloc performed at preRun time.
this.stackTopWatermark = Math.max(this.stackTopWatermark, STACKTOP);
if (this.allocateStatistics) {
var loc = this.allocationSitePtrs[ptr];
if (loc) {
var str = loc;
--this.allocationSiteStatistics[str];
}
this.allocationSitePtrs[ptr] = null;
}
// Decrement per-alloc stats if this was a large allocation.
if (sz > this.trackedCallstackMinSizeBytes) {
var caller = this.callstackOfAllocatedPtr[ptr];
delete this.callstackOfAllocatedPtr[ptr];
this.allocatedBytesPerCallstack[caller] -= sz;
if (this.allocatedBytesPerCallstack[caller] <= 0) delete this.allocatedBytesPerCallstack[caller];
}
++this.totalTimesFreeCalled;
},
onRealloc: function onRealloc(oldAddress, newAddress, size) {
this.onFree(oldAddress);
this.onMalloc(newAddress, size);
},
onPreloadComplete: function onPreloadComplete() {
this.pagePreRunIsFinished = true;
},
// Installs startup hook and periodic UI update timer.
initialize: function initialize() {
// Inject the memoryprofiler hooks.
Module['onMalloc'] = function onMalloc(ptr, size) { emscriptenMemoryProfiler.onMalloc(ptr, size); };
Module['onRealloc'] = function onRealloc(oldAddress, newAddress, size) { emscriptenMemoryProfiler.onRealloc(oldAddress, newAddress, size); };
Module['onFree'] = function onFree(ptr) { emscriptenMemoryProfiler.onFree(ptr); };
// Add a tracking mechanism to detect when VFS loading is complete.
Module['preRun'].push(function() { emscriptenMemoryProfiler.onPreloadComplete(); });
if (this.hookStackAlloc) {
// Inject stack allocator.
var prevStackAlloc = Runtime.stackAlloc;
function hookedStackAlloc(size) {
emscriptenMemoryProfiler.stackTopWatermark = Math.max(emscriptenMemoryProfiler.stackTopWatermark, STACKTOP + size);
return prevStackAlloc(size);
}
Runtime.stackAlloc = hookedStackAlloc;
}
memoryprofiler = document.getElementById('memoryprofiler');
if (!memoryprofiler) {
var div = document.createElement("div");
div.innerHTML = "<div style='border: 2px solid black; padding: 2px;'><canvas style='border: 1px solid black; margin-left: auto; margin-right: auto; display: block;' id='memoryprofiler_canvas' width='100%' height='50'></canvas>trackedCallstackMinSizeBytes=<input id='memoryprofiler_min_tracked_alloc_size' type=number value="+this.trackedCallstackMinSizeBytes+"></input><br/><input id='memoryprofiler_enable_allocation_stats' type='checkbox'>Print allocation statistics by callstack to html log (warning: slow!)</input><input id='memoryprofiler_clear_alloc_stats' type='button' value='Clear alloc stats' ></input><div id='memoryprofiler'></div>";
document.body.appendChild(div);
memoryprofiler = document.getElementById('memoryprofiler');
var self = this;
document.getElementById('memoryprofiler_min_tracked_alloc_size').addEventListener("change", function(e){self.trackedCallstackMinSizeBytes=this.value;});
document.getElementById('memoryprofiler_clear_alloc_stats').addEventListener("click", function(e){self.allocationSiteStatistics = {}; self.allocationSitePtrs = {};});
document.getElementById('memoryprofiler_enable_allocation_stats').addEventListener("change", function(e){self.allocateStatistics=this.checked;});
}
this.canvas = document.getElementById('memoryprofiler_canvas');
this.canvas.width = document.documentElement.clientWidth - 32;
this.drawContext = this.canvas.getContext('2d');
this.updateUi();
setInterval(function() { emscriptenMemoryProfiler.updateUi() }, this.uiUpdateIntervalMsecs);
},
// Given a pointer 'bytes', compute the linear 1D position on the graph as pixels, rounding down for start address of a block.
bytesToPixelsRoundedDown: function bytesToPixelsRoundedDown(bytes) {
return (bytes * this.canvas.width * this.canvas.height / TOTAL_MEMORY) | 0;
},
// Same as bytesToPixelsRoundedDown, but rounds up for the end address of a block. The different rounding will
// guarantee that even 'thin' allocations should get at least one pixel dot in the graph.
bytesToPixelsRoundedUp: function bytesToPixelsRoundedUp(bytes) {
return ((bytes * this.canvas.width * this.canvas.height + TOTAL_MEMORY - 1) / TOTAL_MEMORY) | 0;
},
// Graphs a range of allocated memory. The memory range will be drawn as a top-to-bottom, left-to-right stripes or columns of pixels.
fillLine: function fillLine(startBytes, endBytes) {
var startPixels = this.bytesToPixelsRoundedDown(startBytes);
var endPixels = this.bytesToPixelsRoundedUp(endBytes);
// Starting pos (top-left corner) of this allocation on the graph.
var x0 = (startPixels / this.canvas.height) | 0;
var y0 = startPixels - x0 * this.canvas.height;
// Ending pos (bottom-right corner) of this allocation on the graph.
var x1 = (endPixels / this.canvas.height) | 0;
var y1 = endPixels - x1 * this.canvas.height;
// Draw the left side partial column of the allocation block.
if (y0 > 0 && x0 < x1) {
this.drawContext.fillRect(x0, y0, 1, this.canvas.height - y0);
// Proceed to the start of the next full column.
y0 = 0;
++x0;
}
// Draw the right side partial column.
if (y1 < this.canvas.height && x0 < x1) {
this.drawContext.fillRect(x1, 0, 1, y1);
// Decrement to the previous full column.
y1 = this.canvas.height - 1;
--x1;
}
// After filling the previous leftovers with one-pixel-wide lines, we are only left with a rectangular shape of full columns to blit.
this.drawContext.fillRect(x0, 0, x1 - x0 + 1, this.canvas.height);
},
countOpenALAudioDataSize: function countOpenALAudioDataSize() {
if (typeof AL == "undefined" || !AL.currentContext) return 0;
var totalMemory = 0;
for (var i in AL.currentContext.buf) {
var buffer = AL.currentContext.buf[i];
for (var channel = 0; channel < buffer.numberOfChannels; ++channel) totalMemory += buffer.getChannelData(channel).length * 4;
}
return totalMemory;
},
// Print accurate map of individual allocations. This will show information about
// memory fragmentation and allocation sizes.
// Warning: This will walk through all allocations, so it is slow!
printAllocsWithCyclingColors: function printAllocsWithCyclingColors(colors, allocs) {
var colorIndex = 0;
for (var i in allocs) {
this.drawContext.fillStyle = colors[colorIndex];
colorIndex = (colorIndex + 1) % colors.length;
var start = i|0;
var sz = allocs[start]|0;
this.fillLine(start, start + sz);
}
},
// Main UI update entry point.
updateUi: function updateUi() {
function colorBar(color) {
return '<span style="padding:0px; border:solid 1px black; width:28px;height:14px; vertical-align:middle; display:inline-block; background-color:'+color+';"></span>';
}
// Naive function to compute how many bits will be needed to represent the number 'n' in binary. This will be our pointer 'word width' in the UI.
function nBits(n) {
var i = 0;
while (n >= 1) {
++i;
n /= 2;
}
return i;
}
// Returns i formatted to string as fixed-width hexadecimal.
function toHex(i, width) {
var str = i.toString(16);
while (str.length < width) str = '0' + str;
return '0x'+str;
}
// Poll whether user as changed the browser window, and if so, resize the profiler window and redraw it.
if (this.canvas.width != document.documentElement.clientWidth - 32) {
this.canvas.width = document.documentElement.clientWidth - 32;
}
var width = (nBits(TOTAL_MEMORY) + 3) / 4; // Pointer 'word width'
var html = 'Total HEAP size: ' + this.formatBytes(TOTAL_MEMORY) + '.';
html += '<br />' + colorBar('#202020') + 'STATIC memory area size: ' + this.formatBytes(STATICTOP - STATIC_BASE);
html += '. STATIC_BASE: ' + toHex(STATIC_BASE, width);
html += '. STATICTOP: ' + toHex(STATICTOP, width) + '.';
html += '<br />' + colorBar('#FF8080') + 'STACK memory area size: ' + this.formatBytes(STACK_MAX - STACK_BASE);
html += '. STACK_BASE: ' + toHex(STACK_BASE, width);
html += '. STACKTOP: ' + toHex(STACKTOP, width);
html += '. STACK_MAX: ' + toHex(STACK_MAX, width) + '.';
html += '<br />STACK memory area used now (should be zero): ' + this.formatBytes(STACKTOP - STACK_BASE) + '.' + colorBar('#FFFF00') + ' STACK watermark highest seen usage (approximate lower-bound!): ' + this.formatBytes(this.stackTopWatermark - STACK_BASE);
var preloadedMemoryUsed = 0;
for (i in this.preRunMallocs) preloadedMemoryUsed += this.preRunMallocs[i]|0;
html += '<br />' + colorBar('#FF9900') + colorBar('#FFDD33') + 'Preloaded memory used, most likely memory reserved by files in the virtual filesystem : ' + this.formatBytes(preloadedMemoryUsed);
html += '<br />OpenAL audio data: ' + this.formatBytes(this.countOpenALAudioDataSize()) + ' (outside HEAP)';
html += '<br />' + colorBar('#FFFFFF') + 'Unallocated HEAP space: ' + this.formatBytes(TOTAL_MEMORY - STACK_MAX);
html += '<br /># of total malloc()s/free()s performed in app lifetime: ' + this.totalTimesMallocCalled + '/' + this.totalTimesFreeCalled + ' (delta: ' + (this.totalTimesMallocCalled-this.totalTimesFreeCalled) + ')';
// Background clear
this.drawContext.fillStyle = "#FFFFFF";
this.drawContext.fillRect(0, 0, this.canvas.width, this.canvas.height);
this.drawContext.fillStyle = "#202020";
this.fillLine(STATIC_BASE, STATICTOP);
this.drawContext.fillStyle = "#FF8080";
this.fillLine(STACK_BASE, STACK_MAX);
this.drawContext.fillStyle = "#FFFF00";
this.fillLine(STACK_BASE, this.stackTopWatermark);
this.drawContext.fillStyle = "#FF0000";
this.fillLine(STACK_BASE, STACKTOP);
if (this.detailedHeapUsage) {
this.printAllocsWithCyclingColors(["#6699CC", "#003366", "#0000FF"], this.allocatedPtrSizes);
this.printAllocsWithCyclingColors(["#FF9900", "#FFDD33"], this.preRunMallocs);
} else {
// Print only a single naive blob of individual allocations. This will not be accurate, but is constant-time.
this.drawContext.fillStyle = "#0000FF";
this.fillLine(DYNAMIC_BASE, DYNAMIC_BASE + this.totalMemoryAllocated);
}
function isEmpty(cont) {
for (i in cont) return false;
return true;
}
// Print out statistics of individual allocations if they were tracked.
if (!isEmpty(this.allocatedBytesPerCallstack)) {
html += '<h4>Notable allocation sites<h4>'
for (var i in this.allocatedBytesPerCallstack) {
html += '<b>'+this.formatBytes(this.allocatedBytesPerCallstack[i]|0)+'</b>: ' + i + '<br />';
}
}
if (!isEmpty(this.allocationSiteStatistics)) {
var calls = [];
for (var i in this.allocationSiteStatistics) {
var numcalls = this.allocationSiteStatistics[i];
if (numcalls >= this.allocateStatisticsMinReported) calls.push(i);
}
var self = this;
calls.sort(function(a,b) { return self.allocationSiteStatistics[b] - self.allocationSiteStatistics[a]; });
html += '<h4>Allocated pointers by call stack:<h4>';
var ndemangled = 10;
for (var i in calls) {
var callstack = calls[i];
var numcalls = this.allocationSiteStatistics[callstack];
if (ndemangled > 0) {
callstack = demangleAll(callstack);
callstack = callstack.split('\n').join('<br />');
--ndemangled;
}
html += callstack + ': <b>' + numcalls + '</b><br /><br />';
}
}
memoryprofiler.innerHTML = html;
}
};
// Backwards compatibility with previously compiled code. Don't call this anymore!
function memoryprofiler_add_hooks() { emscriptenMemoryProfiler.initialize(); }
if (typeof Module !== 'undefined' && typeof document !== 'undefined') emscriptenMemoryProfiler.initialize();