Bug 1117825 - Create an utility function for the FlameGraph widget to convert the profiler data into something drawable, r=jsantell
authorVictor Porof <vporof@mozilla.com>
Sun, 11 Jan 2015 09:54:26 -0500
changeset 249091 f919a460dc30cacdf64465560f7d4f3ed85a3b01
parent 249090 c92d931cbd4a50299a6be08b3f8e25a5516f9fbd
child 249092 3c5db371a6385189c4d2d7d23d8d3e6113a514d4
push id4489
push userraliiev@mozilla.com
push dateMon, 23 Feb 2015 15:17:55 +0000
treeherdermozilla-beta@fd7c3dc24146 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjsantell
bugs1117825
milestone37.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1117825 - Create an utility function for the FlameGraph widget to convert the profiler data into something drawable, r=jsantell
browser/devtools/shared/test/browser.ini
browser/devtools/shared/test/browser_flame-graph-utils.js
browser/devtools/shared/widgets/FlameGraph.jsm
--- a/browser/devtools/shared/test/browser.ini
+++ b/browser/devtools/shared/test/browser.ini
@@ -14,16 +14,17 @@ support-files =
 [browser_css_color.js]
 [browser_cubic-bezier-01.js]
 [browser_cubic-bezier-02.js]
 [browser_cubic-bezier-03.js]
 [browser_flame-graph-01.js]
 [browser_flame-graph-02.js]
 [browser_flame-graph-03.js]
 [browser_flame-graph-04.js]
+[browser_flame-graph-utils.js]
 [browser_graphs-01.js]
 [browser_graphs-02.js]
 [browser_graphs-03.js]
 [browser_graphs-04.js]
 [browser_graphs-05.js]
 [browser_graphs-06.js]
 [browser_graphs-07a.js]
 [browser_graphs-07b.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/shared/test/browser_flame-graph-utils.js
@@ -0,0 +1,262 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that text metrics in the flame graph widget work properly.
+
+let {FlameGraphUtils} = Cu.import("resource:///modules/devtools/FlameGraph.jsm", {});
+
+let test = Task.async(function*() {
+  yield promiseTab("about:blank");
+  yield performTest();
+  gBrowser.removeCurrentTab();
+  finish();
+});
+
+function* performTest() {
+  let out = FlameGraphUtils.createFlameGraphDataFromSamples(TEST_DATA);
+
+  dump(">>> " + out + "\n");
+  dump(">>> " + out.toSource() + "\n");
+
+  ok(out, "Some data was outputted properly");
+  is(out.length, 10, "The outputted length is correct.");
+
+  for (let i = 0; i < out.length; i++) {
+    let found = out[i];
+    let expected = EXPECTED_OUTPUT[i];
+
+    is(found.blocks.length, expected.blocks.length,
+      "The correct number of blocks were found in this bucket.");
+
+    for (let j = 0; j < found.blocks.length; j++) {
+      is(found.blocks[j].x, expected.blocks[j].x,
+        "The expected block X position is correct for this frame.");
+      is(found.blocks[j].y, expected.blocks[j].y,
+        "The expected block Y position is correct for this frame.");
+      is(found.blocks[j].width, expected.blocks[j].width,
+        "The expected block width is correct for this frame.");
+      is(found.blocks[j].height, expected.blocks[j].height,
+        "The expected block height is correct for this frame.");
+      is(found.blocks[j].text, expected.blocks[j].text,
+        "The expected block text is correct for this frame.");
+    }
+  }
+}
+
+let TEST_DATA = [{
+  frames: [{
+    location: "M"
+  }, {
+    location: "N",
+  }, {
+    location: "P"
+  }],
+  time: 50,
+}, {
+  frames: [{
+    location: "A"
+  }, {
+    location: "B",
+  }, {
+    location: "C"
+  }],
+  time: 100,
+}, {
+  frames: [{
+    location: "A"
+  }, {
+    location: "B",
+  }, {
+    location: "D"
+  }],
+  time: 210,
+}, {
+  frames: [{
+    location: "A"
+  }, {
+    location: "E",
+  }, {
+    location: "F"
+  }],
+  time: 330,
+}, {
+  frames: [{
+    location: "A"
+  }, {
+    location: "B",
+  }, {
+    location: "C"
+  }],
+  time: 460,
+}, {
+  frames: [{
+    location: "X"
+  }, {
+    location: "Y",
+  }, {
+    location: "Z"
+  }],
+  time: 500
+}];
+
+let EXPECTED_OUTPUT = [{
+  blocks: []
+}, {
+  blocks: []
+}, {
+  blocks: [{
+    srcData: {
+      startTime: 50,
+      rawLocation: "A"
+    },
+    x: 50,
+    y: 0,
+    width: 410,
+    height: 12,
+    text: "A"
+  }]
+}, {
+  blocks: [{
+    srcData: {
+      startTime: 50,
+      rawLocation: "B"
+    },
+    x: 50,
+    y: 12,
+    width: 160,
+    height: 12,
+    text: "B"
+  }, {
+    srcData: {
+      startTime: 330,
+      rawLocation: "B"
+    },
+    x: 330,
+    y: 12,
+    width: 130,
+    height: 12,
+    text: "B"
+  }]
+}, {
+  blocks: [{
+    srcData: {
+      startTime: 0,
+      rawLocation: "M"
+    },
+    x: 0,
+    y: 0,
+    width: 50,
+    height: 12,
+    text: "M"
+  }, {
+    srcData: {
+      startTime: 50,
+      rawLocation: "C"
+    },
+    x: 50,
+    y: 24,
+    width: 50,
+    height: 12,
+    text: "C"
+  }, {
+    srcData: {
+      startTime: 330,
+      rawLocation: "C"
+    },
+    x: 330,
+    y: 24,
+    width: 130,
+    height: 12,
+    text: "C"
+  }]
+}, {
+  blocks: [{
+    srcData: {
+      startTime: 0,
+      rawLocation: "N"
+    },
+    x: 0,
+    y: 12,
+    width: 50,
+    height: 12,
+    text: "N"
+  }, {
+    srcData: {
+      startTime: 100,
+      rawLocation: "D"
+    },
+    x: 100,
+    y: 24,
+    width: 110,
+    height: 12,
+    text: "D"
+  }, {
+    srcData: {
+      startTime: 460,
+      rawLocation: "X"
+    },
+    x: 460,
+    y: 0,
+    width: 40,
+    height: 12,
+    text: "X"
+  }]
+}, {
+  blocks: [{
+    srcData: {
+      startTime: 210,
+      rawLocation: "E"
+    },
+    x: 210,
+    y: 12,
+    width: 120,
+    height: 12,
+    text: "E"
+  }, {
+    srcData: {
+      startTime: 460,
+      rawLocation: "Y"
+    },
+    x: 460,
+    y: 12,
+    width: 40,
+    height: 12,
+    text: "Y"
+  }]
+}, {
+  blocks: [{
+    srcData: {
+      startTime: 0,
+      rawLocation: "P"
+    },
+    x: 0,
+    y: 24,
+    width: 50,
+    height: 12,
+    text: "P"
+  }, {
+    srcData: {
+      startTime: 210,
+      rawLocation: "F"
+    },
+    x: 210,
+    y: 24,
+    width: 120,
+    height: 12,
+    text: "F"
+  }, {
+    srcData: {
+      startTime: 460,
+      rawLocation: "Z"
+    },
+    x: 460,
+    y: 24,
+    width: 40,
+    height: 12,
+    text: "Z"
+  }]
+}, {
+  blocks: []
+}, {
+  blocks: []
+}];
--- a/browser/devtools/shared/widgets/FlameGraph.jsm
+++ b/browser/devtools/shared/widgets/FlameGraph.jsm
@@ -741,15 +741,126 @@ FlameGraph.prototype = {
       x += node.offsetLeft;
       y += node.offsetTop;
     }
 
     return { left: x, top: y };
   }
 };
 
+const FLAME_GRAPH_BLOCK_HEIGHT = 12; // px
+
+const PALLETTE_SIZE = 10;
+const PALLETTE_HUE_OFFSET = Math.random() * 90;
+const PALLETTE_HUE_RANGE = 270;
+const PALLETTE_SATURATION = 60;
+const PALLETTE_BRIGHTNESS = 75;
+const PALLETTE_OPACITY = 0.7;
+
+const COLOR_PALLETTE = Array.from(Array(PALLETTE_SIZE)).map((_, i) => "hsla" +
+  "(" + ((PALLETTE_HUE_OFFSET + (i / PALLETTE_SIZE * PALLETTE_HUE_RANGE))|0 % 360) +
+  "," + PALLETTE_SATURATION + "%" +
+  "," + PALLETTE_BRIGHTNESS + "%" +
+  "," + PALLETTE_OPACITY +
+  ")"
+);
+
 /**
  * A collection of utility functions converting various data sources
  * into a format drawable by the FlameGraph.
  */
 let FlameGraphUtils = {
-  // TODO bug 1077459
+  /**
+   * Converts a list of samples from the profiler data to something that's
+   * drawable by a FlameGraph widget.
+   *
+   * @param array samples
+   *        A list of { time, frames: [{ location }] } objects.
+   * @param array out [optional]
+   *        An output storage to reuse for storing the flame graph data.
+   * @return array
+   *         The flame graph data.
+   */
+  createFlameGraphDataFromSamples: function(samples, out = []) {
+    // 1. Create a map of colors to arrays, representing buckets of
+    // blocks inside the flame graph pyramid sharing the same style.
+
+    let buckets = new Map();
+
+    for (let color of COLOR_PALLETTE) {
+      buckets.set(color, []);
+    }
+
+    // 2. Populate the buckets by iterating over every frame in every sample.
+
+    let prevTime = 0;
+    let prevFrames = [];
+
+    for (let { frames, time } of samples) {
+      let frameIndex = 0;
+
+      for (let { location } of frames) {
+        let prevFrame = prevFrames[frameIndex];
+
+        // Frames at the same location and the same depth will be reused.
+        // If there is a block already created, change its width.
+        if (prevFrame && prevFrame.srcData.rawLocation == location) {
+          prevFrame.width = (time - prevFrame.srcData.startTime);
+        }
+        // Otherwise, create a new block for this frame at this depth,
+        // using a simple location based salt for picking a color.
+        else {
+          let hash = this._getStringHash(location);
+          let color = COLOR_PALLETTE[hash % PALLETTE_SIZE];
+          let bucket = buckets.get(color);
+
+          bucket.push(prevFrames[frameIndex] = {
+            srcData: { startTime: prevTime, rawLocation: location },
+            x: prevTime,
+            y: frameIndex * FLAME_GRAPH_BLOCK_HEIGHT,
+            width: time - prevTime,
+            height: FLAME_GRAPH_BLOCK_HEIGHT,
+            text: location
+          });
+        }
+
+        frameIndex++;
+      }
+
+      // Previous frames at stack depths greater than the current sample's
+      // maximum need to be nullified. It's nonsensical to reuse them.
+      for (let i = frameIndex; i < prevFrames.length; i++) {
+        prevFrames[i] = null;
+      }
+
+      prevTime = time;
+    }
+
+    // 3. Convert the buckets into a data source usable by the FlameGraph.
+    // This is a simple conversion from a Map to an Array.
+
+    for (let [color, blocks] of buckets) {
+      out.push({ color, blocks });
+    }
+
+    return out;
+  },
+
+  /**
+   * Very dumb hashing of a string. Used to pick colors from a pallette.
+   *
+   * @param string input
+   * @return number
+   */
+  _getStringHash: function(input) {
+    const STRING_HASH_PRIME1 = 7;
+    const STRING_HASH_PRIME2 = 31;
+
+    let hash = STRING_HASH_PRIME1;
+
+    for (let i = 0, len = input.length; i < len; i++) {
+      hash *= STRING_HASH_PRIME2;
+      hash += input.charCodeAt(i);
+    }
+
+    return hash;
+  }
 };