/* 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);
};