michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: "use strict"; michael@0: michael@0: module.metadata = { michael@0: "stability": "experimental" michael@0: }; michael@0: michael@0: const { Cc, Ci, Cu } = require("chrome"); michael@0: const { Loader } = require('./loader'); michael@0: const { serializeStack, parseStack } = require("toolkit/loader"); michael@0: const { setTimeout } = require('../timers'); michael@0: const { PlainTextConsole } = require("../console/plain-text"); michael@0: const { when: unload } = require("../system/unload"); michael@0: const { format, fromException } = require("../console/traceback"); michael@0: const system = require("../system"); michael@0: const memory = require('../deprecated/memory'); michael@0: const { gc: gcPromise } = require('./memory'); michael@0: const { defer } = require('../core/promise'); michael@0: michael@0: // Trick manifest builder to make it think we need these modules ? michael@0: const unit = require("../deprecated/unit-test"); michael@0: const test = require("../../test"); michael@0: const url = require("../url"); michael@0: michael@0: function emptyPromise() { michael@0: let { promise, resolve } = defer(); michael@0: resolve(); michael@0: return promise; michael@0: } michael@0: michael@0: var cService = Cc['@mozilla.org/consoleservice;1'].getService() michael@0: .QueryInterface(Ci.nsIConsoleService); michael@0: michael@0: // The console used to log messages michael@0: var testConsole; michael@0: michael@0: // Cuddlefish loader in which we load and execute tests. michael@0: var loader; michael@0: michael@0: // Function to call when we're done running tests. michael@0: var onDone; michael@0: michael@0: // Function to print text to a console, w/o CR at the end. michael@0: var print; michael@0: michael@0: // How many more times to run all tests. michael@0: var iterationsLeft; michael@0: michael@0: // Whether to report memory profiling information. michael@0: var profileMemory; michael@0: michael@0: // Whether we should stop as soon as a test reports a failure. michael@0: var stopOnError; michael@0: michael@0: // Function to call to retrieve a list of tests to execute michael@0: var findAndRunTests; michael@0: michael@0: // Combined information from all test runs. michael@0: var results = { michael@0: passed: 0, michael@0: failed: 0, michael@0: testRuns: [] michael@0: }; michael@0: michael@0: // A list of the compartments and windows loaded after startup michael@0: var startLeaks; michael@0: michael@0: // JSON serialization of last memory usage stats; we keep it stringified michael@0: // so we don't actually change the memory usage stats (in terms of objects) michael@0: // of the JSRuntime we're profiling. michael@0: var lastMemoryUsage; michael@0: michael@0: function analyzeRawProfilingData(data) { michael@0: var graph = data.graph; michael@0: var shapes = {}; michael@0: michael@0: // Convert keys in the graph from strings to ints. michael@0: // TODO: Can we get rid of this ridiculousness? michael@0: var newGraph = {}; michael@0: for (id in graph) { michael@0: newGraph[parseInt(id)] = graph[id]; michael@0: } michael@0: graph = newGraph; michael@0: michael@0: var modules = 0; michael@0: var moduleIds = []; michael@0: var moduleObjs = {UNKNOWN: 0}; michael@0: for (let name in data.namedObjects) { michael@0: moduleObjs[name] = 0; michael@0: moduleIds[data.namedObjects[name]] = name; michael@0: modules++; michael@0: } michael@0: michael@0: var count = 0; michael@0: for (id in graph) { michael@0: var parent = graph[id].parent; michael@0: while (parent) { michael@0: if (parent in moduleIds) { michael@0: var name = moduleIds[parent]; michael@0: moduleObjs[name]++; michael@0: break; michael@0: } michael@0: if (!(parent in graph)) { michael@0: moduleObjs.UNKNOWN++; michael@0: break; michael@0: } michael@0: parent = graph[parent].parent; michael@0: } michael@0: count++; michael@0: } michael@0: michael@0: print("\nobject count is " + count + " in " + modules + " modules" + michael@0: " (" + data.totalObjectCount + " across entire JS runtime)\n"); michael@0: if (lastMemoryUsage) { michael@0: var last = JSON.parse(lastMemoryUsage); michael@0: var diff = { michael@0: moduleObjs: dictDiff(last.moduleObjs, moduleObjs), michael@0: totalObjectClasses: dictDiff(last.totalObjectClasses, michael@0: data.totalObjectClasses) michael@0: }; michael@0: michael@0: for (let name in diff.moduleObjs) michael@0: print(" " + diff.moduleObjs[name] + " in " + name + "\n"); michael@0: for (let name in diff.totalObjectClasses) michael@0: print(" " + diff.totalObjectClasses[name] + " instances of " + michael@0: name + "\n"); michael@0: } michael@0: lastMemoryUsage = JSON.stringify( michael@0: {moduleObjs: moduleObjs, michael@0: totalObjectClasses: data.totalObjectClasses} michael@0: ); michael@0: } michael@0: michael@0: function dictDiff(last, curr) { michael@0: var diff = {}; michael@0: michael@0: for (let name in last) { michael@0: var result = (curr[name] || 0) - last[name]; michael@0: if (result) michael@0: diff[name] = (result > 0 ? "+" : "") + result; michael@0: } michael@0: for (let name in curr) { michael@0: var result = curr[name] - (last[name] || 0); michael@0: if (result) michael@0: diff[name] = (result > 0 ? "+" : "") + result; michael@0: } michael@0: return diff; michael@0: } michael@0: michael@0: function reportMemoryUsage() { michael@0: if (!profileMemory) { michael@0: return emptyPromise(); michael@0: } michael@0: michael@0: return gcPromise().then((function () { michael@0: var mgr = Cc["@mozilla.org/memory-reporter-manager;1"] michael@0: .getService(Ci.nsIMemoryReporterManager); michael@0: let count = 0; michael@0: function logReporter(process, path, kind, units, amount, description) { michael@0: print(((++count == 1) ? "\n" : "") + description + ": " + amount + "\n"); michael@0: } michael@0: mgr.getReportsForThisProcess(logReporter, null); michael@0: michael@0: var weakrefs = [info.weakref.get() michael@0: for each (info in memory.getObjects())]; michael@0: weakrefs = [weakref for each (weakref in weakrefs) if (weakref)]; michael@0: print("Tracked memory objects in testing sandbox: " + weakrefs.length + "\n"); michael@0: })); michael@0: } michael@0: michael@0: var gWeakrefInfo; michael@0: michael@0: function checkMemory() { michael@0: return gcPromise().then(_ => { michael@0: let leaks = getPotentialLeaks(); michael@0: michael@0: let compartmentURLs = Object.keys(leaks.compartments).filter(function(url) { michael@0: return !(url in startLeaks.compartments); michael@0: }); michael@0: michael@0: let windowURLs = Object.keys(leaks.windows).filter(function(url) { michael@0: return !(url in startLeaks.windows); michael@0: }); michael@0: michael@0: for (let url of compartmentURLs) michael@0: console.warn("LEAKED", leaks.compartments[url]); michael@0: michael@0: for (let url of windowURLs) michael@0: console.warn("LEAKED", leaks.windows[url]); michael@0: }).then(showResults); michael@0: } michael@0: michael@0: function showResults() { michael@0: let { promise, resolve } = defer(); michael@0: michael@0: if (gWeakrefInfo) { michael@0: gWeakrefInfo.forEach( michael@0: function(info) { michael@0: var ref = info.weakref.get(); michael@0: if (ref !== null) { michael@0: var data = ref.__url__ ? ref.__url__ : ref; michael@0: var warning = data == "[object Object]" michael@0: ? "[object " + data.constructor.name + "(" + michael@0: [p for (p in data)].join(", ") + ")]" michael@0: : data; michael@0: console.warn("LEAK", warning, info.bin); michael@0: } michael@0: } michael@0: ); michael@0: } michael@0: michael@0: onDone(results); michael@0: michael@0: resolve(); michael@0: return promise; michael@0: } michael@0: michael@0: function cleanup() { michael@0: let coverObject = {}; michael@0: try { michael@0: for (let name in loader.modules) michael@0: memory.track(loader.modules[name], michael@0: "module global scope: " + name); michael@0: memory.track(loader, "Cuddlefish Loader"); michael@0: michael@0: if (profileMemory) { michael@0: gWeakrefInfo = [{ weakref: info.weakref, bin: info.bin } michael@0: for each (info in memory.getObjects())]; michael@0: } michael@0: michael@0: loader.unload(); michael@0: michael@0: if (loader.globals.console.errorsLogged && !results.failed) { michael@0: results.failed++; michael@0: console.error("warnings and/or errors were logged."); michael@0: } michael@0: michael@0: if (consoleListener.errorsLogged && !results.failed) { michael@0: console.warn(consoleListener.errorsLogged + " " + michael@0: "warnings or errors were logged to the " + michael@0: "platform's nsIConsoleService, which could " + michael@0: "be of no consequence; however, they could also " + michael@0: "be indicative of aberrant behavior."); michael@0: } michael@0: michael@0: // read the code coverage object, if it exists, from CoverJS-moz michael@0: if (typeof loader.globals.global == "object") { michael@0: coverObject = loader.globals.global['__$coverObject'] || {}; michael@0: } michael@0: michael@0: consoleListener.errorsLogged = 0; michael@0: loader = null; michael@0: michael@0: memory.gc(); michael@0: } michael@0: catch (e) { michael@0: results.failed++; michael@0: console.error("unload.send() threw an exception."); michael@0: console.exception(e); michael@0: }; michael@0: michael@0: setTimeout(require('@test/options').checkMemory ? checkMemory : showResults, 1); michael@0: michael@0: // dump the coverobject michael@0: if (Object.keys(coverObject).length){ michael@0: const self = require('sdk/self'); michael@0: const {pathFor} = require("sdk/system"); michael@0: let file = require('sdk/io/file'); michael@0: const {env} = require('sdk/system/environment'); michael@0: console.log("CWD:", env.PWD); michael@0: let out = file.join(env.PWD,'coverstats-'+self.id+'.json'); michael@0: console.log('coverstats:', out); michael@0: let outfh = file.open(out,'w'); michael@0: outfh.write(JSON.stringify(coverObject,null,2)); michael@0: outfh.flush(); michael@0: outfh.close(); michael@0: } michael@0: } michael@0: michael@0: function getPotentialLeaks() { michael@0: memory.gc(); michael@0: michael@0: // Things we can assume are part of the platform and so aren't leaks michael@0: let WHITELIST_BASE_URLS = [ michael@0: "chrome://", michael@0: "resource:///", michael@0: "resource://app/", michael@0: "resource://gre/", michael@0: "resource://gre-resources/", michael@0: "resource://pdf.js/", michael@0: "resource://pdf.js.components/", michael@0: "resource://services-common/", michael@0: "resource://services-crypto/", michael@0: "resource://services-sync/" michael@0: ]; michael@0: michael@0: let ioService = Cc["@mozilla.org/network/io-service;1"]. michael@0: getService(Ci.nsIIOService); michael@0: let uri = ioService.newURI("chrome://global/content/", "UTF-8", null); michael@0: let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"]. michael@0: getService(Ci.nsIChromeRegistry); michael@0: uri = chromeReg.convertChromeURL(uri); michael@0: let spec = uri.spec; michael@0: let pos = spec.indexOf("!/"); michael@0: WHITELIST_BASE_URLS.push(spec.substring(0, pos + 2)); michael@0: michael@0: let zoneRegExp = new RegExp("^explicit/js-non-window/zones/zone[^/]+/compartment\\((.+)\\)"); michael@0: let compartmentRegexp = new RegExp("^explicit/js-non-window/compartments/non-window-global/compartment\\((.+)\\)/"); michael@0: let compartmentDetails = new RegExp("^([^,]+)(?:, (.+?))?(?: \\(from: (.*)\\))?$"); michael@0: let windowRegexp = new RegExp("^explicit/window-objects/top\\((.*)\\)/active"); michael@0: let windowDetails = new RegExp("^(.*), id=.*$"); michael@0: michael@0: function isPossibleLeak(item) { michael@0: if (!item.location) michael@0: return false; michael@0: michael@0: for (let whitelist of WHITELIST_BASE_URLS) { michael@0: if (item.location.substring(0, whitelist.length) == whitelist) michael@0: return false; michael@0: } michael@0: michael@0: return true; michael@0: } michael@0: michael@0: let compartments = {}; michael@0: let windows = {}; michael@0: function logReporter(process, path, kind, units, amount, description) { michael@0: let matches; michael@0: michael@0: if ((matches = compartmentRegexp.exec(path)) || (matches = zoneRegExp.exec(path))) { michael@0: if (matches[1] in compartments) michael@0: return; michael@0: michael@0: let details = compartmentDetails.exec(matches[1]); michael@0: if (!details) { michael@0: console.error("Unable to parse compartment detail " + matches[1]); michael@0: return; michael@0: } michael@0: michael@0: let item = { michael@0: path: matches[1], michael@0: principal: details[1], michael@0: location: details[2] ? details[2].replace("\\", "/", "g") : undefined, michael@0: source: details[3] ? details[3].split(" -> ").reverse() : undefined, michael@0: toString: function() this.location michael@0: }; michael@0: michael@0: if (!isPossibleLeak(item)) michael@0: return; michael@0: michael@0: compartments[matches[1]] = item; michael@0: return; michael@0: } michael@0: michael@0: if (matches = windowRegexp.exec(path)) { michael@0: if (matches[1] in windows) michael@0: return; michael@0: michael@0: let details = windowDetails.exec(matches[1]); michael@0: if (!details) { michael@0: console.error("Unable to parse window detail " + matches[1]); michael@0: return; michael@0: } michael@0: michael@0: let item = { michael@0: path: matches[1], michael@0: location: details[1].replace("\\", "/", "g"), michael@0: source: [details[1].replace("\\", "/", "g")], michael@0: toString: function() this.location michael@0: }; michael@0: michael@0: if (!isPossibleLeak(item)) michael@0: return; michael@0: michael@0: windows[matches[1]] = item; michael@0: } michael@0: } michael@0: michael@0: Cc["@mozilla.org/memory-reporter-manager;1"] michael@0: .getService(Ci.nsIMemoryReporterManager) michael@0: .getReportsForThisProcess(logReporter, null); michael@0: michael@0: return { compartments: compartments, windows: windows }; michael@0: } michael@0: michael@0: function nextIteration(tests) { michael@0: if (tests) { michael@0: results.passed += tests.passed; michael@0: results.failed += tests.failed; michael@0: michael@0: reportMemoryUsage().then(_ => { michael@0: let testRun = []; michael@0: for each (let test in tests.testRunSummary) { michael@0: let testCopy = {}; michael@0: for (let info in test) { michael@0: testCopy[info] = test[info]; michael@0: } michael@0: testRun.push(testCopy); michael@0: } michael@0: michael@0: results.testRuns.push(testRun); michael@0: iterationsLeft--; michael@0: michael@0: checkForEnd(); michael@0: }) michael@0: } michael@0: else { michael@0: checkForEnd(); michael@0: } michael@0: } michael@0: michael@0: function checkForEnd() { michael@0: if (iterationsLeft && (!stopOnError || results.failed == 0)) { michael@0: // Pass the loader which has a hooked console that doesn't dispatch michael@0: // errors to the JS console and avoid firing false alarm in our michael@0: // console listener michael@0: findAndRunTests(loader, nextIteration); michael@0: } michael@0: else { michael@0: setTimeout(cleanup, 0); michael@0: } michael@0: } michael@0: michael@0: var POINTLESS_ERRORS = [ michael@0: 'Invalid chrome URI:', michael@0: 'OpenGL LayerManager Initialized Succesfully.', michael@0: '[JavaScript Error: "TelemetryStopwatch:', michael@0: 'reference to undefined property', michael@0: '[JavaScript Error: "The character encoding of the HTML document was ' + michael@0: 'not declared.', michael@0: '[Javascript Warning: "Error: Failed to preserve wrapper of wrapped ' + michael@0: 'native weak map key', michael@0: '[JavaScript Warning: "Duplicate resource declaration for', michael@0: 'file: "chrome://browser/content/', michael@0: 'file: "chrome://global/content/', michael@0: '[JavaScript Warning: "The character encoding of a framed document was ' + michael@0: 'not declared.' michael@0: ]; michael@0: michael@0: var consoleListener = { michael@0: errorsLogged: 0, michael@0: observe: function(object) { michael@0: if (!(object instanceof Ci.nsIScriptError)) michael@0: return; michael@0: this.errorsLogged++; michael@0: var message = object.QueryInterface(Ci.nsIConsoleMessage).message; michael@0: var pointless = [err for each (err in POINTLESS_ERRORS) michael@0: if (message.indexOf(err) >= 0)]; michael@0: if (pointless.length == 0 && message) michael@0: testConsole.log(message); michael@0: } michael@0: }; michael@0: michael@0: function TestRunnerConsole(base, options) { michael@0: this.__proto__ = { michael@0: errorsLogged: 0, michael@0: warn: function warn() { michael@0: this.errorsLogged++; michael@0: base.warn.apply(base, arguments); michael@0: }, michael@0: error: function error() { michael@0: this.errorsLogged++; michael@0: base.error.apply(base, arguments); michael@0: }, michael@0: info: function info(first) { michael@0: if (options.verbose) michael@0: base.info.apply(base, arguments); michael@0: else michael@0: if (first == "pass:") michael@0: print("."); michael@0: }, michael@0: __proto__: base michael@0: }; michael@0: } michael@0: michael@0: function stringify(arg) { michael@0: try { michael@0: return String(arg); michael@0: } michael@0: catch(ex) { michael@0: return ""; michael@0: } michael@0: } michael@0: michael@0: function stringifyArgs(args) { michael@0: return Array.map(args, stringify).join(" "); michael@0: } michael@0: michael@0: function TestRunnerTinderboxConsole(base, options) { michael@0: this.base = base; michael@0: this.print = options.print; michael@0: this.verbose = options.verbose; michael@0: this.errorsLogged = 0; michael@0: michael@0: // Binding all the public methods to an instance so that they can be used michael@0: // as callback / listener functions straightaway. michael@0: this.log = this.log.bind(this); michael@0: this.info = this.info.bind(this); michael@0: this.warn = this.warn.bind(this); michael@0: this.error = this.error.bind(this); michael@0: this.debug = this.debug.bind(this); michael@0: this.exception = this.exception.bind(this); michael@0: this.trace = this.trace.bind(this); michael@0: }; michael@0: michael@0: TestRunnerTinderboxConsole.prototype = { michael@0: testMessage: function testMessage(pass, expected, test, message) { michael@0: let type = "TEST-"; michael@0: if (expected) { michael@0: if (pass) michael@0: type += "PASS"; michael@0: else michael@0: type += "KNOWN-FAIL"; michael@0: } michael@0: else { michael@0: this.errorsLogged++; michael@0: if (pass) michael@0: type += "UNEXPECTED-PASS"; michael@0: else michael@0: type += "UNEXPECTED-FAIL"; michael@0: } michael@0: michael@0: this.print(type + " | " + test + " | " + message + "\n"); michael@0: if (!expected) michael@0: this.trace(); michael@0: }, michael@0: michael@0: log: function log() { michael@0: this.print("TEST-INFO | " + stringifyArgs(arguments) + "\n"); michael@0: }, michael@0: michael@0: info: function info(first) { michael@0: this.print("TEST-INFO | " + stringifyArgs(arguments) + "\n"); michael@0: }, michael@0: michael@0: warn: function warn() { michael@0: this.errorsLogged++; michael@0: this.print("TEST-UNEXPECTED-FAIL | " + stringifyArgs(arguments) + "\n"); michael@0: }, michael@0: michael@0: error: function error() { michael@0: this.errorsLogged++; michael@0: this.print("TEST-UNEXPECTED-FAIL | " + stringifyArgs(arguments) + "\n"); michael@0: this.base.error.apply(this.base, arguments); michael@0: }, michael@0: michael@0: debug: function debug() { michael@0: this.print("TEST-INFO | " + stringifyArgs(arguments) + "\n"); michael@0: }, michael@0: michael@0: exception: function exception(e) { michael@0: this.print("An exception occurred.\n" + michael@0: require("../console/traceback").format(e) + "\n" + e + "\n"); michael@0: }, michael@0: michael@0: trace: function trace() { michael@0: var traceback = require("../console/traceback"); michael@0: var stack = traceback.get(); michael@0: stack.splice(-1, 1); michael@0: this.print("TEST-INFO | " + stringify(traceback.format(stack)) + "\n"); michael@0: } michael@0: }; michael@0: michael@0: var runTests = exports.runTests = function runTests(options) { michael@0: iterationsLeft = options.iterations; michael@0: profileMemory = options.profileMemory; michael@0: stopOnError = options.stopOnError; michael@0: onDone = options.onDone; michael@0: print = options.print; michael@0: findAndRunTests = options.findAndRunTests; michael@0: michael@0: try { michael@0: cService.registerListener(consoleListener); michael@0: print("Running tests on " + system.name + " " + system.version + michael@0: "/Gecko " + system.platformVersion + " (" + michael@0: system.id + ") under " + michael@0: system.platform + "/" + system.architecture + ".\n"); michael@0: michael@0: if (options.parseable) michael@0: testConsole = new TestRunnerTinderboxConsole(new PlainTextConsole(), options); michael@0: else michael@0: testConsole = new TestRunnerConsole(new PlainTextConsole(), options); michael@0: michael@0: loader = Loader(module, { michael@0: console: testConsole, michael@0: global: {} // useful for storing things like coverage testing. michael@0: }); michael@0: michael@0: // Load these before getting initial leak stats as they will still be in michael@0: // memory when we check later michael@0: require("../deprecated/unit-test"); michael@0: require("../deprecated/unit-test-finder"); michael@0: startLeaks = getPotentialLeaks(); michael@0: michael@0: nextIteration(); michael@0: } catch (e) { michael@0: let frames = fromException(e).reverse().reduce(function(frames, frame) { michael@0: if (frame.fileName.split("/").pop() === "unit-test-finder.js") michael@0: frames.done = true michael@0: if (!frames.done) frames.push(frame) michael@0: michael@0: return frames michael@0: }, []) michael@0: michael@0: let prototype = typeof(e) === "object" ? e.constructor.prototype : michael@0: Error.prototype; michael@0: let stack = serializeStack(frames.reverse()); michael@0: michael@0: let error = Object.create(prototype, { michael@0: message: { value: e.message, writable: true, configurable: true }, michael@0: fileName: { value: e.fileName, writable: true, configurable: true }, michael@0: lineNumber: { value: e.lineNumber, writable: true, configurable: true }, michael@0: stack: { value: stack, writable: true, configurable: true }, michael@0: toString: { value: function() String(e), writable: true, configurable: true }, michael@0: }); michael@0: michael@0: print("Error: " + error + " \n " + format(error)); michael@0: onDone({passed: 0, failed: 1}); michael@0: } michael@0: }; michael@0: michael@0: unload(function() { michael@0: cService.unregisterListener(consoleListener); michael@0: }); michael@0: