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