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