diff options
Diffstat (limited to 'devtools/client/performance/test/unit')
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] |