leakhunt.js (3878B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 /** 8 * Memory leak hunter. Walks a tree of objects looking for DOM nodes. 9 * Usage: 10 * leakHunt({ 11 * thing: thing, 12 * otherthing: otherthing 13 * }); 14 */ 15 function leakHunt(root) { 16 const path = []; 17 const seen = []; 18 19 try { 20 const output = leakHunt.inner(root, path, seen); 21 output.forEach(function (line) { 22 dump(line + "\n"); 23 }); 24 } catch (ex) { 25 dump(ex + "\n"); 26 } 27 } 28 29 leakHunt.inner = function (root, path, seen) { 30 const prefix = new Array(path.length).join(" "); 31 32 const reply = []; 33 function log(msg) { 34 reply.push(msg); 35 } 36 37 let direct; 38 try { 39 direct = Object.keys(root); 40 } catch (ex) { 41 log(prefix + " Error enumerating: " + ex); 42 return reply; 43 } 44 45 try { 46 let index = 0; 47 for (const data of root) { 48 const prop = "" + index; 49 leakHunt.digProperty(prop, data, path, seen, direct, log); 50 index++; 51 } 52 } catch (ex) { 53 /* Ignore things that are not enumerable */ 54 } 55 56 for (const prop in root) { 57 let data; 58 try { 59 data = root[prop]; 60 } catch (ex) { 61 log(prefix + " " + prop + " = Error: " + ex.toString().substring(0, 30)); 62 continue; 63 } 64 65 leakHunt.digProperty(prop, data, path, seen, direct, log); 66 } 67 68 return reply; 69 }; 70 71 leakHunt.hide = [/^string$/, /^number$/, /^boolean$/, /^null/, /^undefined/]; 72 73 leakHunt.noRecurse = [ 74 /^string$/, 75 /^number$/, 76 /^boolean$/, 77 /^null/, 78 /^undefined/, 79 /^Window$/, 80 /^Document$/, 81 /^XULElement$/, 82 /^DOMWindow$/, 83 /^HTMLDocument$/, 84 /^HTML.*Element$/, 85 /^ChromeWindow$/, 86 ]; 87 88 leakHunt.digProperty = function (prop, data, path, seen, direct, log) { 89 const newPath = path.slice(); 90 newPath.push(prop); 91 const prefix = new Array(newPath.length).join(" "); 92 93 let recurse = true; 94 let message = leakHunt.getType(data); 95 96 if (leakHunt.matchesAnyPattern(message, leakHunt.hide)) { 97 return; 98 } 99 100 if (message === "function" && !direct.includes(prop)) { 101 return; 102 } 103 104 if (message === "string") { 105 const extra = data.length > 10 ? data.substring(0, 9) + "_" : data; 106 message += ' "' + extra.replace(/\n/g, "|") + '"'; 107 recurse = false; 108 } else if (leakHunt.matchesAnyPattern(message, leakHunt.noRecurse)) { 109 message += " (no recurse)"; 110 recurse = false; 111 } else if (seen.includes(data)) { 112 message += " (already seen)"; 113 recurse = false; 114 } 115 116 if (recurse) { 117 seen.push(data); 118 const lines = leakHunt.inner(data, newPath, seen); 119 if (!lines.length) { 120 if (message !== "function") { 121 log(prefix + prop + " = " + message + " { }"); 122 } 123 } else { 124 log(prefix + prop + " = " + message + " {"); 125 lines.forEach(function (line) { 126 log(line); 127 }); 128 log(prefix + "}"); 129 } 130 } else { 131 log(prefix + prop + " = " + message); 132 } 133 }; 134 135 leakHunt.matchesAnyPattern = function (str, patterns) { 136 let match = false; 137 patterns.forEach(function (pattern) { 138 if (str.match(pattern)) { 139 match = true; 140 } 141 }); 142 return match; 143 }; 144 145 leakHunt.getType = function (data) { 146 if (data === null) { 147 return "null"; 148 } 149 if (data === undefined) { 150 return "undefined"; 151 } 152 153 let type = typeof data; 154 if (type === "object" || type === "Object") { 155 type = leakHunt.getCtorName(data); 156 } 157 158 return type; 159 }; 160 161 leakHunt.getCtorName = function (obj) { 162 try { 163 if (obj.constructor && obj.constructor.name) { 164 return obj.constructor.name; 165 } 166 } catch (ex) { 167 return "UnknownObject"; 168 } 169 170 // If that fails, use Objects toString which sometimes gives something 171 // better than 'Object', and at least defaults to Object if nothing better 172 return Object.prototype.toString.call(obj).slice(8, -1); 173 };