summaryrefslogtreecommitdiffstats
path: root/devtools/client/performance/test/unit
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/performance/test/unit')
-rw-r--r--devtools/client/performance/test/unit/.eslintrc.js6
-rw-r--r--devtools/client/performance/test/unit/head.js46
-rw-r--r--devtools/client/performance/test/unit/test_frame-utils-01.js133
-rw-r--r--devtools/client/performance/test/unit/test_frame-utils-02.js59
-rw-r--r--devtools/client/performance/test/unit/test_jit-graph-data.js209
-rw-r--r--devtools/client/performance/test/unit/test_jit-model-01.js120
-rw-r--r--devtools/client/performance/test/unit/test_jit-model-02.js149
-rw-r--r--devtools/client/performance/test/unit/test_marker-blueprint.js29
-rw-r--r--devtools/client/performance/test/unit/test_marker-utils.js115
-rw-r--r--devtools/client/performance/test/unit/test_perf-utils-allocations-to-samples.js96
-rw-r--r--devtools/client/performance/test/unit/test_profiler-categories.js38
-rw-r--r--devtools/client/performance/test/unit/test_tree-model-01.js160
-rw-r--r--devtools/client/performance/test/unit/test_tree-model-02.js62
-rw-r--r--devtools/client/performance/test/unit/test_tree-model-03.js95
-rw-r--r--devtools/client/performance/test/unit/test_tree-model-04.js91
-rw-r--r--devtools/client/performance/test/unit/test_tree-model-05.js82
-rw-r--r--devtools/client/performance/test/unit/test_tree-model-06.js176
-rw-r--r--devtools/client/performance/test/unit/test_tree-model-07.js101
-rw-r--r--devtools/client/performance/test/unit/test_tree-model-08.js99
-rw-r--r--devtools/client/performance/test/unit/test_tree-model-09.js84
-rw-r--r--devtools/client/performance/test/unit/test_tree-model-10.js153
-rw-r--r--devtools/client/performance/test/unit/test_tree-model-11.js90
-rw-r--r--devtools/client/performance/test/unit/test_tree-model-12.js94
-rw-r--r--devtools/client/performance/test/unit/test_tree-model-13.js86
-rw-r--r--devtools/client/performance/test/unit/test_tree-model-allocations-01.js95
-rw-r--r--devtools/client/performance/test/unit/test_tree-model-allocations-02.js105
-rw-r--r--devtools/client/performance/test/unit/test_waterfall-utils-collapse-01.js71
-rw-r--r--devtools/client/performance/test/unit/test_waterfall-utils-collapse-02.js82
-rw-r--r--devtools/client/performance/test/unit/test_waterfall-utils-collapse-03.js64
-rw-r--r--devtools/client/performance/test/unit/test_waterfall-utils-collapse-04.js103
-rw-r--r--devtools/client/performance/test/unit/test_waterfall-utils-collapse-05.js164
-rw-r--r--devtools/client/performance/test/unit/xpcshell.ini36
32 files changed, 3093 insertions, 0 deletions
diff --git a/devtools/client/performance/test/unit/.eslintrc.js b/devtools/client/performance/test/unit/.eslintrc.js
new file mode 100644
index 000000000..aec096a0f
--- /dev/null
+++ b/devtools/client/performance/test/unit/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../../.eslintrc.xpcshell.js"
+};
diff --git a/devtools/client/performance/test/unit/head.js b/devtools/client/performance/test/unit/head.js
new file mode 100644
index 000000000..84128a7e8
--- /dev/null
+++ b/devtools/client/performance/test/unit/head.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+/* exported Cc, Ci, Cu, Cr, Services, console, PLATFORM_DATA_PREF, getFrameNodePath,
+ synthesizeProfileForTest */
+var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+var { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+var Services = require("Services");
+var { console } = require("resource://gre/modules/Console.jsm");
+const RecordingUtils = require("devtools/shared/performance/recording-utils");
+const PLATFORM_DATA_PREF = "devtools.performance.ui.show-platform-data";
+
+/**
+ * Get a path in a FrameNode call tree.
+ */
+function getFrameNodePath(root, path) {
+ let calls = root.calls;
+ let foundNode;
+ for (let key of path.split(" > ")) {
+ foundNode = calls.find((node) => node.key == key);
+ if (!foundNode) {
+ break;
+ }
+ calls = foundNode.calls;
+ }
+ return foundNode;
+}
+
+/**
+ * Synthesize a profile for testing.
+ */
+function synthesizeProfileForTest(samples) {
+ samples.unshift({
+ time: 0,
+ frames: [
+ { location: "(root)" }
+ ]
+ });
+
+ let uniqueStacks = new RecordingUtils.UniqueStacks();
+ return RecordingUtils.deflateThread({
+ samples: samples,
+ markers: []
+ }, uniqueStacks);
+}
diff --git a/devtools/client/performance/test/unit/test_frame-utils-01.js b/devtools/client/performance/test/unit/test_frame-utils-01.js
new file mode 100644
index 000000000..a85ec9282
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_frame-utils-01.js
@@ -0,0 +1,133 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that frame-utils isContent and parseLocation work as intended
+ * when parsing over frames from the profiler.
+ */
+
+const CONTENT_LOCATIONS = [
+ "hello/<.world (https://foo/bar.js:123:987)",
+ "hello/<.world (http://foo/bar.js:123:987)",
+ "hello/<.world (http://foo/bar.js:123)",
+ "hello/<.world (http://foo/bar.js#baz:123:987)",
+ "hello/<.world (http://foo/bar.js?myquery=params&search=1:123:987)",
+ "hello/<.world (http://foo/#bar:123:987)",
+ "hello/<.world (http://foo/:123:987)",
+
+ // Test scripts with port numbers (bug 1164131)
+ "hello/<.world (http://localhost:8888/file.js:100:1)",
+ "hello/<.world (http://localhost:8888/file.js:100)",
+
+ // Eval
+ "hello/<.world (http://localhost:8888/file.js line 65 > eval:1)",
+
+ // Occurs when executing an inline script on a root html page with port
+ // (I've never seen it with a column number but check anyway) bug 1164131
+ "hello/<.world (http://localhost:8888/:1)",
+ "hello/<.world (http://localhost:8888/:100:50)",
+
+ // bug 1197636
+ "Native[\"arraycopy(blah)\"] (http://localhost:8888/profiler.html:4)",
+ "Native[\"arraycopy(blah)\"] (http://localhost:8888/profiler.html:4:5)",
+].map(argify);
+
+const CHROME_LOCATIONS = [
+ { location: "Startup::XRE_InitChildProcess", line: 456, column: 123 },
+ { location: "chrome://browser/content/content.js", line: 456, column: 123 },
+ "setTimeout_timer (resource://gre/foo.js:123:434)",
+ "hello/<.world (jar:file://Users/mcurie/Dev/jetpacks.js)",
+ "hello/<.world (resource://foo.js -> http://bar/baz.js:123:987)",
+ "EnterJIT",
+].map(argify);
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function () {
+ const { computeIsContentAndCategory, parseLocation } = require("devtools/client/performance/modules/logic/frame-utils");
+ let isContent = (frame) => {
+ computeIsContentAndCategory(frame);
+ return frame.isContent;
+ };
+
+ for (let frame of CONTENT_LOCATIONS) {
+ ok(isContent.apply(null, frameify(frame)),
+ `${frame[0]} should be considered a content frame.`);
+ }
+
+ for (let frame of CHROME_LOCATIONS) {
+ ok(!isContent.apply(null, frameify(frame)),
+ `${frame[0]} should not be considered a content frame.`);
+ }
+
+ // functionName, fileName, host, url, line, column
+ const FIELDS = ["functionName", "fileName", "host", "url", "line", "column", "host",
+ "port"];
+
+ /* eslint-disable max-len */
+ const PARSED_CONTENT = [
+ ["hello/<.world", "bar.js", "foo", "https://foo/bar.js", 123, 987, "foo", null],
+ ["hello/<.world", "bar.js", "foo", "http://foo/bar.js", 123, 987, "foo", null],
+ ["hello/<.world", "bar.js", "foo", "http://foo/bar.js", 123, null, "foo", null],
+ ["hello/<.world", "bar.js", "foo", "http://foo/bar.js#baz", 123, 987, "foo", null],
+ ["hello/<.world", "bar.js", "foo", "http://foo/bar.js?myquery=params&search=1", 123, 987, "foo", null],
+ ["hello/<.world", "/", "foo", "http://foo/#bar", 123, 987, "foo", null],
+ ["hello/<.world", "/", "foo", "http://foo/", 123, 987, "foo", null],
+ ["hello/<.world", "file.js", "localhost:8888", "http://localhost:8888/file.js", 100, 1, "localhost:8888", 8888],
+ ["hello/<.world", "file.js", "localhost:8888", "http://localhost:8888/file.js", 100, null, "localhost:8888", 8888],
+ ["hello/<.world", "file.js (eval:1)", "localhost:8888", "http://localhost:8888/file.js", 65, null, "localhost:8888", 8888],
+ ["hello/<.world", "/", "localhost:8888", "http://localhost:8888/", 1, null, "localhost:8888", 8888],
+ ["hello/<.world", "/", "localhost:8888", "http://localhost:8888/", 100, 50, "localhost:8888", 8888],
+ ["Native[\"arraycopy(blah)\"]", "profiler.html", "localhost:8888", "http://localhost:8888/profiler.html", 4, null, "localhost:8888", 8888],
+ ["Native[\"arraycopy(blah)\"]", "profiler.html", "localhost:8888", "http://localhost:8888/profiler.html", 4, 5, "localhost:8888", 8888],
+ ];
+ /* eslint-enable max-len */
+
+ for (let i = 0; i < PARSED_CONTENT.length; i++) {
+ let parsed = parseLocation.apply(null, CONTENT_LOCATIONS[i]);
+ for (let j = 0; j < FIELDS.length; j++) {
+ equal(parsed[FIELDS[j]], PARSED_CONTENT[i][j],
+ `${CONTENT_LOCATIONS[i]} was parsed to correct ${FIELDS[j]}`);
+ }
+ }
+
+ const PARSED_CHROME = [
+ ["Startup::XRE_InitChildProcess", null, null, null, 456, 123, null, null],
+ ["chrome://browser/content/content.js", null, null, null, 456, 123, null, null],
+ ["setTimeout_timer", "foo.js", null, "resource://gre/foo.js", 123, 434, null, null],
+ ["hello/<.world (jar:file://Users/mcurie/Dev/jetpacks.js)", null, null, null,
+ null, null, null, null],
+ ["hello/<.world", "baz.js", "bar", "http://bar/baz.js", 123, 987, "bar", null],
+ ["EnterJIT", null, null, null, null, null, null, null],
+ ];
+
+ for (let i = 0; i < PARSED_CHROME.length; i++) {
+ let parsed = parseLocation.apply(null, CHROME_LOCATIONS[i]);
+ for (let j = 0; j < FIELDS.length; j++) {
+ equal(parsed[FIELDS[j]], PARSED_CHROME[i][j],
+ `${CHROME_LOCATIONS[i]} was parsed to correct ${FIELDS[j]}`);
+ }
+ }
+});
+
+/**
+ * Takes either a string or an object and turns it into an array that
+ * parseLocation.apply expects.
+ */
+function argify(val) {
+ if (typeof val === "string") {
+ return [val];
+ }
+ return [val.location, val.line, val.column];
+}
+
+/**
+ * Takes the result of argify and turns it into an array that can be passed to
+ * isContent.apply.
+ */
+function frameify(val) {
+ return [{ location: val[0] }];
+}
diff --git a/devtools/client/performance/test/unit/test_frame-utils-02.js b/devtools/client/performance/test/unit/test_frame-utils-02.js
new file mode 100644
index 000000000..ef0d275bd
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_frame-utils-02.js
@@ -0,0 +1,59 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests the function testing whether or not a frame is content or chrome
+ * works properly.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function () {
+ let FrameUtils = require("devtools/client/performance/modules/logic/frame-utils");
+
+ let isContent = (frame) => {
+ FrameUtils.computeIsContentAndCategory(frame);
+ return frame.isContent;
+ };
+
+ ok(isContent({ location: "http://foo" }),
+ "Verifying content/chrome frames is working properly.");
+ ok(isContent({ location: "https://foo" }),
+ "Verifying content/chrome frames is working properly.");
+ ok(isContent({ location: "file://foo" }),
+ "Verifying content/chrome frames is working properly.");
+
+ ok(!isContent({ location: "chrome://foo" }),
+ "Verifying content/chrome frames is working properly.");
+ ok(!isContent({ location: "resource://foo" }),
+ "Verifying content/chrome frames is working properly.");
+
+ ok(!isContent({ location: "chrome://foo -> http://bar" }),
+ "Verifying content/chrome frames is working properly.");
+ ok(!isContent({ location: "chrome://foo -> https://bar" }),
+ "Verifying content/chrome frames is working properly.");
+ ok(!isContent({ location: "chrome://foo -> file://bar" }),
+ "Verifying content/chrome frames is working properly.");
+
+ ok(!isContent({ location: "resource://foo -> http://bar" }),
+ "Verifying content/chrome frames is working properly.");
+ ok(!isContent({ location: "resource://foo -> https://bar" }),
+ "Verifying content/chrome frames is working properly.");
+ ok(!isContent({ location: "resource://foo -> file://bar" }),
+ "Verifying content/chrome frames is working properly.");
+
+ ok(!isContent({ category: 1, location: "chrome://foo" }),
+ "Verifying content/chrome frames is working properly.");
+ ok(!isContent({ category: 1, location: "resource://foo" }),
+ "Verifying content/chrome frames is working properly.");
+
+ ok(!isContent({ category: 1, location: "file://foo -> http://bar" }),
+ "Verifying content/chrome frames is working properly.");
+ ok(!isContent({ category: 1, location: "file://foo -> https://bar" }),
+ "Verifying content/chrome frames is working properly.");
+ ok(!isContent({ category: 1, location: "file://foo -> file://bar" }),
+ "Verifying content/chrome frames is working properly.");
+});
diff --git a/devtools/client/performance/test/unit/test_jit-graph-data.js b/devtools/client/performance/test/unit/test_jit-graph-data.js
new file mode 100644
index 000000000..b298f4bcc
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_jit-graph-data.js
@@ -0,0 +1,209 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Unit test for `createTierGraphDataFromFrameNode` function.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+const SAMPLE_COUNT = 1000;
+const RESOLUTION = 50;
+const TIME_PER_SAMPLE = 5;
+
+// Offset needed since ThreadNode requires the first sample to be strictly
+// greater than its start time. This lets us still have pretty numbers
+// in this test to keep it (more) simple, which it sorely needs.
+const TIME_OFFSET = 5;
+
+add_task(function test() {
+ let { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
+ let { createTierGraphDataFromFrameNode } = require("devtools/client/performance/modules/logic/jit");
+
+ // Select the second half of the set of samples
+ let startTime = (SAMPLE_COUNT / 2 * TIME_PER_SAMPLE) - TIME_OFFSET;
+ let endTime = (SAMPLE_COUNT * TIME_PER_SAMPLE) - TIME_OFFSET;
+ let invertTree = true;
+
+ let root = new ThreadNode(gThread, { invertTree, startTime, endTime });
+
+ equal(root.samples, SAMPLE_COUNT / 2,
+ "root has correct amount of samples");
+ equal(root.sampleTimes.length, SAMPLE_COUNT / 2,
+ "root has correct amount of sample times");
+ // Add time offset since the first sample begins TIME_OFFSET after startTime
+ equal(root.sampleTimes[0], startTime + TIME_OFFSET,
+ "root recorded first sample time in scope");
+ equal(root.sampleTimes[root.sampleTimes.length - 1], endTime,
+ "root recorded last sample time in scope");
+
+ let frame = getFrameNodePath(root, "X");
+ let data = createTierGraphDataFromFrameNode(frame, root.sampleTimes,
+ (endTime - startTime) / RESOLUTION);
+
+ let TIME_PER_WINDOW = SAMPLE_COUNT / 2 / RESOLUTION * TIME_PER_SAMPLE;
+
+ // Filter out the dupes created with the same delta so the graph
+ // can render correctly.
+ let filteredData = [];
+ for (let i = 0; i < data.length; i++) {
+ if (!i || data[i].delta !== data[i - 1].delta) {
+ filteredData.push(data[i]);
+ }
+ }
+ data = filteredData;
+
+ for (let i = 0; i < 11; i++) {
+ equal(data[i].delta, startTime + TIME_OFFSET + (TIME_PER_WINDOW * i),
+ "first window has correct x");
+ equal(data[i].values[0], 0.2, "first window has 2 frames in interpreter");
+ equal(data[i].values[1], 0.2, "first window has 2 frames in baseline");
+ equal(data[i].values[2], 0.2, "first window has 2 frames in ion");
+ }
+ // Start on 11, since i===10 is where the values change, and the new value (0,0,0)
+ // is removed in `filteredData`
+ for (let i = 11; i < 20; i++) {
+ equal(data[i].delta, startTime + TIME_OFFSET + (TIME_PER_WINDOW * i),
+ "second window has correct x");
+ equal(data[i].values[0], 0, "second window observed no optimizations");
+ equal(data[i].values[1], 0, "second window observed no optimizations");
+ equal(data[i].values[2], 0, "second window observed no optimizations");
+ }
+ // Start on 21, since i===20 is where the values change, and the new value (0.3,0,0)
+ // is removed in `filteredData`
+ for (let i = 21; i < 30; i++) {
+ equal(data[i].delta, startTime + TIME_OFFSET + (TIME_PER_WINDOW * i),
+ "third window has correct x");
+ equal(data[i].values[0], 0.3, "third window has 3 frames in interpreter");
+ equal(data[i].values[1], 0, "third window has 0 frames in baseline");
+ equal(data[i].values[2], 0, "third window has 0 frames in ion");
+ }
+});
+
+var gUniqueStacks = new RecordingUtils.UniqueStacks();
+
+function uniqStr(s) {
+ return gUniqueStacks.getOrAddStringIndex(s);
+}
+
+const TIER_PATTERNS = [
+ // 0-99
+ ["X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0"],
+ // 100-199
+ ["X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0"],
+ // 200-299
+ ["X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0"],
+ // 300-399
+ ["X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0"],
+ // 400-499
+ ["X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0"],
+
+ // 500-599
+ // Test current frames in all opts
+ ["A", "A", "A", "A", "X_1", "X_2", "X_1", "X_2", "X_0", "X_0"],
+
+ // 600-699
+ // Nothing for current frame
+ ["A", "B", "A", "B", "A", "B", "A", "B", "A", "B"],
+
+ // 700-799
+ // A few frames where the frame is not the leaf node
+ ["X_2 -> Y", "X_2 -> Y", "X_2 -> Y", "X_0", "X_0", "X_0", "A", "A", "A", "A"],
+
+ // 800-899
+ ["X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0"],
+ // 900-999
+ ["X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0", "X_0"],
+];
+
+function createSample(i, frames) {
+ let sample = {};
+ sample.time = i * TIME_PER_SAMPLE;
+ sample.frames = [{ location: "(root)" }];
+ if (i === 0) {
+ return sample;
+ }
+ if (frames) {
+ frames.split(" -> ").forEach(frame => sample.frames.push({ location: frame }));
+ }
+ return sample;
+}
+
+var SAMPLES = (function () {
+ let samples = [];
+
+ for (let i = 0; i < SAMPLE_COUNT;) {
+ let pattern = TIER_PATTERNS[Math.floor(i / 100)];
+ for (let j = 0; j < pattern.length; j++) {
+ samples.push(createSample(i + j, pattern[j]));
+ }
+ i += 10;
+ }
+
+ return samples;
+})();
+
+var gThread = RecordingUtils.deflateThread({ samples: SAMPLES, markers: [] },
+ gUniqueStacks);
+
+var gRawSite1 = {
+ line: 12,
+ column: 2,
+ types: [{
+ mirType: uniqStr("Object"),
+ site: uniqStr("B (http://foo/bar:10)"),
+ typeset: [{
+ keyedBy: uniqStr("constructor"),
+ name: uniqStr("Foo"),
+ location: uniqStr("B (http://foo/bar:10)")
+ }, {
+ keyedBy: uniqStr("primitive"),
+ location: uniqStr("self-hosted")
+ }]
+ }],
+ attempts: {
+ schema: {
+ outcome: 0,
+ strategy: 1
+ },
+ data: [
+ [uniqStr("Failure1"), uniqStr("SomeGetter1")],
+ [uniqStr("Failure2"), uniqStr("SomeGetter2")],
+ [uniqStr("Inlined"), uniqStr("SomeGetter3")]
+ ]
+ }
+};
+
+function serialize(x) {
+ return JSON.parse(JSON.stringify(x));
+}
+
+gThread.frameTable.data.forEach((frame) => {
+ const LOCATION_SLOT = gThread.frameTable.schema.location;
+ const OPTIMIZATIONS_SLOT = gThread.frameTable.schema.optimizations;
+ const IMPLEMENTATION_SLOT = gThread.frameTable.schema.implementation;
+
+ let l = gThread.stringTable[frame[LOCATION_SLOT]];
+ switch (l) {
+ // Rename some of the location sites so we can register different
+ // frames with different opt sites
+ case "X_0":
+ frame[LOCATION_SLOT] = uniqStr("X");
+ frame[OPTIMIZATIONS_SLOT] = serialize(gRawSite1);
+ frame[IMPLEMENTATION_SLOT] = null;
+ break;
+ case "X_1":
+ frame[LOCATION_SLOT] = uniqStr("X");
+ frame[OPTIMIZATIONS_SLOT] = serialize(gRawSite1);
+ frame[IMPLEMENTATION_SLOT] = uniqStr("baseline");
+ break;
+ case "X_2":
+ frame[LOCATION_SLOT] = uniqStr("X");
+ frame[OPTIMIZATIONS_SLOT] = serialize(gRawSite1);
+ frame[IMPLEMENTATION_SLOT] = uniqStr("ion");
+ break;
+ }
+});
diff --git a/devtools/client/performance/test/unit/test_jit-model-01.js b/devtools/client/performance/test/unit/test_jit-model-01.js
new file mode 100644
index 000000000..da50f293c
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_jit-model-01.js
@@ -0,0 +1,120 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that JITOptimizations track optimization sites and create
+ * an OptimizationSiteProfile when adding optimization sites, like from the
+ * FrameNode, and the returning of that data is as expected.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function test() {
+ let { JITOptimizations } = require("devtools/client/performance/modules/logic/jit");
+
+ let rawSites = [];
+ rawSites.push(gRawSite2);
+ rawSites.push(gRawSite2);
+ rawSites.push(gRawSite1);
+ rawSites.push(gRawSite1);
+ rawSites.push(gRawSite2);
+ rawSites.push(gRawSite3);
+
+ let jit = new JITOptimizations(rawSites, gStringTable.stringTable);
+ let sites = jit.optimizationSites;
+
+ let [first, second, third] = sites;
+
+ equal(first.id, 0, "site id is array index");
+ equal(first.samples, 3, "first OptimizationSiteProfile has correct sample count");
+ equal(first.data.line, 34, "includes OptimizationSite as reference under `data`");
+ equal(second.id, 1, "site id is array index");
+ equal(second.samples, 2, "second OptimizationSiteProfile has correct sample count");
+ equal(second.data.line, 12, "includes OptimizationSite as reference under `data`");
+ equal(third.id, 2, "site id is array index");
+ equal(third.samples, 1, "third OptimizationSiteProfile has correct sample count");
+ equal(third.data.line, 78, "includes OptimizationSite as reference under `data`");
+});
+
+var gStringTable = new RecordingUtils.UniqueStrings();
+
+function uniqStr(s) {
+ return gStringTable.getOrAddStringIndex(s);
+}
+
+var gRawSite1 = {
+ line: 12,
+ column: 2,
+ types: [{
+ mirType: uniqStr("Object"),
+ site: uniqStr("A (http://foo/bar/bar:12)"),
+ typeset: [{
+ keyedBy: uniqStr("constructor"),
+ name: uniqStr("Foo"),
+ location: uniqStr("A (http://foo/bar/baz:12)")
+ }, {
+ keyedBy: uniqStr("primitive"),
+ location: uniqStr("self-hosted")
+ }]
+ }],
+ attempts: {
+ schema: {
+ outcome: 0,
+ strategy: 1
+ },
+ data: [
+ [uniqStr("Failure1"), uniqStr("SomeGetter1")],
+ [uniqStr("Failure2"), uniqStr("SomeGetter2")],
+ [uniqStr("Inlined"), uniqStr("SomeGetter3")]
+ ]
+ }
+};
+
+var gRawSite2 = {
+ line: 34,
+ types: [{
+ mirType: uniqStr("Int32"),
+ site: uniqStr("Receiver")
+ }],
+ attempts: {
+ schema: {
+ outcome: 0,
+ strategy: 1
+ },
+ data: [
+ [uniqStr("Failure1"), uniqStr("SomeGetter1")],
+ [uniqStr("Failure2"), uniqStr("SomeGetter2")],
+ [uniqStr("Failure3"), uniqStr("SomeGetter3")]
+ ]
+ }
+};
+
+var gRawSite3 = {
+ line: 78,
+ types: [{
+ mirType: uniqStr("Object"),
+ site: uniqStr("A (http://foo/bar/bar:12)"),
+ typeset: [{
+ keyedBy: uniqStr("constructor"),
+ name: uniqStr("Foo"),
+ location: uniqStr("A (http://foo/bar/baz:12)")
+ }, {
+ keyedBy: uniqStr("primitive"),
+ location: uniqStr("self-hosted")
+ }]
+ }],
+ attempts: {
+ schema: {
+ outcome: 0,
+ strategy: 1
+ },
+ data: [
+ [uniqStr("Failure1"), uniqStr("SomeGetter1")],
+ [uniqStr("Failure2"), uniqStr("SomeGetter2")],
+ [uniqStr("GenericSuccess"), uniqStr("SomeGetter3")]
+ ]
+ }
+};
diff --git a/devtools/client/performance/test/unit/test_jit-model-02.js b/devtools/client/performance/test/unit/test_jit-model-02.js
new file mode 100644
index 000000000..19373e399
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_jit-model-02.js
@@ -0,0 +1,149 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that JITOptimizations create OptimizationSites, and the underlying
+ * hasSuccessfulOutcome/isSuccessfulOutcome work as intended.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function test() {
+ let {
+ JITOptimizations, hasSuccessfulOutcome, isSuccessfulOutcome, SUCCESSFUL_OUTCOMES
+ } = require("devtools/client/performance/modules/logic/jit");
+
+ let rawSites = [];
+ rawSites.push(gRawSite2);
+ rawSites.push(gRawSite2);
+ rawSites.push(gRawSite1);
+ rawSites.push(gRawSite1);
+ rawSites.push(gRawSite2);
+ rawSites.push(gRawSite3);
+
+ let jit = new JITOptimizations(rawSites, gStringTable.stringTable);
+ let sites = jit.optimizationSites;
+
+ let [first, second, third] = sites;
+
+ /* hasSuccessfulOutcome */
+ equal(hasSuccessfulOutcome(first), false,
+ "hasSuccessfulOutcome() returns expected (1)");
+ equal(hasSuccessfulOutcome(second), true,
+ "hasSuccessfulOutcome() returns expected (2)");
+ equal(hasSuccessfulOutcome(third), true,
+ "hasSuccessfulOutcome() returns expected (3)");
+
+ /* .data.attempts */
+ equal(first.data.attempts.length, 2,
+ "optSite.data.attempts has the correct amount of attempts (1)");
+ equal(second.data.attempts.length, 5,
+ "optSite.data.attempts has the correct amount of attempts (2)");
+ equal(third.data.attempts.length, 3,
+ "optSite.data.attempts has the correct amount of attempts (3)");
+
+ /* .data.types */
+ equal(first.data.types.length, 1,
+ "optSite.data.types has the correct amount of IonTypes (1)");
+ equal(second.data.types.length, 2,
+ "optSite.data.types has the correct amount of IonTypes (2)");
+ equal(third.data.types.length, 1,
+ "optSite.data.types has the correct amount of IonTypes (3)");
+
+ /* isSuccessfulOutcome */
+ ok(SUCCESSFUL_OUTCOMES.length, "Have some successful outcomes in SUCCESSFUL_OUTCOMES");
+ SUCCESSFUL_OUTCOMES.forEach(outcome =>
+ ok(isSuccessfulOutcome(outcome),
+ `${outcome} considered a successful outcome via isSuccessfulOutcome()`));
+});
+
+var gStringTable = new RecordingUtils.UniqueStrings();
+
+function uniqStr(s) {
+ return gStringTable.getOrAddStringIndex(s);
+}
+
+var gRawSite1 = {
+ line: 12,
+ column: 2,
+ types: [{
+ mirType: uniqStr("Object"),
+ site: uniqStr("A (http://foo/bar/bar:12)"),
+ typeset: [{
+ keyedBy: uniqStr("constructor"),
+ name: uniqStr("Foo"),
+ location: uniqStr("A (http://foo/bar/baz:12)")
+ }, {
+ keyedBy: uniqStr("constructor"),
+ location: uniqStr("A (http://foo/bar/baz:12)")
+ }]
+ }, {
+ mirType: uniqStr("Int32"),
+ site: uniqStr("A (http://foo/bar/bar:12)"),
+ typeset: [{
+ keyedBy: uniqStr("primitive"),
+ location: uniqStr("self-hosted")
+ }]
+ }],
+ attempts: {
+ schema: {
+ outcome: 0,
+ strategy: 1
+ },
+ data: [
+ [uniqStr("Failure1"), uniqStr("SomeGetter1")],
+ [uniqStr("Failure1"), uniqStr("SomeGetter1")],
+ [uniqStr("Failure1"), uniqStr("SomeGetter1")],
+ [uniqStr("Failure2"), uniqStr("SomeGetter2")],
+ [uniqStr("Inlined"), uniqStr("SomeGetter3")]
+ ]
+ }
+};
+
+var gRawSite2 = {
+ line: 34,
+ types: [{
+ mirType: uniqStr("Int32"),
+ site: uniqStr("Receiver")
+ }],
+ attempts: {
+ schema: {
+ outcome: 0,
+ strategy: 1
+ },
+ data: [
+ [uniqStr("Failure1"), uniqStr("SomeGetter1")],
+ [uniqStr("Failure2"), uniqStr("SomeGetter2")]
+ ]
+ }
+};
+
+var gRawSite3 = {
+ line: 78,
+ types: [{
+ mirType: uniqStr("Object"),
+ site: uniqStr("A (http://foo/bar/bar:12)"),
+ typeset: [{
+ keyedBy: uniqStr("constructor"),
+ name: uniqStr("Foo"),
+ location: uniqStr("A (http://foo/bar/baz:12)")
+ }, {
+ keyedBy: uniqStr("primitive"),
+ location: uniqStr("self-hosted")
+ }]
+ }],
+ attempts: {
+ schema: {
+ outcome: 0,
+ strategy: 1
+ },
+ data: [
+ [uniqStr("Failure1"), uniqStr("SomeGetter1")],
+ [uniqStr("Failure2"), uniqStr("SomeGetter2")],
+ [uniqStr("GenericSuccess"), uniqStr("SomeGetter3")]
+ ]
+ }
+};
diff --git a/devtools/client/performance/test/unit/test_marker-blueprint.js b/devtools/client/performance/test/unit/test_marker-blueprint.js
new file mode 100644
index 000000000..b3db47c0f
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_marker-blueprint.js
@@ -0,0 +1,29 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+/**
+ * Tests if the timeline blueprint has a correct structure.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function () {
+ let { TIMELINE_BLUEPRINT } = require("devtools/client/performance/modules/markers");
+
+ ok(TIMELINE_BLUEPRINT,
+ "A timeline blueprint should be available.");
+
+ ok(Object.keys(TIMELINE_BLUEPRINT).length,
+ "The timeline blueprint has at least one entry.");
+
+ for (let value of Object.values(TIMELINE_BLUEPRINT)) {
+ ok("group" in value,
+ "Each entry in the timeline blueprint contains a `group` key.");
+ ok("colorName" in value,
+ "Each entry in the timeline blueprint contains a `colorName` key.");
+ ok("label" in value,
+ "Each entry in the timeline blueprint contains a `label` key.");
+ }
+});
diff --git a/devtools/client/performance/test/unit/test_marker-utils.js b/devtools/client/performance/test/unit/test_marker-utils.js
new file mode 100644
index 000000000..6fc06efbe
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_marker-utils.js
@@ -0,0 +1,115 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests the marker utils methods.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function () {
+ let { TIMELINE_BLUEPRINT } = require("devtools/client/performance/modules/markers");
+ let { PREFS } = require("devtools/client/performance/modules/global");
+ let { MarkerBlueprintUtils } = require("devtools/client/performance/modules/marker-blueprint-utils");
+
+ PREFS.registerObserver();
+
+ Services.prefs.setBoolPref(PLATFORM_DATA_PREF, false);
+
+ equal(MarkerBlueprintUtils.getMarkerLabel(
+ { name: "DOMEvent" }), "DOM Event",
+ "getMarkerLabel() returns a simple label");
+ equal(MarkerBlueprintUtils.getMarkerLabel(
+ { name: "Javascript", causeName: "setTimeout handler" }), "setTimeout",
+ "getMarkerLabel() returns a label defined via function");
+ equal(MarkerBlueprintUtils.getMarkerLabel(
+ { name: "GarbageCollection", causeName: "ALLOC_TRIGGER" }), "Incremental GC",
+ "getMarkerLabel() returns a label for a function that is generalizable");
+
+ ok(MarkerBlueprintUtils.getMarkerFields({ name: "Paint" }).length === 0,
+ "getMarkerFields() returns an empty array when no fields defined");
+
+ let fields = MarkerBlueprintUtils.getMarkerFields(
+ { name: "ConsoleTime", causeName: "snowstorm" });
+ equal(fields[0].label, "Timer Name:",
+ "getMarkerFields() returns an array with proper label");
+ equal(fields[0].value, "snowstorm",
+ "getMarkerFields() returns an array with proper value");
+
+ fields = MarkerBlueprintUtils.getMarkerFields({ name: "DOMEvent", type: "mouseclick" });
+ equal(fields.length, 1,
+ "getMarkerFields() ignores fields that are not found on marker");
+ equal(fields[0].label, "Event Type:",
+ "getMarkerFields() returns an array with proper label");
+ equal(fields[0].value, "mouseclick",
+ "getMarkerFields() returns an array with proper value");
+
+ fields = MarkerBlueprintUtils.getMarkerFields(
+ { name: "DOMEvent", eventPhase: Ci.nsIDOMEvent.AT_TARGET, type: "mouseclick" });
+ equal(fields.length, 2,
+ "getMarkerFields() returns multiple fields when using a fields function");
+ equal(fields[0].label, "Event Type:",
+ "getMarkerFields() correctly returns fields via function (1)");
+ equal(fields[0].value, "mouseclick",
+ "getMarkerFields() correctly returns fields via function (2)");
+ equal(fields[1].label, "Phase:",
+ "getMarkerFields() correctly returns fields via function (3)");
+ equal(fields[1].value, "Target",
+ "getMarkerFields() correctly returns fields via function (4)");
+
+ fields = MarkerBlueprintUtils.getMarkerFields(
+ { name: "GarbageCollection", causeName: "ALLOC_TRIGGER" });
+ equal(fields[0].value, "Too Many Allocations", "Uses L10N for GC reasons");
+
+ fields = MarkerBlueprintUtils.getMarkerFields(
+ { name: "GarbageCollection", causeName: "NOT_A_GC_REASON" });
+ equal(fields[0].value, "NOT_A_GC_REASON",
+ "Defaults to enum for GC reasons when not L10N'd");
+
+ equal(MarkerBlueprintUtils.getMarkerFields(
+ { name: "Javascript", causeName: "Some Platform Field" })[0].value, "(Gecko)",
+ "Correctly obfuscates JS markers when platform data is off.");
+ Services.prefs.setBoolPref(PLATFORM_DATA_PREF, true);
+ equal(MarkerBlueprintUtils.getMarkerFields(
+ { name: "Javascript", causeName: "Some Platform Field" })[0].value,
+ "Some Platform Field",
+ "Correctly deobfuscates JS markers when platform data is on.");
+
+ equal(MarkerBlueprintUtils.getMarkerGenericName("Javascript"), "Function Call",
+ "getMarkerGenericName() returns correct string when defined via function");
+ equal(MarkerBlueprintUtils.getMarkerGenericName("GarbageCollection"),
+ "Garbage Collection",
+ "getMarkerGenericName() returns correct string when defined via function");
+ equal(MarkerBlueprintUtils.getMarkerGenericName("Reflow"), "Layout",
+ "getMarkerGenericName() returns correct string when defined via string");
+
+ TIMELINE_BLUEPRINT.fakemarker = { group: 0 };
+ try {
+ MarkerBlueprintUtils.getMarkerGenericName("fakemarker");
+ ok(false, "getMarkerGenericName() should throw when no label on blueprint.");
+ } catch (e) {
+ ok(true, "getMarkerGenericName() should throw when no label on blueprint.");
+ }
+
+ TIMELINE_BLUEPRINT.fakemarker = { group: 0, label: () => void 0 };
+ try {
+ MarkerBlueprintUtils.getMarkerGenericName("fakemarker");
+ ok(false,
+ "getMarkerGenericName() should throw when label function returnd undefined.");
+ } catch (e) {
+ ok(true,
+ "getMarkerGenericName() should throw when label function returnd undefined.");
+ }
+
+ delete TIMELINE_BLUEPRINT.fakemarker;
+
+ equal(MarkerBlueprintUtils.getBlueprintFor({ name: "Reflow" }).label, "Layout",
+ "getBlueprintFor() should return marker def for passed in marker.");
+ equal(MarkerBlueprintUtils.getBlueprintFor({ name: "Not sure!" }).label(), "Unknown",
+ "getBlueprintFor() should return a default marker def if the marker is undefined.");
+
+ PREFS.unregisterObserver();
+});
diff --git a/devtools/client/performance/test/unit/test_perf-utils-allocations-to-samples.js b/devtools/client/performance/test/unit/test_perf-utils-allocations-to-samples.js
new file mode 100644
index 000000000..2b114ab82
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_perf-utils-allocations-to-samples.js
@@ -0,0 +1,96 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if allocations data received from the performance actor is properly
+ * converted to something that follows the same structure as the samples data
+ * received from the profiler.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function () {
+ const { getProfileThreadFromAllocations } = require("devtools/shared/performance/recording-utils");
+ let output = getProfileThreadFromAllocations(TEST_DATA);
+ equal(output.toSource(), EXPECTED_OUTPUT.toSource(), "The output is correct.");
+});
+
+var TEST_DATA = {
+ sites: [0, 0, 1, 2, 3],
+ timestamps: [50, 100, 150, 200, 250],
+ sizes: [0, 0, 100, 200, 300],
+ frames: [
+ null, {
+ source: "A",
+ line: 1,
+ column: 2,
+ functionDisplayName: "x",
+ parent: 0
+ }, {
+ source: "B",
+ line: 3,
+ column: 4,
+ functionDisplayName: "y",
+ parent: 1
+ }, {
+ source: "C",
+ line: 5,
+ column: 6,
+ functionDisplayName: null,
+ parent: 2
+ }
+ ]
+};
+
+/* eslint-disable no-inline-comments */
+var EXPECTED_OUTPUT = {
+ name: "allocations",
+ samples: {
+ "schema": {
+ "stack": 0,
+ "time": 1,
+ "size": 2,
+ },
+ data: [
+ [ 1, 150, 100 ],
+ [ 2, 200, 200 ],
+ [ 3, 250, 300 ]
+ ]
+ },
+ stackTable: {
+ "schema": {
+ "prefix": 0,
+ "frame": 1
+ },
+ "data": [
+ null,
+ [ null, 1 ], // x (A:1:2)
+ [ 1, 2 ], // x (A:1:2) > y (B:3:4)
+ [ 2, 3 ] // x (A:1:2) > y (B:3:4) > C:5:6
+ ]
+ },
+ frameTable: {
+ "schema": {
+ "location": 0,
+ "implementation": 1,
+ "optimizations": 2,
+ "line": 3,
+ "category": 4
+ },
+ data: [
+ null,
+ [ 0 ],
+ [ 1 ],
+ [ 2 ]
+ ]
+ },
+ "stringTable": [
+ "x (A:1:2)",
+ "y (B:3:4)",
+ "C:5:6"
+ ],
+};
+/* eslint-enable no-inline-comments */
diff --git a/devtools/client/performance/test/unit/test_profiler-categories.js b/devtools/client/performance/test/unit/test_profiler-categories.js
new file mode 100644
index 000000000..7ba288167
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_profiler-categories.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the profiler categories are mapped correctly.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function () {
+ let { CATEGORIES, CATEGORY_MAPPINGS } = require("devtools/client/performance/modules/categories");
+ let { L10N } = require("devtools/client/performance/modules/global");
+ let count = CATEGORIES.length;
+
+ ok(count,
+ "Should have a non-empty list of categories available.");
+
+ ok(CATEGORIES.some(e => e.color),
+ "All categories have an associated color.");
+
+ ok(CATEGORIES.every(e => e.label),
+ "All categories have an associated label.");
+
+ ok(CATEGORIES.every(e => e.label === L10N.getStr("category." + e.abbrev)),
+ "All categories have a correctly localized label.");
+
+ ok(Object.keys(CATEGORY_MAPPINGS).every(e => (Number(e) >= 9000 && Number(e) <= 9999) ||
+ Number.isInteger(Math.log2(e))),
+ "All bitmask mappings keys are powers of 2, or between 9000-9999 for special " +
+ "categories.");
+
+ ok(Object.keys(CATEGORY_MAPPINGS).every(e => CATEGORIES.indexOf(CATEGORY_MAPPINGS[e])
+ !== -1),
+ "All bitmask mappings point to a category.");
+});
diff --git a/devtools/client/performance/test/unit/test_tree-model-01.js b/devtools/client/performance/test/unit/test_tree-model-01.js
new file mode 100644
index 000000000..cac397795
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_tree-model-01.js
@@ -0,0 +1,160 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if a call tree model can be correctly computed from a samples array.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function test() {
+ const { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
+
+ // Create a root node from a given samples array.
+
+ let threadNode = new ThreadNode(gThread, { startTime: 0, endTime: 20 });
+ let root = getFrameNodePath(threadNode, "(root)");
+
+ // Test the root node.
+
+ equal(threadNode.getInfo().nodeType, "Thread",
+ "The correct node type was retrieved for the root node.");
+
+ equal(threadNode.duration, 20,
+ "The correct duration was calculated for the ThreadNode.");
+ equal(root.getInfo().functionName, "(root)",
+ "The correct function name was retrieved for the root node.");
+ equal(root.getInfo().categoryData.abbrev, "other",
+ "The correct empty category data was retrieved for the root node.");
+
+ equal(root.calls.length, 1,
+ "The correct number of child calls were calculated for the root node.");
+ ok(getFrameNodePath(root, "A"),
+ "The root node's only child call is correct.");
+
+ // Test all the descendant nodes.
+
+ equal(getFrameNodePath(root, "A").calls.length, 2,
+ "The correct number of child calls were calculated for the 'A' node.");
+ ok(getFrameNodePath(root, "A > B"),
+ "The 'A' node has a 'B' child call.");
+ ok(getFrameNodePath(root, "A > E"),
+ "The 'A' node has a 'E' child call.");
+
+ equal(getFrameNodePath(root, "A > B").calls.length, 2,
+ "The correct number of child calls were calculated for the 'A > B' node.");
+ ok(getFrameNodePath(root, "A > B > C"),
+ "The 'A > B' node has a 'C' child call.");
+ ok(getFrameNodePath(root, "A > B > D"),
+ "The 'A > B' node has a 'D' child call.");
+
+ equal(getFrameNodePath(root, "A > E").calls.length, 1,
+ "The correct number of child calls were calculated for the 'A > E' node.");
+ ok(getFrameNodePath(root, "A > E > F"),
+ "The 'A > E' node has a 'F' child call.");
+
+ equal(getFrameNodePath(root, "A > B > C").calls.length, 1,
+ "The correct number of child calls were calculated for the 'A > B > C' node.");
+ ok(getFrameNodePath(root, "A > B > C > D"),
+ "The 'A > B > C' node has a 'D' child call.");
+
+ equal(getFrameNodePath(root, "A > B > C > D").calls.length, 1,
+ "The correct number of child calls were calculated for the 'A > B > C > D' node.");
+ ok(getFrameNodePath(root, "A > B > C > D > E"),
+ "The 'A > B > C > D' node has a 'E' child call.");
+
+ equal(getFrameNodePath(root, "A > B > C > D > E").calls.length, 1,
+ "The correct number of child calls were calculated for the 'A > B > C > D > E' " +
+ "node.");
+ ok(getFrameNodePath(root, "A > B > C > D > E > F"),
+ "The 'A > B > C > D > E' node has a 'F' child call.");
+
+ equal(getFrameNodePath(root, "A > B > C > D > E > F").calls.length, 1,
+ "The correct number of child calls were calculated for the 'A > B > C > D > E > F' " +
+ "node.");
+ ok(getFrameNodePath(root, "A > B > C > D > E > F > G"),
+ "The 'A > B > C > D > E > F' node has a 'G' child call.");
+
+ equal(getFrameNodePath(root, "A > B > C > D > E > F > G").calls.length, 0,
+ "The correct number of child calls were calculated for the " +
+ "'A > B > C > D > E > F > G' node.");
+ equal(getFrameNodePath(root, "A > B > D").calls.length, 0,
+ "The correct number of child calls were calculated for the 'A > B > D' node.");
+ equal(getFrameNodePath(root, "A > E > F").calls.length, 0,
+ "The correct number of child calls were calculated for the 'A > E > F' node.");
+
+ // Check the location, sample times, and samples of the root.
+
+ equal(getFrameNodePath(root, "A").location, "A",
+ "The 'A' node has the correct location.");
+ equal(getFrameNodePath(root, "A").youngestFrameSamples, 0,
+ "The 'A' has correct `youngestFrameSamples`");
+ equal(getFrameNodePath(root, "A").samples, 4,
+ "The 'A' has correct `samples`");
+
+ // A frame that is both a leaf and caught in another stack
+ equal(getFrameNodePath(root, "A > B > C").youngestFrameSamples, 1,
+ "The 'A > B > C' has correct `youngestFrameSamples`");
+ equal(getFrameNodePath(root, "A > B > C").samples, 2,
+ "The 'A > B > C' has correct `samples`");
+
+ // ...and the rightmost leaf.
+
+ equal(getFrameNodePath(root, "A > E > F").location, "F",
+ "The 'A > E > F' node has the correct location.");
+ equal(getFrameNodePath(root, "A > E > F").samples, 1,
+ "The 'A > E > F' node has the correct number of samples.");
+ equal(getFrameNodePath(root, "A > E > F").youngestFrameSamples, 1,
+ "The 'A > E > F' node has the correct number of youngestFrameSamples.");
+
+ // ...and the leftmost leaf.
+
+ equal(getFrameNodePath(root, "A > B > C > D > E > F > G").location, "G",
+ "The 'A > B > C > D > E > F > G' node has the correct location.");
+ equal(getFrameNodePath(root, "A > B > C > D > E > F > G").samples, 1,
+ "The 'A > B > C > D > E > F > G' node has the correct number of samples.");
+ equal(getFrameNodePath(root, "A > B > C > D > E > F > G").youngestFrameSamples, 1,
+ "The 'A > B > C > D > E > F > G' node has the correct number of " +
+ "youngestFrameSamples.");
+});
+
+var gThread = synthesizeProfileForTest([{
+ time: 5,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "C" }
+ ]
+}, {
+ time: 5 + 6,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "D" }
+ ]
+}, {
+ time: 5 + 6 + 7,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "E" },
+ { location: "F" }
+ ]
+}, {
+ time: 20,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "C" },
+ { location: "D" },
+ { location: "E" },
+ { location: "F" },
+ { location: "G" }
+ ]
+}]);
diff --git a/devtools/client/performance/test/unit/test_tree-model-02.js b/devtools/client/performance/test/unit/test_tree-model-02.js
new file mode 100644
index 000000000..2cbff11be
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_tree-model-02.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if a call tree model ignores samples with no timing information.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function test() {
+ let { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
+
+ // Create a root node from a given samples array.
+
+ let thread = new ThreadNode(gThread, { startTime: 0, endTime: 10 });
+ let root = getFrameNodePath(thread, "(root)");
+
+ // Test the ThreadNode, only node with a duration.
+ equal(thread.duration, 10,
+ "The correct duration was calculated for the ThreadNode.");
+
+ equal(root.calls.length, 1,
+ "The correct number of child calls were calculated for the root node.");
+ ok(getFrameNodePath(root, "A"),
+ "The root node's only child call is correct.");
+
+ // Test all the descendant nodes.
+
+ equal(getFrameNodePath(root, "A").calls.length, 1,
+ "The correct number of child calls were calculated for the 'A' node.");
+ ok(getFrameNodePath(root, "A > B"),
+ "The 'A' node's only child call is correct.");
+
+ equal(getFrameNodePath(root, "A > B").calls.length, 1,
+ "The correct number of child calls were calculated for the 'A > B' node.");
+ ok(getFrameNodePath(root, "A > B > C"),
+ "The 'A > B' node's only child call is correct.");
+
+ equal(getFrameNodePath(root, "A > B > C").calls.length, 0,
+ "The correct number of child calls were calculated for the 'A > B > C' node.");
+});
+
+var gThread = synthesizeProfileForTest([{
+ time: 5,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "C" }
+ ]
+}, {
+ time: null,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "D" }
+ ]
+}]);
diff --git a/devtools/client/performance/test/unit/test_tree-model-03.js b/devtools/client/performance/test/unit/test_tree-model-03.js
new file mode 100644
index 000000000..dad90710a
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_tree-model-03.js
@@ -0,0 +1,95 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if a call tree model can be correctly computed from a samples array,
+ * while at the same time filtering by duration.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function test() {
+ let { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
+
+ // Create a root node from a given samples array, filtering by time.
+ //
+ // Filtering from 5 to 18 includes the 2nd and 3rd samples. The 2nd sample
+ // starts exactly on 5 and ends at 11. The 3rd sample starts at 11 and ends
+ // exactly at 18.
+ let startTime = 5;
+ let endTime = 18;
+ let thread = new ThreadNode(gThread, { startTime, endTime });
+ let root = getFrameNodePath(thread, "(root)");
+
+ // Test the root node.
+
+ equal(thread.duration, endTime - startTime,
+ "The correct duration was calculated for the ThreadNode.");
+
+ equal(root.calls.length, 1,
+ "The correct number of child calls were calculated for the root node.");
+ ok(getFrameNodePath(root, "A"),
+ "The root node's only child call is correct.");
+
+ // Test all the descendant nodes.
+
+ equal(getFrameNodePath(root, "A").calls.length, 2,
+ "The correct number of child calls were calculated for the 'A' node.");
+ ok(getFrameNodePath(root, "A > B"),
+ "The 'A' node has a 'B' child call.");
+ ok(getFrameNodePath(root, "A > E"),
+ "The 'A' node has a 'E' child call.");
+
+ equal(getFrameNodePath(root, "A > B").calls.length, 1,
+ "The correct number of child calls were calculated for the 'A > B' node.");
+ ok(getFrameNodePath(root, "A > B > D"),
+ "The 'A > B' node's only child call is correct.");
+
+ equal(getFrameNodePath(root, "A > E").calls.length, 1,
+ "The correct number of child calls were calculated for the 'A > E' node.");
+ ok(getFrameNodePath(root, "A > E > F"),
+ "The 'A > E' node's only child call is correct.");
+
+ equal(getFrameNodePath(root, "A > B > D").calls.length, 0,
+ "The correct number of child calls were calculated for the 'A > B > D' node.");
+ equal(getFrameNodePath(root, "A > E > F").calls.length, 0,
+ "The correct number of child calls were calculated for the 'A > E > F' node.");
+});
+
+var gThread = synthesizeProfileForTest([{
+ time: 5,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "C" }
+ ]
+}, {
+ time: 5 + 6,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "D" }
+ ]
+}, {
+ time: 5 + 6 + 7,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "E" },
+ { location: "F" }
+ ]
+}, {
+ time: 5 + 6 + 7 + 8,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "C" },
+ { location: "D" }
+ ]
+}]);
diff --git a/devtools/client/performance/test/unit/test_tree-model-04.js b/devtools/client/performance/test/unit/test_tree-model-04.js
new file mode 100644
index 000000000..6bf69111e
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_tree-model-04.js
@@ -0,0 +1,91 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if a call tree model can be correctly computed from a samples array,
+ * while at the same time filtering by duration and content-only frames.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function test() {
+ let { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
+
+ // Create a root node from a given samples array, filtering by time.
+
+ let startTime = 5;
+ let endTime = 18;
+ let thread = new ThreadNode(gThread, { startTime, endTime, contentOnly: true });
+ let root = getFrameNodePath(thread, "(root)");
+
+ // Test the ThreadNode, only node which should have duration
+ equal(thread.duration, endTime - startTime,
+ "The correct duration was calculated for the root ThreadNode.");
+
+ equal(root.calls.length, 2,
+ "The correct number of child calls were calculated for the root node.");
+ ok(getFrameNodePath(root, "http://D"),
+ "The root has a 'http://D' child call.");
+ ok(getFrameNodePath(root, "http://A"),
+ "The root has a 'http://A' child call.");
+
+ // Test all the descendant nodes.
+
+ equal(getFrameNodePath(root, "http://A").calls.length, 1,
+ "The correct number of child calls were calculated for the 'http://A' node.");
+ ok(getFrameNodePath(root, "http://A > https://E"),
+ "The 'http://A' node's only child call is correct.");
+
+ equal(getFrameNodePath(root, "http://A > https://E").calls.length, 1,
+ "The correct number of child calls were calculated for the 'http://A > http://E' node.");
+ ok(getFrameNodePath(root, "http://A > https://E > file://F"),
+ "The 'http://A > https://E' node's only child call is correct.");
+
+ equal(getFrameNodePath(root, "http://A > https://E > file://F").calls.length, 1,
+ "The correct number of child calls were calculated for the 'http://A > https://E >> file://F' node.");
+ ok(getFrameNodePath(root, "http://A > https://E > file://F > app://H"),
+ "The 'http://A > https://E >> file://F' node's only child call is correct.");
+
+ equal(getFrameNodePath(root, "http://D").calls.length, 0,
+ "The correct number of child calls were calculated for the 'http://D' node.");
+});
+
+var gThread = synthesizeProfileForTest([{
+ time: 5,
+ frames: [
+ { location: "(root)" },
+ { location: "http://A" },
+ { location: "http://B" },
+ { location: "http://C" }
+ ]
+}, {
+ time: 5 + 6,
+ frames: [
+ { location: "(root)" },
+ { location: "chrome://A" },
+ { location: "resource://B" },
+ { location: "jar:file://G" },
+ { location: "http://D" }
+ ]
+}, {
+ time: 5 + 6 + 7,
+ frames: [
+ { location: "(root)" },
+ { location: "http://A" },
+ { location: "https://E" },
+ { location: "file://F" },
+ { location: "app://H" },
+ ]
+}, {
+ time: 5 + 6 + 7 + 8,
+ frames: [
+ { location: "(root)" },
+ { location: "http://A" },
+ { location: "http://B" },
+ { location: "http://C" },
+ { location: "http://D" }
+ ]
+}]);
diff --git a/devtools/client/performance/test/unit/test_tree-model-05.js b/devtools/client/performance/test/unit/test_tree-model-05.js
new file mode 100644
index 000000000..3b9470798
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_tree-model-05.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if an inverted call tree model can be correctly computed from a samples
+ * array.
+ */
+
+var time = 1;
+
+var gThread = synthesizeProfileForTest([{
+ time: time++,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "C" }
+ ]
+}, {
+ time: time++,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "D" },
+ { location: "C" }
+ ]
+}, {
+ time: time++,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "E" },
+ { location: "C" }
+ ],
+}, {
+ time: time++,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "F" }
+ ]
+}]);
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function test() {
+ let { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
+
+ let root = new ThreadNode(gThread, { invertTree: true, startTime: 0, endTime: 4 });
+
+ equal(root.calls.length, 2,
+ "Should get the 2 youngest frames, not the 1 oldest frame");
+
+ let C = getFrameNodePath(root, "C");
+ ok(C, "Should have C as a child of the root.");
+
+ equal(C.calls.length, 3,
+ "Should have 3 frames that called C.");
+ ok(getFrameNodePath(C, "B"), "B called C.");
+ ok(getFrameNodePath(C, "D"), "D called C.");
+ ok(getFrameNodePath(C, "E"), "E called C.");
+
+ equal(getFrameNodePath(C, "B").calls.length, 1);
+ ok(getFrameNodePath(C, "B > A"), "A called B called C");
+ equal(getFrameNodePath(C, "D").calls.length, 1);
+ ok(getFrameNodePath(C, "D > A"), "A called D called C");
+ equal(getFrameNodePath(C, "E").calls.length, 1);
+ ok(getFrameNodePath(C, "E > A"), "A called E called C");
+
+ let F = getFrameNodePath(root, "F");
+ ok(F, "Should have F as a child of the root.");
+
+ equal(F.calls.length, 1);
+ ok(getFrameNodePath(F, "B"), "B called F");
+
+ equal(getFrameNodePath(F, "B").calls.length, 1);
+ ok(getFrameNodePath(F, "B > A"), "A called B called F");
+});
diff --git a/devtools/client/performance/test/unit/test_tree-model-06.js b/devtools/client/performance/test/unit/test_tree-model-06.js
new file mode 100644
index 000000000..7a678852c
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_tree-model-06.js
@@ -0,0 +1,176 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that when constructing FrameNodes, if optimization data is available,
+ * the FrameNodes have the correct optimization data after iterating over samples,
+ * and only youngest frames capture optimization data.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function test() {
+ let { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
+ let root = getFrameNodePath(new ThreadNode(gThread, { startTime: 0,
+ endTime: 30 }), "(root)");
+
+ let A = getFrameNodePath(root, "A");
+ let B = getFrameNodePath(A, "B");
+ let C = getFrameNodePath(B, "C");
+ let Aopts = A.getOptimizations();
+ let Bopts = B.getOptimizations();
+ let Copts = C.getOptimizations();
+
+ ok(!Aopts, "A() was never youngest frame, so should not have optimization data");
+
+ equal(Bopts.length, 2, "B() only has optimization data when it was a youngest frame");
+
+ // Check a few properties on the OptimizationSites.
+ let optSitesObserved = new Set();
+ for (let opt of Bopts) {
+ if (opt.data.line === 12) {
+ equal(opt.samples, 2, "Correct amount of samples for B()'s first opt site");
+ equal(opt.data.attempts.length, 3, "First opt site has 3 attempts");
+ equal(opt.data.attempts[0].strategy, "SomeGetter1", "inflated strategy name");
+ equal(opt.data.attempts[0].outcome, "Failure1", "inflated outcome name");
+ equal(opt.data.types[0].typeset[0].keyedBy, "constructor", "inflates type info");
+ optSitesObserved.add("first");
+ } else {
+ equal(opt.samples, 1, "Correct amount of samples for B()'s second opt site");
+ optSitesObserved.add("second");
+ }
+ }
+
+ ok(optSitesObserved.has("first"), "first opt site for B() was checked");
+ ok(optSitesObserved.has("second"), "second opt site for B() was checked");
+
+ equal(Copts.length, 1, "C() always youngest frame, so has optimization data");
+});
+
+var gUniqueStacks = new RecordingUtils.UniqueStacks();
+
+function uniqStr(s) {
+ return gUniqueStacks.getOrAddStringIndex(s);
+}
+
+var gThread = RecordingUtils.deflateThread({
+ samples: [{
+ time: 0,
+ frames: [
+ { location: "(root)" }
+ ]
+ }, {
+ time: 10,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B_LEAF_1" }
+ ]
+ }, {
+ time: 15,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B_NOTLEAF" },
+ { location: "C" },
+ ]
+ }, {
+ time: 20,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B_LEAF_2" }
+ ]
+ }, {
+ time: 25,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B_LEAF_2" }
+ ]
+ }],
+ markers: []
+}, gUniqueStacks);
+
+var gRawSite1 = {
+ line: 12,
+ column: 2,
+ types: [{
+ mirType: uniqStr("Object"),
+ site: uniqStr("B (http://foo/bar:10)"),
+ typeset: [{
+ keyedBy: uniqStr("constructor"),
+ name: uniqStr("Foo"),
+ location: uniqStr("B (http://foo/bar:10)")
+ }, {
+ keyedBy: uniqStr("primitive"),
+ location: uniqStr("self-hosted")
+ }]
+ }],
+ attempts: {
+ schema: {
+ outcome: 0,
+ strategy: 1
+ },
+ data: [
+ [uniqStr("Failure1"), uniqStr("SomeGetter1")],
+ [uniqStr("Failure2"), uniqStr("SomeGetter2")],
+ [uniqStr("Inlined"), uniqStr("SomeGetter3")]
+ ]
+ }
+};
+
+var gRawSite2 = {
+ line: 22,
+ types: [{
+ mirType: uniqStr("Int32"),
+ site: uniqStr("Receiver")
+ }],
+ attempts: {
+ schema: {
+ outcome: 0,
+ strategy: 1
+ },
+ data: [
+ [uniqStr("Failure1"), uniqStr("SomeGetter1")],
+ [uniqStr("Failure2"), uniqStr("SomeGetter2")],
+ [uniqStr("Failure3"), uniqStr("SomeGetter3")]
+ ]
+ }
+};
+
+function serialize(x) {
+ return JSON.parse(JSON.stringify(x));
+}
+
+gThread.frameTable.data.forEach((frame) => {
+ const LOCATION_SLOT = gThread.frameTable.schema.location;
+ const OPTIMIZATIONS_SLOT = gThread.frameTable.schema.optimizations;
+
+ let l = gThread.stringTable[frame[LOCATION_SLOT]];
+ switch (l) {
+ case "A":
+ frame[OPTIMIZATIONS_SLOT] = serialize(gRawSite1);
+ break;
+ // Rename some of the location sites so we can register different
+ // frames with different opt sites
+ case "B_LEAF_1":
+ frame[OPTIMIZATIONS_SLOT] = serialize(gRawSite2);
+ frame[LOCATION_SLOT] = uniqStr("B");
+ break;
+ case "B_LEAF_2":
+ frame[OPTIMIZATIONS_SLOT] = serialize(gRawSite1);
+ frame[LOCATION_SLOT] = uniqStr("B");
+ break;
+ case "B_NOTLEAF":
+ frame[OPTIMIZATIONS_SLOT] = serialize(gRawSite1);
+ frame[LOCATION_SLOT] = uniqStr("B");
+ break;
+ case "C":
+ frame[OPTIMIZATIONS_SLOT] = serialize(gRawSite1);
+ break;
+ }
+});
diff --git a/devtools/client/performance/test/unit/test_tree-model-07.js b/devtools/client/performance/test/unit/test_tree-model-07.js
new file mode 100644
index 000000000..2ea08c5ca
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_tree-model-07.js
@@ -0,0 +1,101 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that when displaying only content nodes, platform nodes are generalized.
+ */
+
+var { CATEGORY_MASK } = require("devtools/client/performance/modules/categories");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function test() {
+ let { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
+ let url = (n) => `http://content/${n}`;
+
+ // Create a root node from a given samples array.
+
+ let root = getFrameNodePath(new ThreadNode(gThread, { startTime: 5, endTime: 30,
+ contentOnly: true }), "(root)");
+
+ /*
+ * should have a tree like:
+ * root
+ * - (JS)
+ * - A
+ * - (GC)
+ * - B
+ * - C
+ * - D
+ * - E
+ * - F
+ * - (JS)
+ */
+
+ // Test the root node.
+
+ equal(root.calls.length, 2, "root has 2 children");
+ ok(getFrameNodePath(root, url("A")), "root has content child");
+ ok(getFrameNodePath(root, "64"), "root has platform generalized child");
+ equal(getFrameNodePath(root, "64").calls.length, 0,
+ "platform generalized child is a leaf.");
+
+ ok(getFrameNodePath(root, `${url("A")} > 128`),
+ "A has platform generalized child of another type");
+ equal(getFrameNodePath(root, `${url("A")} > 128`).calls.length, 0,
+ "second generalized type is a leaf.");
+
+ ok(getFrameNodePath(root, `${url("A")} > ${url("E")} > ${url("F")} > 64`),
+ "a second leaf of the first generalized type exists deep in the tree.");
+ ok(getFrameNodePath(root, `${url("A")} > 128`),
+ "A has platform generalized child of another type");
+
+ equal(getFrameNodePath(root, "64").category,
+ getFrameNodePath(root, `${url("A")} > ${url("E")} > ${url("F")} > 64`).category,
+ "generalized frames of same type are duplicated in top-down view");
+});
+
+var gThread = synthesizeProfileForTest([{
+ time: 5,
+ frames: [
+ { location: "(root)" },
+ { location: "http://content/A" },
+ { location: "http://content/B" },
+ { location: "http://content/C" }
+ ]
+}, {
+ time: 5 + 6,
+ frames: [
+ { location: "(root)" },
+ { location: "http://content/A" },
+ { location: "http://content/B" },
+ { location: "contentY", category: CATEGORY_MASK("css") },
+ { location: "http://content/D" }
+ ]
+}, {
+ time: 5 + 6 + 7,
+ frames: [
+ { location: "(root)" },
+ { location: "http://content/A" },
+ { location: "contentY", category: CATEGORY_MASK("css") },
+ { location: "http://content/E" },
+ { location: "http://content/F" },
+ { location: "contentY", category: CATEGORY_MASK("js") },
+ ]
+}, {
+ time: 5 + 20,
+ frames: [
+ { location: "(root)" },
+ { location: "contentX", category: CATEGORY_MASK("js") },
+ ]
+}, {
+ time: 5 + 25,
+ frames: [
+ { location: "(root)" },
+ { location: "http://content/A" },
+ { location: "contentZ", category: CATEGORY_MASK("gc", 1) },
+ ]
+}]);
diff --git a/devtools/client/performance/test/unit/test_tree-model-08.js b/devtools/client/performance/test/unit/test_tree-model-08.js
new file mode 100644
index 000000000..59f7e0d34
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_tree-model-08.js
@@ -0,0 +1,99 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Verifies if FrameNodes retain and parse their data appropriately.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function test() {
+ let FrameUtils = require("devtools/client/performance/modules/logic/frame-utils");
+ let { FrameNode } = require("devtools/client/performance/modules/logic/tree-model");
+ let { CATEGORY_MASK } = require("devtools/client/performance/modules/categories");
+ let compute = frame => {
+ FrameUtils.computeIsContentAndCategory(frame);
+ return frame;
+ };
+
+ let frames = [
+ new FrameNode("hello/<.world (http://foo/bar.js:123:987)", compute({
+ location: "hello/<.world (http://foo/bar.js:123:987)",
+ line: 456,
+ }), false),
+ new FrameNode("hello/<.world (http://foo/bar.js#baz:123:987)", compute({
+ location: "hello/<.world (http://foo/bar.js#baz:123:987)",
+ line: 456,
+ }), false),
+ new FrameNode("hello/<.world (http://foo/#bar:123:987)", compute({
+ location: "hello/<.world (http://foo/#bar:123:987)",
+ line: 456,
+ }), false),
+ new FrameNode("hello/<.world (http://foo/:123:987)", compute({
+ location: "hello/<.world (http://foo/:123:987)",
+ line: 456,
+ }), false),
+ new FrameNode("hello/<.world (resource://foo.js -> http://bar/baz.js:123:987)", compute({
+ location: "hello/<.world (resource://foo.js -> http://bar/baz.js:123:987)",
+ line: 456,
+ }), false),
+ new FrameNode("Foo::Bar::Baz", compute({
+ location: "Foo::Bar::Baz",
+ line: 456,
+ category: CATEGORY_MASK("other"),
+ }), false),
+ new FrameNode("EnterJIT", compute({
+ location: "EnterJIT",
+ }), false),
+ new FrameNode("chrome://browser/content/content.js", compute({
+ location: "chrome://browser/content/content.js",
+ line: 456,
+ column: 123
+ }), false),
+ new FrameNode("hello/<.world (resource://gre/foo.js:123:434)", compute({
+ location: "hello/<.world (resource://gre/foo.js:123:434)",
+ line: 456
+ }), false),
+ new FrameNode("main (http://localhost:8888/file.js:123:987)", compute({
+ location: "main (http://localhost:8888/file.js:123:987)",
+ line: 123,
+ }), false),
+ new FrameNode("main (resource://devtools/timeline.js:123)", compute({
+ location: "main (resource://devtools/timeline.js:123)",
+ }), false),
+ ];
+
+ let fields = ["nodeType", "functionName", "fileName", "host", "url", "line", "column",
+ "categoryData.abbrev", "isContent", "port"];
+ let expected = [
+ // nodeType, functionName, fileName, host, url, line, column, categoryData.abbrev,
+ // isContent, port
+ ["Frame", "hello/<.world", "bar.js", "foo", "http://foo/bar.js", 123, 987, void 0, true],
+ ["Frame", "hello/<.world", "bar.js", "foo", "http://foo/bar.js#baz", 123, 987, void 0, true],
+ ["Frame", "hello/<.world", "/", "foo", "http://foo/#bar", 123, 987, void 0, true],
+ ["Frame", "hello/<.world", "/", "foo", "http://foo/", 123, 987, void 0, true],
+ ["Frame", "hello/<.world", "baz.js", "bar", "http://bar/baz.js", 123, 987, "other", false],
+ ["Frame", "Foo::Bar::Baz", null, null, null, 456, void 0, "other", false],
+ ["Frame", "EnterJIT", null, null, null, null, null, "js", false],
+ ["Frame", "chrome://browser/content/content.js", null, null, null, 456, null, "other", false],
+ ["Frame", "hello/<.world", "foo.js", null, "resource://gre/foo.js", 123, 434, "other", false],
+ ["Frame", "main", "file.js", "localhost:8888", "http://localhost:8888/file.js", 123, 987, null, true, 8888],
+ ["Frame", "main", "timeline.js", null, "resource://devtools/timeline.js", 123, null, "tools", false]
+ ];
+
+ for (let i = 0; i < frames.length; i++) {
+ let info = frames[i].getInfo();
+ let expect = expected[i];
+
+ for (let j = 0; j < fields.length; j++) {
+ let field = fields[j];
+ let value = field === "categoryData.abbrev"
+ ? info.categoryData.abbrev
+ : info[field];
+ equal(value, expect[j], `${field} for frame #${i} is correct: ${expect[j]}`);
+ }
+ }
+});
diff --git a/devtools/client/performance/test/unit/test_tree-model-09.js b/devtools/client/performance/test/unit/test_tree-model-09.js
new file mode 100644
index 000000000..1bf267227
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_tree-model-09.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that when displaying only content nodes, platform nodes are generalized.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function test() {
+ let { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
+ let url = (n) => `http://content/${n}`;
+
+ // Create a root node from a given samples array.
+
+ let root = getFrameNodePath(new ThreadNode(gThread, { startTime: 5, endTime: 25,
+ contentOnly: true }), "(root)");
+
+ /*
+ * should have a tree like:
+ * root
+ * - (Tools)
+ * - A
+ * - B
+ * - C
+ * - D
+ * - E
+ * - F
+ * - (Tools)
+ */
+
+ // Test the root node.
+
+ equal(root.calls.length, 2, "root has 2 children");
+ ok(getFrameNodePath(root, url("A")), "root has content child");
+ ok(getFrameNodePath(root, "9000"),
+ "root has platform generalized child from Chrome JS");
+ equal(getFrameNodePath(root, "9000").calls.length, 0,
+ "platform generalized child is a leaf.");
+
+ ok(getFrameNodePath(root, `${url("A")} > ${url("E")} > ${url("F")} > 9000`),
+ "a second leaf of the generalized Chrome JS exists.");
+
+ equal(getFrameNodePath(root, "9000").category,
+ getFrameNodePath(root, `${url("A")} > ${url("E")} > ${url("F")} > 9000`).category,
+ "generalized frames of same type are duplicated in top-down view");
+});
+
+var gThread = synthesizeProfileForTest([{
+ time: 5,
+ frames: [
+ { location: "(root)" },
+ { location: "http://content/A" },
+ { location: "http://content/B" },
+ { location: "http://content/C" }
+ ]
+}, {
+ time: 5 + 6,
+ frames: [
+ { location: "(root)" },
+ { location: "http://content/A" },
+ { location: "http://content/B" },
+ { location: "fn (resource://loader.js -> resource://devtools/timeline.js)" },
+ { location: "http://content/D" }
+ ]
+}, {
+ time: 5 + 6 + 7,
+ frames: [
+ { location: "(root)" },
+ { location: "http://content/A" },
+ { location: "http://content/E" },
+ { location: "http://content/F" },
+ { location: "fn (resource://loader.js -> resource://devtools/promise.js)" }
+ ]
+}, {
+ time: 5 + 20,
+ frames: [
+ { location: "(root)" },
+ { location: "somefn (resource://loader.js -> resource://devtools/framerate.js)" }
+ ]
+}]);
diff --git a/devtools/client/performance/test/unit/test_tree-model-10.js b/devtools/client/performance/test/unit/test_tree-model-10.js
new file mode 100644
index 000000000..9553c7052
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_tree-model-10.js
@@ -0,0 +1,153 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the tree model calculates correct costs/percentages for
+ * frame nodes. The model-only version of browser_profiler-tree-view-10.js
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function () {
+ let { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
+ let thread = new ThreadNode(gThread, { invertTree: true, startTime: 0, endTime: 50 });
+
+ /**
+ * Samples
+ *
+ * A->C
+ * A->B
+ * A->B->C x4
+ * A->B->D x4
+ *
+ * Expected Tree
+ * +--total--+--self--+--tree-------------+
+ * | 50% | 50% | C
+ * | 40% | 0 | -> B
+ * | 30% | 0 | -> A
+ * | 10% | 0 | -> A
+ *
+ * | 40% | 40% | D
+ * | 40% | 0 | -> B
+ * | 40% | 0 | -> A
+ *
+ * | 10% | 10% | B
+ * | 10% | 0 | -> A
+ */
+
+ [
+ // total, self, name
+ [ 50, 50, "C", [
+ [ 40, 0, "B", [
+ [ 30, 0, "A"]
+ ]],
+ [ 10, 0, "A"]
+ ]],
+ [ 40, 40, "D", [
+ [ 40, 0, "B", [
+ [ 40, 0, "A"],
+ ]]
+ ]],
+ [ 10, 10, "B", [
+ [ 10, 0, "A"],
+ ]]
+ ].forEach(compareFrameInfo(thread));
+});
+
+function compareFrameInfo(root, parent) {
+ parent = parent || root;
+ return function (def) {
+ let [total, self, name, children] = def;
+ let node = getFrameNodePath(parent, name);
+ let data = node.getInfo({ root });
+ equal(total, data.totalPercentage,
+ `${name} has correct total percentage: ${data.totalPercentage}`);
+ equal(self, data.selfPercentage,
+ `${name} has correct self percentage: ${data.selfPercentage}`);
+ if (children) {
+ children.forEach(compareFrameInfo(root, node));
+ }
+ };
+}
+
+var gThread = synthesizeProfileForTest([{
+ time: 5,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "C" }
+ ]
+}, {
+ time: 10,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "D" }
+ ]
+}, {
+ time: 15,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "C" },
+ ]
+}, {
+ time: 20,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ ]
+}, {
+ time: 25,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "C" }
+ ]
+}, {
+ time: 30,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "C" }
+ ]
+}, {
+ time: 35,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "D" }
+ ]
+}, {
+ time: 40,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "D" }
+ ]
+}, {
+ time: 45,
+ frames: [
+ { location: "(root)" },
+ { location: "B" },
+ { location: "C" }
+ ]
+}, {
+ time: 50,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "D" }
+ ]
+}]);
diff --git a/devtools/client/performance/test/unit/test_tree-model-11.js b/devtools/client/performance/test/unit/test_tree-model-11.js
new file mode 100644
index 000000000..c665dfe32
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_tree-model-11.js
@@ -0,0 +1,90 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the costs for recursive frames does not overcount the collapsed
+ * samples.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function () {
+ let { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
+ let thread = new ThreadNode(gThread, { startTime: 0, endTime: 50,
+ flattenRecursion: true });
+
+ /**
+ * Samples
+ *
+ * A->B->C
+ * A->B->B->B->C
+ * A->B
+ * A->B->B->B
+ */
+
+ [
+ // total, self, name
+ [ 100, 0, "(root)", [
+ [ 100, 0, "A", [
+ [ 100, 50, "B", [
+ [ 50, 50, "C"]
+ ]]
+ ]],
+ ]],
+ ].forEach(compareFrameInfo(thread));
+});
+
+function compareFrameInfo(root, parent) {
+ parent = parent || root;
+ return function (def) {
+ let [total, self, name, children] = def;
+ let node = getFrameNodePath(parent, name);
+ let data = node.getInfo({ root });
+ equal(total, data.totalPercentage,
+ `${name} has correct total percentage: ${data.totalPercentage}`);
+ equal(self, data.selfPercentage,
+ `${name} has correct self percentage: ${data.selfPercentage}`);
+ if (children) {
+ children.forEach(compareFrameInfo(root, node));
+ }
+ };
+}
+
+var gThread = synthesizeProfileForTest([{
+ time: 5,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "B" },
+ { location: "B" },
+ { location: "C" }
+ ]
+}, {
+ time: 10,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "C" }
+ ]
+}, {
+ time: 15,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ { location: "B" },
+ { location: "B" },
+ ]
+}, {
+ time: 20,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ ]
+}]);
diff --git a/devtools/client/performance/test/unit/test_tree-model-12.js b/devtools/client/performance/test/unit/test_tree-model-12.js
new file mode 100644
index 000000000..fde96e349
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_tree-model-12.js
@@ -0,0 +1,94 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that uninverting the call tree works correctly when there are stacks
+// in the profile that prefixes of other stacks.
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function () {
+ let { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
+ let thread = new ThreadNode(gThread, { startTime: 0, endTime: 50 });
+ let root = getFrameNodePath(thread, "(root)");
+
+ /**
+ * Samples
+ *
+ * A->B
+ * C->B
+ * B
+ * A
+ * Z->Y->X
+ * W->Y->X
+ * Y->X
+ */
+
+ equal(getFrameNodePath(root, "A > B").youngestFrameSamples, 1,
+ "A > B has the correct self count");
+ equal(getFrameNodePath(root, "C > B").youngestFrameSamples, 1,
+ "C > B has the correct self count");
+ equal(getFrameNodePath(root, "B").youngestFrameSamples, 1,
+ "B has the correct self count");
+ equal(getFrameNodePath(root, "A").youngestFrameSamples, 1,
+ "A has the correct self count");
+ equal(getFrameNodePath(root, "Z > Y > X").youngestFrameSamples, 1,
+ "Z > Y > X has the correct self count");
+ equal(getFrameNodePath(root, "W > Y > X").youngestFrameSamples, 1,
+ "W > Y > X has the correct self count");
+ equal(getFrameNodePath(root, "Y > X").youngestFrameSamples, 1,
+ "Y > X has the correct self count");
+});
+
+var gThread = synthesizeProfileForTest([{
+ time: 5,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ ]
+}, {
+ time: 10,
+ frames: [
+ { location: "(root)" },
+ { location: "C" },
+ { location: "B" },
+ ]
+}, {
+ time: 15,
+ frames: [
+ { location: "(root)" },
+ { location: "B" },
+ ]
+}, {
+ time: 20,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ ]
+}, {
+ time: 21,
+ frames: [
+ { location: "(root)" },
+ { location: "Z" },
+ { location: "Y" },
+ { location: "X" },
+ ]
+}, {
+ time: 22,
+ frames: [
+ { location: "(root)" },
+ { location: "W" },
+ { location: "Y" },
+ { location: "X" },
+ ]
+}, {
+ time: 23,
+ frames: [
+ { location: "(root)" },
+ { location: "Y" },
+ { location: "X" },
+ ]
+}]);
diff --git a/devtools/client/performance/test/unit/test_tree-model-13.js b/devtools/client/performance/test/unit/test_tree-model-13.js
new file mode 100644
index 000000000..a1aa666f1
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_tree-model-13.js
@@ -0,0 +1,86 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Like test_tree-model-12, but inverted.
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function () {
+ let { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
+ let root = new ThreadNode(gThread, { invertTree: true, startTime: 0, endTime: 50 });
+
+ /**
+ * Samples
+ *
+ * A->B
+ * C->B
+ * B
+ * A
+ * Z->Y->X
+ * W->Y->X
+ * Y->X
+ */
+
+ equal(getFrameNodePath(root, "B").youngestFrameSamples, 3,
+ "B has the correct self count");
+ equal(getFrameNodePath(root, "A").youngestFrameSamples, 1,
+ "A has the correct self count");
+ equal(getFrameNodePath(root, "X").youngestFrameSamples, 3,
+ "X has the correct self count");
+ equal(getFrameNodePath(root, "X > Y").samples, 3,
+ "X > Y has the correct total count");
+});
+
+var gThread = synthesizeProfileForTest([{
+ time: 5,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ { location: "B" },
+ ]
+}, {
+ time: 10,
+ frames: [
+ { location: "(root)" },
+ { location: "C" },
+ { location: "B" },
+ ]
+}, {
+ time: 15,
+ frames: [
+ { location: "(root)" },
+ { location: "B" },
+ ]
+}, {
+ time: 20,
+ frames: [
+ { location: "(root)" },
+ { location: "A" },
+ ]
+}, {
+ time: 21,
+ frames: [
+ { location: "(root)" },
+ { location: "Z" },
+ { location: "Y" },
+ { location: "X" },
+ ]
+}, {
+ time: 22,
+ frames: [
+ { location: "(root)" },
+ { location: "W" },
+ { location: "Y" },
+ { location: "X" },
+ ]
+}, {
+ time: 23,
+ frames: [
+ { location: "(root)" },
+ { location: "Y" },
+ { location: "X" },
+ ]
+}]);
diff --git a/devtools/client/performance/test/unit/test_tree-model-allocations-01.js b/devtools/client/performance/test/unit/test_tree-model-allocations-01.js
new file mode 100644
index 000000000..331a625f9
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_tree-model-allocations-01.js
@@ -0,0 +1,95 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+/**
+ * Tests that the tree model calculates correct costs/percentages for
+ * allocation frame nodes.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function () {
+ let { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
+ const { getProfileThreadFromAllocations } = require("devtools/shared/performance/recording-utils");
+ let allocationData = getProfileThreadFromAllocations(TEST_DATA);
+ let thread = new ThreadNode(allocationData, { startTime: 0, endTime: 1000 });
+
+ /* eslint-disable max-len */
+ /**
+ * Values are in order according to:
+ * +-------------+------------+-------------+-------------+------------------------------+
+ * | Self Bytes | Self Count | Total Bytes | Total Count | Function |
+ * +-------------+------------+-------------+-------------+------------------------------+
+ * | 1790272 41% | 8307 17% | 1790372 42% | 8317 18% | V someFunc @ a.j:345:6 |
+ * | 100 1% | 10 1% | 100 1% | 10 1% | > callerFunc @ b.j:765:34 |
+ * +-------------+------------+-------------+-------------+------------------------------+
+ */
+ /* eslint-enable max-len */
+ [
+ [100, 10, 1, 33, 1000, 100, 3, 100, "x (A:1:2)", [
+ [200, 20, 1, 33, 900, 90, 2, 66, "y (B:3:4)", [
+ [700, 70, 1, 33, 700, 70, 1, 33, "z (C:5:6)"]
+ ]]
+ ]]
+ ].forEach(compareFrameInfo(thread));
+});
+
+function compareFrameInfo(root, parent) {
+ parent = parent || root;
+ let fields = [
+ "selfSize", "selfSizePercentage", "selfCount", "selfCountPercentage",
+ "totalSize", "totalSizePercentage", "totalCount", "totalCountPercentage"
+ ];
+ return function (def) {
+ let children;
+ if (Array.isArray(def[def.length - 1])) {
+ children = def.pop();
+ }
+ let name = def.pop();
+ let expected = def;
+
+ let node = getFrameNodePath(parent, name);
+ let data = node.getInfo({ root, allocations: true });
+
+ fields.forEach((field, i) => {
+ let actual = data[field];
+ if (/percentage/i.test(field)) {
+ actual = Number.parseInt(actual, 10);
+ }
+ equal(actual, expected[i], `${name} has correct ${field}: ${expected[i]}`);
+ });
+
+ if (children) {
+ children.forEach(compareFrameInfo(root, node));
+ }
+ };
+}
+
+var TEST_DATA = {
+ sites: [1, 2, 3],
+ timestamps: [150, 200, 250],
+ sizes: [100, 200, 700],
+ frames: [
+ null, {
+ source: "A",
+ line: 1,
+ column: 2,
+ functionDisplayName: "x",
+ parent: 0
+ }, {
+ source: "B",
+ line: 3,
+ column: 4,
+ functionDisplayName: "y",
+ parent: 1
+ }, {
+ source: "C",
+ line: 5,
+ column: 6,
+ functionDisplayName: "z",
+ parent: 2
+ }
+ ]
+};
diff --git a/devtools/client/performance/test/unit/test_tree-model-allocations-02.js b/devtools/client/performance/test/unit/test_tree-model-allocations-02.js
new file mode 100644
index 000000000..cfc5c4048
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_tree-model-allocations-02.js
@@ -0,0 +1,105 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the tree model calculates correct costs/percentages for
+ * allocation frame nodes. Inverted version of test_tree-model-allocations-01.js
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function () {
+ let { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
+ const { getProfileThreadFromAllocations } = require("devtools/shared/performance/recording-utils");
+ let allocationData = getProfileThreadFromAllocations(TEST_DATA);
+ let thread = new ThreadNode(allocationData, { invertTree: true, startTime: 0,
+ endTime: 1000 });
+
+ /* eslint-disable max-len */
+ /**
+ * Values are in order according to:
+ * +-------------+------------+-------------+-------------+------------------------------+
+ * | Self Bytes | Self Count | Total Bytes | Total Count | Function |
+ * +-------------+------------+-------------+-------------+------------------------------+
+ * | 1790272 41% | 8307 17% | 1790372 42% | 8317 18% | V someFunc @ a.j:345:6 |
+ * | 100 1% | 10 1% | 100 1% | 10 1% | > callerFunc @ b.j:765:34 |
+ * +-------------+------------+-------------+-------------+------------------------------+
+ */
+ /* eslint-enable max-len */
+ [
+ [700, 70, 1, 33, 700, 70, 1, 33, "z (C:5:6)", [
+ [0, 0, 0, 0, 700, 70, 1, 33, "y (B:3:4)", [
+ [0, 0, 0, 0, 700, 70, 1, 33, "x (A:1:2)"]
+ ]]
+ ]],
+ [200, 20, 1, 33, 200, 20, 1, 33, "y (B:3:4)", [
+ [0, 0, 0, 0, 200, 20, 1, 33, "x (A:1:2)"]
+ ]],
+ [100, 10, 1, 33, 100, 10, 1, 33, "x (A:1:2)"]
+ ].forEach(compareFrameInfo(thread));
+});
+
+function compareFrameInfo(root, parent) {
+ parent = parent || root;
+ let fields = [
+ "selfSize", "selfSizePercentage", "selfCount", "selfCountPercentage",
+ "totalSize", "totalSizePercentage", "totalCount", "totalCountPercentage"
+ ];
+
+ return function (def) {
+ let children;
+
+ if (Array.isArray(def[def.length - 1])) {
+ children = def.pop();
+ }
+
+ let name = def.pop();
+ let expected = def;
+
+ let node = getFrameNodePath(parent, name);
+ let data = node.getInfo({ root, allocations: true });
+
+ fields.forEach((field, i) => {
+ let actual = data[field];
+ if (/percentage/i.test(field)) {
+ actual = Number.parseInt(actual, 10);
+ }
+ equal(actual, expected[i], `${name} has correct ${field}: ${expected[i]}`);
+ });
+
+ if (children) {
+ children.forEach(compareFrameInfo(root, node));
+ }
+ };
+}
+
+var TEST_DATA = {
+ sites: [0, 1, 2, 3],
+ timestamps: [0, 150, 200, 250],
+ sizes: [0, 100, 200, 700],
+ frames: [{
+ source: "(root)"
+ }, {
+ source: "A",
+ line: 1,
+ column: 2,
+ functionDisplayName: "x",
+ parent: 0
+ }, {
+ source: "B",
+ line: 3,
+ column: 4,
+ functionDisplayName: "y",
+ parent: 1
+ }, {
+ source: "C",
+ line: 5,
+ column: 6,
+ functionDisplayName: "z",
+ parent: 2
+ }
+ ]
+};
diff --git a/devtools/client/performance/test/unit/test_waterfall-utils-collapse-01.js b/devtools/client/performance/test/unit/test_waterfall-utils-collapse-01.js
new file mode 100644
index 000000000..e329622db
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_waterfall-utils-collapse-01.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the waterfall collapsing logic works properly.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function test() {
+ const WaterfallUtils = require("devtools/client/performance/modules/logic/waterfall-utils");
+
+ let rootMarkerNode = WaterfallUtils.createParentNode({ name: "(root)" });
+
+ WaterfallUtils.collapseMarkersIntoNode({
+ rootNode: rootMarkerNode,
+ markersList: gTestMarkers
+ });
+
+ function compare(marker, expected) {
+ for (let prop in expected) {
+ if (prop === "submarkers") {
+ for (let i = 0; i < expected.submarkers.length; i++) {
+ compare(marker.submarkers[i], expected.submarkers[i]);
+ }
+ } else if (prop !== "uid") {
+ equal(marker[prop], expected[prop], `${expected.name} matches ${prop}`);
+ }
+ }
+ }
+
+ compare(rootMarkerNode, gExpectedOutput);
+});
+
+const gTestMarkers = [
+ { start: 1, end: 18, name: "DOMEvent" },
+ // Test that JS markers can fold in DOM events and have marker children
+ { start: 2, end: 16, name: "Javascript" },
+ // Test all these markers can be children
+ { start: 3, end: 4, name: "Paint" },
+ { start: 5, end: 6, name: "Reflow" },
+ { start: 7, end: 8, name: "Styles" },
+ { start: 9, end: 9, name: "TimeStamp" },
+ { start: 10, end: 11, name: "Parse HTML" },
+ { start: 12, end: 13, name: "Parse XML" },
+ { start: 14, end: 15, name: "GarbageCollection" },
+ // Test that JS markers can be parents without being a child of DOM events
+ { start: 25, end: 30, name: "Javascript" },
+ { start: 26, end: 27, name: "Paint" },
+];
+
+const gExpectedOutput = {
+ name: "(root)", submarkers: [
+ { start: 1, end: 18, name: "DOMEvent", submarkers: [
+ { start: 2, end: 16, name: "Javascript", submarkers: [
+ { start: 3, end: 4, name: "Paint" },
+ { start: 5, end: 6, name: "Reflow" },
+ { start: 7, end: 8, name: "Styles" },
+ { start: 9, end: 9, name: "TimeStamp" },
+ { start: 10, end: 11, name: "Parse HTML" },
+ { start: 12, end: 13, name: "Parse XML" },
+ { start: 14, end: 15, name: "GarbageCollection" },
+ ]}
+ ]},
+ { start: 25, end: 30, name: "Javascript", submarkers: [
+ { start: 26, end: 27, name: "Paint" },
+ ]}
+ ]};
diff --git a/devtools/client/performance/test/unit/test_waterfall-utils-collapse-02.js b/devtools/client/performance/test/unit/test_waterfall-utils-collapse-02.js
new file mode 100644
index 000000000..1cc33f45a
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_waterfall-utils-collapse-02.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the waterfall collapsing logic works properly for console.time/console.timeEnd
+ * markers, as they should ignore any sort of collapsing.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function test() {
+ const WaterfallUtils = require("devtools/client/performance/modules/logic/waterfall-utils");
+
+ let rootMarkerNode = WaterfallUtils.createParentNode({ name: "(root)" });
+
+ WaterfallUtils.collapseMarkersIntoNode({
+ rootNode: rootMarkerNode,
+ markersList: gTestMarkers
+ });
+
+ function compare(marker, expected) {
+ for (let prop in expected) {
+ if (prop === "submarkers") {
+ for (let i = 0; i < expected.submarkers.length; i++) {
+ compare(marker.submarkers[i], expected.submarkers[i]);
+ }
+ } else if (prop !== "uid") {
+ equal(marker[prop], expected[prop], `${expected.name} matches ${prop}`);
+ }
+ }
+ }
+
+ compare(rootMarkerNode, gExpectedOutput);
+});
+
+const gTestMarkers = [
+ { start: 2, end: 9, name: "Javascript" },
+ { start: 3, end: 4, name: "Paint" },
+ // Time range starting in nest, ending outside
+ { start: 5, end: 12, name: "ConsoleTime", causeName: "1" },
+
+ // Time range starting outside of nest, ending inside
+ { start: 15, end: 21, name: "ConsoleTime", causeName: "2" },
+ { start: 18, end: 22, name: "Javascript" },
+ { start: 19, end: 20, name: "Paint" },
+
+ // Time range completely eclipsing nest
+ { start: 30, end: 40, name: "ConsoleTime", causeName: "3" },
+ { start: 34, end: 39, name: "Javascript" },
+ { start: 35, end: 36, name: "Paint" },
+
+ // Time range completely eclipsed by nest
+ { start: 50, end: 60, name: "Javascript" },
+ { start: 54, end: 59, name: "ConsoleTime", causeName: "4" },
+ { start: 56, end: 57, name: "Paint" },
+];
+
+const gExpectedOutput = {
+ name: "(root)", submarkers: [
+ { start: 2, end: 9, name: "Javascript", submarkers: [
+ { start: 3, end: 4, name: "Paint" }
+ ]},
+ { start: 5, end: 12, name: "ConsoleTime", causeName: "1" },
+
+ { start: 15, end: 21, name: "ConsoleTime", causeName: "2" },
+ { start: 18, end: 22, name: "Javascript", submarkers: [
+ { start: 19, end: 20, name: "Paint" }
+ ]},
+
+ { start: 30, end: 40, name: "ConsoleTime", causeName: "3" },
+ { start: 34, end: 39, name: "Javascript", submarkers: [
+ { start: 35, end: 36, name: "Paint" },
+ ]},
+
+ { start: 50, end: 60, name: "Javascript", submarkers: [
+ { start: 56, end: 57, name: "Paint" },
+ ]},
+ { start: 54, end: 59, name: "ConsoleTime", causeName: "4" },
+ ]};
diff --git a/devtools/client/performance/test/unit/test_waterfall-utils-collapse-03.js b/devtools/client/performance/test/unit/test_waterfall-utils-collapse-03.js
new file mode 100644
index 000000000..00b6d2db0
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_waterfall-utils-collapse-03.js
@@ -0,0 +1,64 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests that the waterfall collapsing works when atleast two
+ * collapsible markers downward, and the following marker is outside of both ranges.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function test() {
+ const WaterfallUtils = require("devtools/client/performance/modules/logic/waterfall-utils");
+
+ let rootMarkerNode = WaterfallUtils.createParentNode({ name: "(root)" });
+
+ WaterfallUtils.collapseMarkersIntoNode({
+ rootNode: rootMarkerNode,
+ markersList: gTestMarkers
+ });
+
+ function compare(marker, expected) {
+ for (let prop in expected) {
+ if (prop === "submarkers") {
+ for (let i = 0; i < expected.submarkers.length; i++) {
+ compare(marker.submarkers[i], expected.submarkers[i]);
+ }
+ } else if (prop !== "uid") {
+ equal(marker[prop], expected[prop], `${expected.name} matches ${prop}`);
+ }
+ }
+ }
+
+ compare(rootMarkerNode, gExpectedOutput);
+});
+
+const gTestMarkers = [
+ { start: 2, end: 10, name: "DOMEvent" },
+ { start: 3, end: 9, name: "Javascript" },
+ { start: 4, end: 8, name: "GarbageCollection" },
+ { start: 11, end: 12, name: "Styles" },
+ { start: 13, end: 14, name: "Styles" },
+ { start: 15, end: 25, name: "DOMEvent" },
+ { start: 17, end: 24, name: "Javascript" },
+ { start: 18, end: 19, name: "GarbageCollection" },
+];
+
+const gExpectedOutput = {
+ name: "(root)", submarkers: [
+ { start: 2, end: 10, name: "DOMEvent", submarkers: [
+ { start: 3, end: 9, name: "Javascript", submarkers: [
+ { start: 4, end: 8, name: "GarbageCollection" }
+ ]}
+ ]},
+ { start: 11, end: 12, name: "Styles" },
+ { start: 13, end: 14, name: "Styles" },
+ { start: 15, end: 25, name: "DOMEvent", submarkers: [
+ { start: 17, end: 24, name: "Javascript", submarkers: [
+ { start: 18, end: 19, name: "GarbageCollection" }
+ ]}
+ ]},
+ ]};
diff --git a/devtools/client/performance/test/unit/test_waterfall-utils-collapse-04.js b/devtools/client/performance/test/unit/test_waterfall-utils-collapse-04.js
new file mode 100644
index 000000000..916a3b1d4
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_waterfall-utils-collapse-04.js
@@ -0,0 +1,103 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the waterfall collapsing logic works properly
+ * when filtering parents and children.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function test() {
+ const WaterfallUtils = require("devtools/client/performance/modules/logic/waterfall-utils");
+
+ [
+ [["DOMEvent"], gExpectedOutputNoDOMEvent],
+ [["Javascript"], gExpectedOutputNoJS],
+ [["DOMEvent", "Javascript"], gExpectedOutputNoDOMEventOrJS],
+ ].forEach(([filter, expected]) => {
+ let rootMarkerNode = WaterfallUtils.createParentNode({ name: "(root)" });
+
+ WaterfallUtils.collapseMarkersIntoNode({
+ rootNode: rootMarkerNode,
+ markersList: gTestMarkers,
+ filter
+ });
+
+ compare(rootMarkerNode, expected);
+ });
+
+ function compare(marker, expected) {
+ for (let prop in expected) {
+ if (prop === "submarkers") {
+ for (let i = 0; i < expected.submarkers.length; i++) {
+ compare(marker.submarkers[i], expected.submarkers[i]);
+ }
+ } else if (prop !== "uid") {
+ equal(marker[prop], expected[prop], `${expected.name} matches ${prop}`);
+ }
+ }
+ }
+});
+
+const gTestMarkers = [
+ { start: 1, end: 18, name: "DOMEvent" },
+ // Test that JS markers can fold in DOM events and have marker children
+ { start: 2, end: 16, name: "Javascript" },
+ // Test all these markers can be children
+ { start: 3, end: 4, name: "Paint" },
+ { start: 5, end: 6, name: "Reflow" },
+ { start: 7, end: 8, name: "Styles" },
+ { start: 9, end: 9, name: "TimeStamp" },
+ { start: 10, end: 11, name: "Parse HTML" },
+ { start: 12, end: 13, name: "Parse XML" },
+ { start: 14, end: 15, name: "GarbageCollection" },
+ // Test that JS markers can be parents without being a child of DOM events
+ { start: 25, end: 30, name: "Javascript" },
+ { start: 26, end: 27, name: "Paint" },
+];
+
+const gExpectedOutputNoJS = {
+ name: "(root)", submarkers: [
+ { start: 1, end: 18, name: "DOMEvent", submarkers: [
+ { start: 3, end: 4, name: "Paint" },
+ { start: 5, end: 6, name: "Reflow" },
+ { start: 7, end: 8, name: "Styles" },
+ { start: 9, end: 9, name: "TimeStamp" },
+ { start: 10, end: 11, name: "Parse HTML" },
+ { start: 12, end: 13, name: "Parse XML" },
+ { start: 14, end: 15, name: "GarbageCollection" },
+ ]},
+ { start: 26, end: 27, name: "Paint" },
+ ]};
+
+const gExpectedOutputNoDOMEvent = {
+ name: "(root)", submarkers: [
+ { start: 2, end: 16, name: "Javascript", submarkers: [
+ { start: 3, end: 4, name: "Paint" },
+ { start: 5, end: 6, name: "Reflow" },
+ { start: 7, end: 8, name: "Styles" },
+ { start: 9, end: 9, name: "TimeStamp" },
+ { start: 10, end: 11, name: "Parse HTML" },
+ { start: 12, end: 13, name: "Parse XML" },
+ { start: 14, end: 15, name: "GarbageCollection" },
+ ]},
+ { start: 25, end: 30, name: "Javascript", submarkers: [
+ { start: 26, end: 27, name: "Paint" },
+ ]}
+ ]};
+
+const gExpectedOutputNoDOMEventOrJS = {
+ name: "(root)", submarkers: [
+ { start: 3, end: 4, name: "Paint" },
+ { start: 5, end: 6, name: "Reflow" },
+ { start: 7, end: 8, name: "Styles" },
+ { start: 9, end: 9, name: "TimeStamp" },
+ { start: 10, end: 11, name: "Parse HTML" },
+ { start: 12, end: 13, name: "Parse XML" },
+ { start: 14, end: 15, name: "GarbageCollection" },
+ { start: 26, end: 27, name: "Paint" },
+ ]};
diff --git a/devtools/client/performance/test/unit/test_waterfall-utils-collapse-05.js b/devtools/client/performance/test/unit/test_waterfall-utils-collapse-05.js
new file mode 100644
index 000000000..ba85c2adc
--- /dev/null
+++ b/devtools/client/performance/test/unit/test_waterfall-utils-collapse-05.js
@@ -0,0 +1,164 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Tests if the waterfall collapsing logic works properly
+ * when dealing with OTMT markers.
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function test() {
+ const WaterfallUtils = require("devtools/client/performance/modules/logic/waterfall-utils");
+
+ let rootMarkerNode = WaterfallUtils.createParentNode({ name: "(root)" });
+
+ WaterfallUtils.collapseMarkersIntoNode({
+ rootNode: rootMarkerNode,
+ markersList: gTestMarkers
+ });
+
+ compare(rootMarkerNode, gExpectedOutput);
+
+ function compare(marker, expected) {
+ for (let prop in expected) {
+ if (prop === "submarkers") {
+ for (let i = 0; i < expected.submarkers.length; i++) {
+ compare(marker.submarkers[i], expected.submarkers[i]);
+ }
+ } else if (prop !== "uid") {
+ equal(marker[prop], expected[prop], `${expected.name} matches ${prop}`);
+ }
+ }
+ }
+});
+
+const gTestMarkers = [
+ { start: 1, end: 4, name: "A1-mt", processType: 1, isOffMainThread: false },
+ // This should collapse only under A1-mt
+ { start: 2, end: 3, name: "B1", processType: 1, isOffMainThread: false },
+ // This should never collapse.
+ { start: 2, end: 3, name: "C1", processType: 1, isOffMainThread: true },
+
+ { start: 5, end: 8, name: "A1-otmt", processType: 1, isOffMainThread: true },
+ // This should collapse only under A1-mt
+ { start: 6, end: 7, name: "B2", processType: 1, isOffMainThread: false },
+ // This should never collapse.
+ { start: 6, end: 7, name: "C2", processType: 1, isOffMainThread: true },
+
+ { start: 9, end: 12, name: "A2-mt", processType: 2, isOffMainThread: false },
+ // This should collapse only under A2-mt
+ { start: 10, end: 11, name: "D1", processType: 2, isOffMainThread: false },
+ // This should never collapse.
+ { start: 10, end: 11, name: "E1", processType: 2, isOffMainThread: true },
+
+ { start: 13, end: 16, name: "A2-otmt", processType: 2, isOffMainThread: true },
+ // This should collapse only under A2-mt
+ { start: 14, end: 15, name: "D2", processType: 2, isOffMainThread: false },
+ // This should never collapse.
+ { start: 14, end: 15, name: "E2", processType: 2, isOffMainThread: true },
+
+ // This should not collapse, because there's no parent in this process.
+ { start: 14, end: 15, name: "F", processType: 3, isOffMainThread: false },
+
+ // This should never collapse.
+ { start: 14, end: 15, name: "G", processType: 3, isOffMainThread: true },
+];
+
+const gExpectedOutput = {
+ name: "(root)",
+ submarkers: [{
+ start: 1,
+ end: 4,
+ name: "A1-mt",
+ processType: 1,
+ isOffMainThread: false,
+ submarkers: [{
+ start: 2,
+ end: 3,
+ name: "B1",
+ processType: 1,
+ isOffMainThread: false
+ }]
+ }, {
+ start: 2,
+ end: 3,
+ name: "C1",
+ processType: 1,
+ isOffMainThread: true
+ }, {
+ start: 5,
+ end: 8,
+ name: "A1-otmt",
+ processType: 1,
+ isOffMainThread: true,
+ submarkers: [{
+ start: 6,
+ end: 7,
+ name: "B2",
+ processType: 1,
+ isOffMainThread: false
+ }]
+ }, {
+ start: 6,
+ end: 7,
+ name: "C2",
+ processType: 1,
+ isOffMainThread: true
+ }, {
+ start: 9,
+ end: 12,
+ name: "A2-mt",
+ processType: 2,
+ isOffMainThread: false,
+ submarkers: [{
+ start: 10,
+ end: 11,
+ name: "D1",
+ processType: 2,
+ isOffMainThread: false
+ }]
+ }, {
+ start: 10,
+ end: 11,
+ name: "E1",
+ processType: 2,
+ isOffMainThread: true
+ }, {
+ start: 13,
+ end: 16,
+ name: "A2-otmt",
+ processType: 2,
+ isOffMainThread: true,
+ submarkers: [{
+ start: 14,
+ end: 15,
+ name: "D2",
+ processType: 2,
+ isOffMainThread: false
+ }]
+ }, {
+ start: 14,
+ end: 15,
+ name: "E2",
+ processType: 2,
+ isOffMainThread: true
+ }, {
+ start: 14,
+ end: 15,
+ name: "F",
+ processType: 3,
+ isOffMainThread: false,
+ submarkers: []
+ }, {
+ start: 14,
+ end: 15,
+ name: "G",
+ processType: 3,
+ isOffMainThread: true,
+ submarkers: []
+ }]
+};
diff --git a/devtools/client/performance/test/unit/xpcshell.ini b/devtools/client/performance/test/unit/xpcshell.ini
new file mode 100644
index 000000000..b9d0c1403
--- /dev/null
+++ b/devtools/client/performance/test/unit/xpcshell.ini
@@ -0,0 +1,36 @@
+[DEFAULT]
+tags = devtools
+head = head.js
+tail =
+firefox-appdir = browser
+skip-if = toolkit == 'android'
+
+[test_frame-utils-01.js]
+[test_frame-utils-02.js]
+[test_marker-blueprint.js]
+[test_marker-utils.js]
+[test_profiler-categories.js]
+[test_jit-graph-data.js]
+[test_jit-model-01.js]
+[test_jit-model-02.js]
+[test_perf-utils-allocations-to-samples.js]
+[test_tree-model-01.js]
+[test_tree-model-02.js]
+[test_tree-model-03.js]
+[test_tree-model-04.js]
+[test_tree-model-05.js]
+[test_tree-model-06.js]
+[test_tree-model-07.js]
+[test_tree-model-08.js]
+[test_tree-model-09.js]
+[test_tree-model-10.js]
+[test_tree-model-11.js]
+[test_tree-model-12.js]
+[test_tree-model-13.js]
+[test_tree-model-allocations-01.js]
+[test_tree-model-allocations-02.js]
+[test_waterfall-utils-collapse-01.js]
+[test_waterfall-utils-collapse-02.js]
+[test_waterfall-utils-collapse-03.js]
+[test_waterfall-utils-collapse-04.js]
+[test_waterfall-utils-collapse-05.js]