diff options
Diffstat (limited to 'devtools/client/shared/test/leakhunt.js')
-rw-r--r-- | devtools/client/shared/test/leakhunt.js | 173 |
1 files changed, 173 insertions, 0 deletions
diff --git a/devtools/client/shared/test/leakhunt.js b/devtools/client/shared/test/leakhunt.js new file mode 100644 index 0000000000..40b5fb6792 --- /dev/null +++ b/devtools/client/shared/test/leakhunt.js @@ -0,0 +1,173 @@ +/* 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/. */ + +"use strict"; + +/** + * Memory leak hunter. Walks a tree of objects looking for DOM nodes. + * Usage: + * leakHunt({ + * thing: thing, + * otherthing: otherthing + * }); + */ +function leakHunt(root) { + const path = []; + const seen = []; + + try { + const output = leakHunt.inner(root, path, seen); + output.forEach(function (line) { + dump(line + "\n"); + }); + } catch (ex) { + dump(ex + "\n"); + } +} + +leakHunt.inner = function (root, path, seen) { + const prefix = new Array(path.length).join(" "); + + const reply = []; + function log(msg) { + reply.push(msg); + } + + let direct; + try { + direct = Object.keys(root); + } catch (ex) { + log(prefix + " Error enumerating: " + ex); + return reply; + } + + try { + let index = 0; + for (const data of root) { + const prop = "" + index; + leakHunt.digProperty(prop, data, path, seen, direct, log); + index++; + } + } catch (ex) { + /* Ignore things that are not enumerable */ + } + + for (const prop in root) { + let 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$/, + /^XULElement$/, + /^DOMWindow$/, + /^HTMLDocument$/, + /^HTML.*Element$/, + /^ChromeWindow$/, +]; + +leakHunt.digProperty = function (prop, data, path, seen, direct, log) { + const newPath = path.slice(); + newPath.push(prop); + const prefix = new Array(newPath.length).join(" "); + + let recurse = true; + let message = leakHunt.getType(data); + + if (leakHunt.matchesAnyPattern(message, leakHunt.hide)) { + return; + } + + if (message === "function" && !direct.includes(prop)) { + return; + } + + if (message === "string") { + const 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.includes(data)) { + message += " (already seen)"; + recurse = false; + } + + if (recurse) { + seen.push(data); + const lines = leakHunt.inner(data, newPath, seen); + if (!lines.length) { + 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 (str, patterns) { + let match = false; + patterns.forEach(function (pattern) { + if (str.match(pattern)) { + match = true; + } + }); + return match; +}; + +leakHunt.getType = function (data) { + if (data === null) { + return "null"; + } + if (data === undefined) { + return "undefined"; + } + + let type = typeof data; + if (type === "object" || type === "Object") { + type = leakHunt.getCtorName(data); + } + + return type; +}; + +leakHunt.getCtorName = function (obj) { + try { + if (obj.constructor && obj.constructor.name) { + return obj.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(obj).slice(8, -1); +}; |