/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /** * Memory leak hunter. Walks a tree of objects looking for DOM nodes. * Usage: * leakHunt({ * thing: thing, * otherthing: otherthing * }); */ function leakHunt(root) { var path = []; var seen = []; try { var output = leakHunt.inner(root, path, seen); output.forEach(function(line) { dump(line + '\n'); }); } catch (ex) { dump(ex + '\n'); } } leakHunt.inner = function LH_inner(root, path, seen) { var prefix = new Array(path.length).join(' '); var reply = []; function log(msg) { reply.push(msg); } var direct try { direct = Object.keys(root); } catch (ex) { log(prefix + ' Error enumerating: ' + ex); return reply; } try { var index = 0; for (var data of root) { var prop = '' + index; leakHunt.digProperty(prop, data, path, seen, direct, log); index++; } } catch (ex) { /* Ignore things that are not enumerable */ } for (var prop in root) { var data; try { data = root[prop]; } catch (ex) { log(prefix + ' ' + prop + ' = Error: ' + ex.toString().substring(0, 30)); continue; } leakHunt.digProperty(prop, data, path, seen, direct, log); } return reply; } leakHunt.hide = [ /^string$/, /^number$/, /^boolean$/, /^null/, /^undefined/ ]; leakHunt.noRecurse = [ /^string$/, /^number$/, /^boolean$/, /^null/, /^undefined/, /^Window$/, /^Document$/, /^XULDocument$/, /^XULElement$/, /^DOMWindow$/, /^HTMLDocument$/, /^HTML.*Element$/, /^ChromeWindow$/ ]; leakHunt.digProperty = function LH_digProperty(prop, data, path, seen, direct, log) { var newPath = path.slice(); newPath.push(prop); var prefix = new Array(newPath.length).join(' '); var recurse = true; var message = leakHunt.getType(data); if (leakHunt.matchesAnyPattern(message, leakHunt.hide)) { return; } if (message === 'function' && direct.indexOf(prop) == -1) { return; } if (message === 'string') { var extra = data.length > 10 ? data.substring(0, 9) + '_' : data; message += ' "' + extra.replace(/\n/g, "|") + '"'; recurse = false; } else if (leakHunt.matchesAnyPattern(message, leakHunt.noRecurse)) { message += ' (no recurse)' recurse = false; } else if (seen.indexOf(data) !== -1) { message += ' (already seen)'; recurse = false; } if (recurse) { seen.push(data); var lines = leakHunt.inner(data, newPath, seen); if (lines.length == 0) { if (message !== 'function') { log(prefix + prop + ' = ' + message + ' { }'); } } else { log(prefix + prop + ' = ' + message + ' {'); lines.forEach(function(line) { log(line); }); log(prefix + '}'); } } else { log(prefix + prop + ' = ' + message); } }; leakHunt.matchesAnyPattern = function LH_matchesAnyPattern(str, patterns) { var match = false; patterns.forEach(function(pattern) { if (str.match(pattern)) { match = true; } }); return match; }; leakHunt.getType = function LH_getType(data) { if (data === null) { return 'null'; } if (data === undefined) { return 'undefined'; } var type = typeof data; if (type === 'object' || type === 'Object') { type = leakHunt.getCtorName(data); } return type; }; leakHunt.getCtorName = function LH_getCtorName(aObj) { try { if (aObj.constructor && aObj.constructor.name) { return aObj.constructor.name; } } catch (ex) { return 'UnknownObject'; } // If that fails, use Objects toString which sometimes gives something // better than 'Object', and at least defaults to Object if nothing better return Object.prototype.toString.call(aObj).slice(8, -1); };