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 240003 f919a460dc30cacdf64465560f7d4f3ed85a3b01
parent 240002 c92d931cbd4a50299a6be08b3f8e25a5516f9fbd
child 240004 3c5db371a6385189c4d2d7d23d8d3e6113a514d4
push id7472
push userraliiev@mozilla.com
push dateMon, 12 Jan 2015 20:36:27 +0000
treeherdermozilla-aurora@300ca104f8fb [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjsantell
bugs1117825
milestone37.0a1
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;
+  }
 };