|
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 "use strict"; |
|
5 |
|
6 module.metadata = { |
|
7 "stability": "experimental" |
|
8 }; |
|
9 |
|
10 const { Cc, Ci, Cu } = require("chrome"); |
|
11 const { Loader } = require('./loader'); |
|
12 const { serializeStack, parseStack } = require("toolkit/loader"); |
|
13 const { setTimeout } = require('../timers'); |
|
14 const { PlainTextConsole } = require("../console/plain-text"); |
|
15 const { when: unload } = require("../system/unload"); |
|
16 const { format, fromException } = require("../console/traceback"); |
|
17 const system = require("../system"); |
|
18 const memory = require('../deprecated/memory'); |
|
19 const { gc: gcPromise } = require('./memory'); |
|
20 const { defer } = require('../core/promise'); |
|
21 |
|
22 // Trick manifest builder to make it think we need these modules ? |
|
23 const unit = require("../deprecated/unit-test"); |
|
24 const test = require("../../test"); |
|
25 const url = require("../url"); |
|
26 |
|
27 function emptyPromise() { |
|
28 let { promise, resolve } = defer(); |
|
29 resolve(); |
|
30 return promise; |
|
31 } |
|
32 |
|
33 var cService = Cc['@mozilla.org/consoleservice;1'].getService() |
|
34 .QueryInterface(Ci.nsIConsoleService); |
|
35 |
|
36 // The console used to log messages |
|
37 var testConsole; |
|
38 |
|
39 // Cuddlefish loader in which we load and execute tests. |
|
40 var loader; |
|
41 |
|
42 // Function to call when we're done running tests. |
|
43 var onDone; |
|
44 |
|
45 // Function to print text to a console, w/o CR at the end. |
|
46 var print; |
|
47 |
|
48 // How many more times to run all tests. |
|
49 var iterationsLeft; |
|
50 |
|
51 // Whether to report memory profiling information. |
|
52 var profileMemory; |
|
53 |
|
54 // Whether we should stop as soon as a test reports a failure. |
|
55 var stopOnError; |
|
56 |
|
57 // Function to call to retrieve a list of tests to execute |
|
58 var findAndRunTests; |
|
59 |
|
60 // Combined information from all test runs. |
|
61 var results = { |
|
62 passed: 0, |
|
63 failed: 0, |
|
64 testRuns: [] |
|
65 }; |
|
66 |
|
67 // A list of the compartments and windows loaded after startup |
|
68 var startLeaks; |
|
69 |
|
70 // JSON serialization of last memory usage stats; we keep it stringified |
|
71 // so we don't actually change the memory usage stats (in terms of objects) |
|
72 // of the JSRuntime we're profiling. |
|
73 var lastMemoryUsage; |
|
74 |
|
75 function analyzeRawProfilingData(data) { |
|
76 var graph = data.graph; |
|
77 var shapes = {}; |
|
78 |
|
79 // Convert keys in the graph from strings to ints. |
|
80 // TODO: Can we get rid of this ridiculousness? |
|
81 var newGraph = {}; |
|
82 for (id in graph) { |
|
83 newGraph[parseInt(id)] = graph[id]; |
|
84 } |
|
85 graph = newGraph; |
|
86 |
|
87 var modules = 0; |
|
88 var moduleIds = []; |
|
89 var moduleObjs = {UNKNOWN: 0}; |
|
90 for (let name in data.namedObjects) { |
|
91 moduleObjs[name] = 0; |
|
92 moduleIds[data.namedObjects[name]] = name; |
|
93 modules++; |
|
94 } |
|
95 |
|
96 var count = 0; |
|
97 for (id in graph) { |
|
98 var parent = graph[id].parent; |
|
99 while (parent) { |
|
100 if (parent in moduleIds) { |
|
101 var name = moduleIds[parent]; |
|
102 moduleObjs[name]++; |
|
103 break; |
|
104 } |
|
105 if (!(parent in graph)) { |
|
106 moduleObjs.UNKNOWN++; |
|
107 break; |
|
108 } |
|
109 parent = graph[parent].parent; |
|
110 } |
|
111 count++; |
|
112 } |
|
113 |
|
114 print("\nobject count is " + count + " in " + modules + " modules" + |
|
115 " (" + data.totalObjectCount + " across entire JS runtime)\n"); |
|
116 if (lastMemoryUsage) { |
|
117 var last = JSON.parse(lastMemoryUsage); |
|
118 var diff = { |
|
119 moduleObjs: dictDiff(last.moduleObjs, moduleObjs), |
|
120 totalObjectClasses: dictDiff(last.totalObjectClasses, |
|
121 data.totalObjectClasses) |
|
122 }; |
|
123 |
|
124 for (let name in diff.moduleObjs) |
|
125 print(" " + diff.moduleObjs[name] + " in " + name + "\n"); |
|
126 for (let name in diff.totalObjectClasses) |
|
127 print(" " + diff.totalObjectClasses[name] + " instances of " + |
|
128 name + "\n"); |
|
129 } |
|
130 lastMemoryUsage = JSON.stringify( |
|
131 {moduleObjs: moduleObjs, |
|
132 totalObjectClasses: data.totalObjectClasses} |
|
133 ); |
|
134 } |
|
135 |
|
136 function dictDiff(last, curr) { |
|
137 var diff = {}; |
|
138 |
|
139 for (let name in last) { |
|
140 var result = (curr[name] || 0) - last[name]; |
|
141 if (result) |
|
142 diff[name] = (result > 0 ? "+" : "") + result; |
|
143 } |
|
144 for (let name in curr) { |
|
145 var result = curr[name] - (last[name] || 0); |
|
146 if (result) |
|
147 diff[name] = (result > 0 ? "+" : "") + result; |
|
148 } |
|
149 return diff; |
|
150 } |
|
151 |
|
152 function reportMemoryUsage() { |
|
153 if (!profileMemory) { |
|
154 return emptyPromise(); |
|
155 } |
|
156 |
|
157 return gcPromise().then((function () { |
|
158 var mgr = Cc["@mozilla.org/memory-reporter-manager;1"] |
|
159 .getService(Ci.nsIMemoryReporterManager); |
|
160 let count = 0; |
|
161 function logReporter(process, path, kind, units, amount, description) { |
|
162 print(((++count == 1) ? "\n" : "") + description + ": " + amount + "\n"); |
|
163 } |
|
164 mgr.getReportsForThisProcess(logReporter, null); |
|
165 |
|
166 var weakrefs = [info.weakref.get() |
|
167 for each (info in memory.getObjects())]; |
|
168 weakrefs = [weakref for each (weakref in weakrefs) if (weakref)]; |
|
169 print("Tracked memory objects in testing sandbox: " + weakrefs.length + "\n"); |
|
170 })); |
|
171 } |
|
172 |
|
173 var gWeakrefInfo; |
|
174 |
|
175 function checkMemory() { |
|
176 return gcPromise().then(_ => { |
|
177 let leaks = getPotentialLeaks(); |
|
178 |
|
179 let compartmentURLs = Object.keys(leaks.compartments).filter(function(url) { |
|
180 return !(url in startLeaks.compartments); |
|
181 }); |
|
182 |
|
183 let windowURLs = Object.keys(leaks.windows).filter(function(url) { |
|
184 return !(url in startLeaks.windows); |
|
185 }); |
|
186 |
|
187 for (let url of compartmentURLs) |
|
188 console.warn("LEAKED", leaks.compartments[url]); |
|
189 |
|
190 for (let url of windowURLs) |
|
191 console.warn("LEAKED", leaks.windows[url]); |
|
192 }).then(showResults); |
|
193 } |
|
194 |
|
195 function showResults() { |
|
196 let { promise, resolve } = defer(); |
|
197 |
|
198 if (gWeakrefInfo) { |
|
199 gWeakrefInfo.forEach( |
|
200 function(info) { |
|
201 var ref = info.weakref.get(); |
|
202 if (ref !== null) { |
|
203 var data = ref.__url__ ? ref.__url__ : ref; |
|
204 var warning = data == "[object Object]" |
|
205 ? "[object " + data.constructor.name + "(" + |
|
206 [p for (p in data)].join(", ") + ")]" |
|
207 : data; |
|
208 console.warn("LEAK", warning, info.bin); |
|
209 } |
|
210 } |
|
211 ); |
|
212 } |
|
213 |
|
214 onDone(results); |
|
215 |
|
216 resolve(); |
|
217 return promise; |
|
218 } |
|
219 |
|
220 function cleanup() { |
|
221 let coverObject = {}; |
|
222 try { |
|
223 for (let name in loader.modules) |
|
224 memory.track(loader.modules[name], |
|
225 "module global scope: " + name); |
|
226 memory.track(loader, "Cuddlefish Loader"); |
|
227 |
|
228 if (profileMemory) { |
|
229 gWeakrefInfo = [{ weakref: info.weakref, bin: info.bin } |
|
230 for each (info in memory.getObjects())]; |
|
231 } |
|
232 |
|
233 loader.unload(); |
|
234 |
|
235 if (loader.globals.console.errorsLogged && !results.failed) { |
|
236 results.failed++; |
|
237 console.error("warnings and/or errors were logged."); |
|
238 } |
|
239 |
|
240 if (consoleListener.errorsLogged && !results.failed) { |
|
241 console.warn(consoleListener.errorsLogged + " " + |
|
242 "warnings or errors were logged to the " + |
|
243 "platform's nsIConsoleService, which could " + |
|
244 "be of no consequence; however, they could also " + |
|
245 "be indicative of aberrant behavior."); |
|
246 } |
|
247 |
|
248 // read the code coverage object, if it exists, from CoverJS-moz |
|
249 if (typeof loader.globals.global == "object") { |
|
250 coverObject = loader.globals.global['__$coverObject'] || {}; |
|
251 } |
|
252 |
|
253 consoleListener.errorsLogged = 0; |
|
254 loader = null; |
|
255 |
|
256 memory.gc(); |
|
257 } |
|
258 catch (e) { |
|
259 results.failed++; |
|
260 console.error("unload.send() threw an exception."); |
|
261 console.exception(e); |
|
262 }; |
|
263 |
|
264 setTimeout(require('@test/options').checkMemory ? checkMemory : showResults, 1); |
|
265 |
|
266 // dump the coverobject |
|
267 if (Object.keys(coverObject).length){ |
|
268 const self = require('sdk/self'); |
|
269 const {pathFor} = require("sdk/system"); |
|
270 let file = require('sdk/io/file'); |
|
271 const {env} = require('sdk/system/environment'); |
|
272 console.log("CWD:", env.PWD); |
|
273 let out = file.join(env.PWD,'coverstats-'+self.id+'.json'); |
|
274 console.log('coverstats:', out); |
|
275 let outfh = file.open(out,'w'); |
|
276 outfh.write(JSON.stringify(coverObject,null,2)); |
|
277 outfh.flush(); |
|
278 outfh.close(); |
|
279 } |
|
280 } |
|
281 |
|
282 function getPotentialLeaks() { |
|
283 memory.gc(); |
|
284 |
|
285 // Things we can assume are part of the platform and so aren't leaks |
|
286 let WHITELIST_BASE_URLS = [ |
|
287 "chrome://", |
|
288 "resource:///", |
|
289 "resource://app/", |
|
290 "resource://gre/", |
|
291 "resource://gre-resources/", |
|
292 "resource://pdf.js/", |
|
293 "resource://pdf.js.components/", |
|
294 "resource://services-common/", |
|
295 "resource://services-crypto/", |
|
296 "resource://services-sync/" |
|
297 ]; |
|
298 |
|
299 let ioService = Cc["@mozilla.org/network/io-service;1"]. |
|
300 getService(Ci.nsIIOService); |
|
301 let uri = ioService.newURI("chrome://global/content/", "UTF-8", null); |
|
302 let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"]. |
|
303 getService(Ci.nsIChromeRegistry); |
|
304 uri = chromeReg.convertChromeURL(uri); |
|
305 let spec = uri.spec; |
|
306 let pos = spec.indexOf("!/"); |
|
307 WHITELIST_BASE_URLS.push(spec.substring(0, pos + 2)); |
|
308 |
|
309 let zoneRegExp = new RegExp("^explicit/js-non-window/zones/zone[^/]+/compartment\\((.+)\\)"); |
|
310 let compartmentRegexp = new RegExp("^explicit/js-non-window/compartments/non-window-global/compartment\\((.+)\\)/"); |
|
311 let compartmentDetails = new RegExp("^([^,]+)(?:, (.+?))?(?: \\(from: (.*)\\))?$"); |
|
312 let windowRegexp = new RegExp("^explicit/window-objects/top\\((.*)\\)/active"); |
|
313 let windowDetails = new RegExp("^(.*), id=.*$"); |
|
314 |
|
315 function isPossibleLeak(item) { |
|
316 if (!item.location) |
|
317 return false; |
|
318 |
|
319 for (let whitelist of WHITELIST_BASE_URLS) { |
|
320 if (item.location.substring(0, whitelist.length) == whitelist) |
|
321 return false; |
|
322 } |
|
323 |
|
324 return true; |
|
325 } |
|
326 |
|
327 let compartments = {}; |
|
328 let windows = {}; |
|
329 function logReporter(process, path, kind, units, amount, description) { |
|
330 let matches; |
|
331 |
|
332 if ((matches = compartmentRegexp.exec(path)) || (matches = zoneRegExp.exec(path))) { |
|
333 if (matches[1] in compartments) |
|
334 return; |
|
335 |
|
336 let details = compartmentDetails.exec(matches[1]); |
|
337 if (!details) { |
|
338 console.error("Unable to parse compartment detail " + matches[1]); |
|
339 return; |
|
340 } |
|
341 |
|
342 let item = { |
|
343 path: matches[1], |
|
344 principal: details[1], |
|
345 location: details[2] ? details[2].replace("\\", "/", "g") : undefined, |
|
346 source: details[3] ? details[3].split(" -> ").reverse() : undefined, |
|
347 toString: function() this.location |
|
348 }; |
|
349 |
|
350 if (!isPossibleLeak(item)) |
|
351 return; |
|
352 |
|
353 compartments[matches[1]] = item; |
|
354 return; |
|
355 } |
|
356 |
|
357 if (matches = windowRegexp.exec(path)) { |
|
358 if (matches[1] in windows) |
|
359 return; |
|
360 |
|
361 let details = windowDetails.exec(matches[1]); |
|
362 if (!details) { |
|
363 console.error("Unable to parse window detail " + matches[1]); |
|
364 return; |
|
365 } |
|
366 |
|
367 let item = { |
|
368 path: matches[1], |
|
369 location: details[1].replace("\\", "/", "g"), |
|
370 source: [details[1].replace("\\", "/", "g")], |
|
371 toString: function() this.location |
|
372 }; |
|
373 |
|
374 if (!isPossibleLeak(item)) |
|
375 return; |
|
376 |
|
377 windows[matches[1]] = item; |
|
378 } |
|
379 } |
|
380 |
|
381 Cc["@mozilla.org/memory-reporter-manager;1"] |
|
382 .getService(Ci.nsIMemoryReporterManager) |
|
383 .getReportsForThisProcess(logReporter, null); |
|
384 |
|
385 return { compartments: compartments, windows: windows }; |
|
386 } |
|
387 |
|
388 function nextIteration(tests) { |
|
389 if (tests) { |
|
390 results.passed += tests.passed; |
|
391 results.failed += tests.failed; |
|
392 |
|
393 reportMemoryUsage().then(_ => { |
|
394 let testRun = []; |
|
395 for each (let test in tests.testRunSummary) { |
|
396 let testCopy = {}; |
|
397 for (let info in test) { |
|
398 testCopy[info] = test[info]; |
|
399 } |
|
400 testRun.push(testCopy); |
|
401 } |
|
402 |
|
403 results.testRuns.push(testRun); |
|
404 iterationsLeft--; |
|
405 |
|
406 checkForEnd(); |
|
407 }) |
|
408 } |
|
409 else { |
|
410 checkForEnd(); |
|
411 } |
|
412 } |
|
413 |
|
414 function checkForEnd() { |
|
415 if (iterationsLeft && (!stopOnError || results.failed == 0)) { |
|
416 // Pass the loader which has a hooked console that doesn't dispatch |
|
417 // errors to the JS console and avoid firing false alarm in our |
|
418 // console listener |
|
419 findAndRunTests(loader, nextIteration); |
|
420 } |
|
421 else { |
|
422 setTimeout(cleanup, 0); |
|
423 } |
|
424 } |
|
425 |
|
426 var POINTLESS_ERRORS = [ |
|
427 'Invalid chrome URI:', |
|
428 'OpenGL LayerManager Initialized Succesfully.', |
|
429 '[JavaScript Error: "TelemetryStopwatch:', |
|
430 'reference to undefined property', |
|
431 '[JavaScript Error: "The character encoding of the HTML document was ' + |
|
432 'not declared.', |
|
433 '[Javascript Warning: "Error: Failed to preserve wrapper of wrapped ' + |
|
434 'native weak map key', |
|
435 '[JavaScript Warning: "Duplicate resource declaration for', |
|
436 'file: "chrome://browser/content/', |
|
437 'file: "chrome://global/content/', |
|
438 '[JavaScript Warning: "The character encoding of a framed document was ' + |
|
439 'not declared.' |
|
440 ]; |
|
441 |
|
442 var consoleListener = { |
|
443 errorsLogged: 0, |
|
444 observe: function(object) { |
|
445 if (!(object instanceof Ci.nsIScriptError)) |
|
446 return; |
|
447 this.errorsLogged++; |
|
448 var message = object.QueryInterface(Ci.nsIConsoleMessage).message; |
|
449 var pointless = [err for each (err in POINTLESS_ERRORS) |
|
450 if (message.indexOf(err) >= 0)]; |
|
451 if (pointless.length == 0 && message) |
|
452 testConsole.log(message); |
|
453 } |
|
454 }; |
|
455 |
|
456 function TestRunnerConsole(base, options) { |
|
457 this.__proto__ = { |
|
458 errorsLogged: 0, |
|
459 warn: function warn() { |
|
460 this.errorsLogged++; |
|
461 base.warn.apply(base, arguments); |
|
462 }, |
|
463 error: function error() { |
|
464 this.errorsLogged++; |
|
465 base.error.apply(base, arguments); |
|
466 }, |
|
467 info: function info(first) { |
|
468 if (options.verbose) |
|
469 base.info.apply(base, arguments); |
|
470 else |
|
471 if (first == "pass:") |
|
472 print("."); |
|
473 }, |
|
474 __proto__: base |
|
475 }; |
|
476 } |
|
477 |
|
478 function stringify(arg) { |
|
479 try { |
|
480 return String(arg); |
|
481 } |
|
482 catch(ex) { |
|
483 return "<toString() error>"; |
|
484 } |
|
485 } |
|
486 |
|
487 function stringifyArgs(args) { |
|
488 return Array.map(args, stringify).join(" "); |
|
489 } |
|
490 |
|
491 function TestRunnerTinderboxConsole(base, options) { |
|
492 this.base = base; |
|
493 this.print = options.print; |
|
494 this.verbose = options.verbose; |
|
495 this.errorsLogged = 0; |
|
496 |
|
497 // Binding all the public methods to an instance so that they can be used |
|
498 // as callback / listener functions straightaway. |
|
499 this.log = this.log.bind(this); |
|
500 this.info = this.info.bind(this); |
|
501 this.warn = this.warn.bind(this); |
|
502 this.error = this.error.bind(this); |
|
503 this.debug = this.debug.bind(this); |
|
504 this.exception = this.exception.bind(this); |
|
505 this.trace = this.trace.bind(this); |
|
506 }; |
|
507 |
|
508 TestRunnerTinderboxConsole.prototype = { |
|
509 testMessage: function testMessage(pass, expected, test, message) { |
|
510 let type = "TEST-"; |
|
511 if (expected) { |
|
512 if (pass) |
|
513 type += "PASS"; |
|
514 else |
|
515 type += "KNOWN-FAIL"; |
|
516 } |
|
517 else { |
|
518 this.errorsLogged++; |
|
519 if (pass) |
|
520 type += "UNEXPECTED-PASS"; |
|
521 else |
|
522 type += "UNEXPECTED-FAIL"; |
|
523 } |
|
524 |
|
525 this.print(type + " | " + test + " | " + message + "\n"); |
|
526 if (!expected) |
|
527 this.trace(); |
|
528 }, |
|
529 |
|
530 log: function log() { |
|
531 this.print("TEST-INFO | " + stringifyArgs(arguments) + "\n"); |
|
532 }, |
|
533 |
|
534 info: function info(first) { |
|
535 this.print("TEST-INFO | " + stringifyArgs(arguments) + "\n"); |
|
536 }, |
|
537 |
|
538 warn: function warn() { |
|
539 this.errorsLogged++; |
|
540 this.print("TEST-UNEXPECTED-FAIL | " + stringifyArgs(arguments) + "\n"); |
|
541 }, |
|
542 |
|
543 error: function error() { |
|
544 this.errorsLogged++; |
|
545 this.print("TEST-UNEXPECTED-FAIL | " + stringifyArgs(arguments) + "\n"); |
|
546 this.base.error.apply(this.base, arguments); |
|
547 }, |
|
548 |
|
549 debug: function debug() { |
|
550 this.print("TEST-INFO | " + stringifyArgs(arguments) + "\n"); |
|
551 }, |
|
552 |
|
553 exception: function exception(e) { |
|
554 this.print("An exception occurred.\n" + |
|
555 require("../console/traceback").format(e) + "\n" + e + "\n"); |
|
556 }, |
|
557 |
|
558 trace: function trace() { |
|
559 var traceback = require("../console/traceback"); |
|
560 var stack = traceback.get(); |
|
561 stack.splice(-1, 1); |
|
562 this.print("TEST-INFO | " + stringify(traceback.format(stack)) + "\n"); |
|
563 } |
|
564 }; |
|
565 |
|
566 var runTests = exports.runTests = function runTests(options) { |
|
567 iterationsLeft = options.iterations; |
|
568 profileMemory = options.profileMemory; |
|
569 stopOnError = options.stopOnError; |
|
570 onDone = options.onDone; |
|
571 print = options.print; |
|
572 findAndRunTests = options.findAndRunTests; |
|
573 |
|
574 try { |
|
575 cService.registerListener(consoleListener); |
|
576 print("Running tests on " + system.name + " " + system.version + |
|
577 "/Gecko " + system.platformVersion + " (" + |
|
578 system.id + ") under " + |
|
579 system.platform + "/" + system.architecture + ".\n"); |
|
580 |
|
581 if (options.parseable) |
|
582 testConsole = new TestRunnerTinderboxConsole(new PlainTextConsole(), options); |
|
583 else |
|
584 testConsole = new TestRunnerConsole(new PlainTextConsole(), options); |
|
585 |
|
586 loader = Loader(module, { |
|
587 console: testConsole, |
|
588 global: {} // useful for storing things like coverage testing. |
|
589 }); |
|
590 |
|
591 // Load these before getting initial leak stats as they will still be in |
|
592 // memory when we check later |
|
593 require("../deprecated/unit-test"); |
|
594 require("../deprecated/unit-test-finder"); |
|
595 startLeaks = getPotentialLeaks(); |
|
596 |
|
597 nextIteration(); |
|
598 } catch (e) { |
|
599 let frames = fromException(e).reverse().reduce(function(frames, frame) { |
|
600 if (frame.fileName.split("/").pop() === "unit-test-finder.js") |
|
601 frames.done = true |
|
602 if (!frames.done) frames.push(frame) |
|
603 |
|
604 return frames |
|
605 }, []) |
|
606 |
|
607 let prototype = typeof(e) === "object" ? e.constructor.prototype : |
|
608 Error.prototype; |
|
609 let stack = serializeStack(frames.reverse()); |
|
610 |
|
611 let error = Object.create(prototype, { |
|
612 message: { value: e.message, writable: true, configurable: true }, |
|
613 fileName: { value: e.fileName, writable: true, configurable: true }, |
|
614 lineNumber: { value: e.lineNumber, writable: true, configurable: true }, |
|
615 stack: { value: stack, writable: true, configurable: true }, |
|
616 toString: { value: function() String(e), writable: true, configurable: true }, |
|
617 }); |
|
618 |
|
619 print("Error: " + error + " \n " + format(error)); |
|
620 onDone({passed: 0, failed: 1}); |
|
621 } |
|
622 }; |
|
623 |
|
624 unload(function() { |
|
625 cService.unregisterListener(consoleListener); |
|
626 }); |
|
627 |