Bug 1462784 - Update the devtools performance panel for the new category list. r?gregtatum draft
authorMarkus Stange <mstange@themasta.com>
Wed, 23 May 2018 23:11:41 -0400
changeset 803075 253e7e895723f413ed91d13a875bf68ed8649f54
parent 803074 996f52eb5c82c34b0290695f42003a18d84a29c9
child 803583 7b059d1b48e9b9be6f1cb63fa6daaa15eb3e60f0
push id112022
push userbmo:mstange@themasta.com
push dateFri, 01 Jun 2018 21:00:49 +0000
reviewersgregtatum
bugs1462784
milestone62.0a1
Bug 1462784 - Update the devtools performance panel for the new category list. r?gregtatum MozReview-Commit-ID: HwRFEfgA4L
devtools/client/locales/en-US/performance.properties
devtools/client/performance/modules/categories.js
devtools/client/performance/modules/logic/frame-utils.js
devtools/client/performance/test/browser_perf-tree-view-02.js
devtools/client/performance/test/browser_perf-tree-view-08.js
devtools/client/performance/test/helpers/synth-utils.js
devtools/client/performance/test/unit/test_profiler-categories.js
devtools/client/performance/test/unit/test_tree-model-07.js
devtools/client/performance/test/unit/test_tree-model-08.js
devtools/client/performance/test/unit/test_tree-model-09.js
devtools/client/shared/widgets/FlameGraph.js
--- a/devtools/client/locales/en-US/performance.properties
+++ b/devtools/client/locales/en-US/performance.properties
@@ -58,23 +58,23 @@ graphs.ms=ms
 # AS SHORT AS POSSIBLE so it doesn't obstruct important parts of the graph.
 graphs.memory=MB
 
 # LOCALIZATION NOTE (category.*):
 # These strings are displayed in the categories graph of the Performance Tools,
 # as the legend for each block in every bar. These labels should be kept
 # AS SHORT AS POSSIBLE so they don't obstruct important parts of the graph.
 category.other=Gecko
-category.css=Styles
+category.layout=Layout
 category.js=JIT
 category.gc=GC
 category.network=Network
 category.graphics=Graphics
-category.storage=Storage
-category.events=Input & Events
+category.dom=DOM
+category.idle=Idle
 category.tools=Tools
 
 # LOCALIZATION NOTE (table.bytes):
 # This string is displayed in the call tree after bytesize units.
 # %S represents the value in bytes.
 table.bytes=%S B
 
 # LOCALIZATION NOTE (table.ms2):
--- a/devtools/client/performance/modules/categories.js
+++ b/devtools/client/performance/modules/categories.js
@@ -2,26 +2,30 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 const { L10N } = require("devtools/client/performance/modules/global");
 
 /**
  * Details about each label stack frame category.
- * @see CATEGORY_MAPPINGS.
+ * To be kept in sync with the js::ProfilingStackFrame::Category in ProfilingStack.h
  */
 const CATEGORIES = [{
+  color: "#d99b28",
+  abbrev: "idle",
+  label: L10N.getStr("category.idle")
+}, {
   color: "#5e88b0",
   abbrev: "other",
   label: L10N.getStr("category.other")
 }, {
   color: "#46afe3",
-  abbrev: "css",
-  label: L10N.getStr("category.css")
+  abbrev: "layout",
+  label: L10N.getStr("category.layout")
 }, {
   color: "#d96629",
   abbrev: "js",
   label: L10N.getStr("category.js")
 }, {
   color: "#eb5368",
   abbrev: "gc",
   label: L10N.getStr("category.gc")
@@ -30,99 +34,40 @@ const CATEGORIES = [{
   abbrev: "network",
   label: L10N.getStr("category.network")
 }, {
   color: "#70bf53",
   abbrev: "graphics",
   label: L10N.getStr("category.graphics")
 }, {
   color: "#8fa1b2",
-  abbrev: "storage",
-  label: L10N.getStr("category.storage")
+  abbrev: "dom",
+  label: L10N.getStr("category.dom")
 }, {
-  color: "#d99b28",
-  abbrev: "events",
-  label: L10N.getStr("category.events")
-}, {
+  // The devtools-only "tools" category which gets computed by
+  // computeIsContentAndCategory and is not part of the category list in
+  // ProfilingStack.h.
   color: "#8fa1b2",
   abbrev: "tools",
   label: L10N.getStr("category.tools")
 }];
 
 /**
- * Mapping from category bitmasks in the profiler data to additional details.
- * To be kept in sync with the js::ProfilingStackFrame::Category in ProfilingStack.h
+ * Get the numeric index for the given category abbreviation.
+ * See `CATEGORIES` above.
  */
-const CATEGORY_MAPPINGS = {
-  // js::ProfilingStackFrame::Category::OTHER
-  "16": CATEGORIES[0],
-  // js::ProfilingStackFrame::Category::CSS
-  "32": CATEGORIES[1],
-  // js::ProfilingStackFrame::Category::JS
-  "64": CATEGORIES[2],
-  // js::ProfilingStackFrame::Category::GC
-  "128": CATEGORIES[3],
-  // js::ProfilingStackFrame::Category::CC
-  "256": CATEGORIES[3],
-  // js::ProfilingStackFrame::Category::NETWORK
-  "512": CATEGORIES[4],
-  // js::ProfilingStackFrame::Category::GRAPHICS
-  "1024": CATEGORIES[5],
-  // js::ProfilingStackFrame::Category::STORAGE
-  "2048": CATEGORIES[6],
-  // js::ProfilingStackFrame::Category::EVENTS
-  "4096": CATEGORIES[7],
-  // non-bitmasks for specially-assigned categories
-  "9000": CATEGORIES[8],
-};
-
-/**
- * Get the numeric bitmask (or set of masks) for the given category
- * abbreviation. See `CATEGORIES` and `CATEGORY_MAPPINGS` above.
- *
- * CATEGORY_MASK can be called with just a name if it is expected that the
- * category is mapped to by exactly one bitmask. If the category is mapped
- * to by multiple masks, CATEGORY_MASK for that name must be called with
- * an additional argument specifying the desired id (in ascending order).
- */
-const [CATEGORY_MASK, CATEGORY_MASK_LIST] = (() => {
-  const bitmasksForCategory = {};
-  const all = Object.keys(CATEGORY_MAPPINGS);
-
-  for (const category of CATEGORIES) {
-    bitmasksForCategory[category.abbrev] = all
-      .filter(mask => CATEGORY_MAPPINGS[mask] == category)
-      .map(mask => +mask)
-      .sort();
+const CATEGORY_INDEX = (() => {
+  const indexForCategory = {};
+  for (let categoryIndex = 0; categoryIndex < CATEGORIES.length; categoryIndex++) {
+    const category = CATEGORIES[categoryIndex];
+    indexForCategory[category.abbrev] = categoryIndex;
   }
 
-  return [
-    function(name, index) {
-      if (!(name in bitmasksForCategory)) {
-        throw new Error(`Category abbreviation "${name}" does not exist.`);
-      }
-      if (arguments.length == 1) {
-        if (bitmasksForCategory[name].length != 1) {
-          throw new Error(`Expected exactly one category number for "${name}".`);
-        } else {
-          return bitmasksForCategory[name][0];
-        }
-      } else {
-        if (index > bitmasksForCategory[name].length) {
-          throw new Error(`Index "${index}" too high for category "${name}".`);
-        }
-        return bitmasksForCategory[name][index - 1];
-      }
-    },
-
-    function(name) {
-      if (!(name in bitmasksForCategory)) {
-        throw new Error(`Category abbreviation "${name}" does not exist.`);
-      }
-      return bitmasksForCategory[name];
+  return function(name) {
+    if (!(name in indexForCategory)) {
+      throw new Error(`Category abbreviation "${name}" does not exist.`);
     }
-  ];
+    return indexForCategory[name];
+  };
 })();
 
 exports.CATEGORIES = CATEGORIES;
-exports.CATEGORY_MAPPINGS = CATEGORY_MAPPINGS;
-exports.CATEGORY_MASK = CATEGORY_MASK;
-exports.CATEGORY_MASK_LIST = CATEGORY_MASK_LIST;
+exports.CATEGORY_INDEX = CATEGORY_INDEX;
--- a/devtools/client/performance/modules/logic/frame-utils.js
+++ b/devtools/client/performance/modules/logic/frame-utils.js
@@ -4,17 +4,17 @@
 "use strict";
 
 const global = require("devtools/client/performance/modules/global");
 const demangle = require("devtools/client/shared/demangle");
 const { assert } = require("devtools/shared/DevToolsUtils");
 const { isChromeScheme, isContentScheme, isWASM, parseURL } =
   require("devtools/client/shared/source-utils");
 
-const { CATEGORY_MASK, CATEGORY_MAPPINGS } = require("devtools/client/performance/modules/categories");
+const { CATEGORY_INDEX, CATEGORIES } = require("devtools/client/performance/modules/categories");
 
 // Character codes used in various parsing helper functions.
 const CHAR_CODE_R = "r".charCodeAt(0);
 const CHAR_CODE_0 = "0".charCodeAt(0);
 const CHAR_CODE_9 = "9".charCodeAt(0);
 const CHAR_CODE_CAP_Z = "Z".charCodeAt(0);
 
 const CHAR_CODE_LPAREN = "(".charCodeAt(0);
@@ -185,17 +185,17 @@ function parseLocation(location, fallbac
 
 /**
  * Sets the properties of `isContent` and `category` on a frame.
  *
  * @param {InflatedFrame} frame
  */
 function computeIsContentAndCategory(frame) {
   // Only C++ stack frames have associated category information.
-  if (frame.category) {
+  if (frame.category !== null && frame.category !== undefined) {
     return;
   }
 
   const location = frame.location;
 
   // There are 3 variants of location strings in the profiler (with optional
   // column numbers):
   //   1) "name (resource:line)"
@@ -229,28 +229,28 @@ function computeIsContentAndCategory(fra
   }
 
   if (schemeStartIndex !== 0) {
     for (let j = schemeStartIndex; j < location.length; j++) {
       if (location.charCodeAt(j) === CHAR_CODE_R &&
           isChromeScheme(location, j) &&
           (location.includes("resource://devtools") ||
            location.includes("resource://devtools"))) {
-        frame.category = CATEGORY_MASK("tools");
+        frame.category = CATEGORY_INDEX("tools");
         return;
       }
     }
   }
 
   if (location === "EnterJIT") {
-    frame.category = CATEGORY_MASK("js");
+    frame.category = CATEGORY_INDEX("js");
     return;
   }
 
-  frame.category = CATEGORY_MASK("other");
+  frame.category = CATEGORY_INDEX("other");
 }
 
 /**
  * Get caches to cache inflated frames and computed frame keys of a frame
  * table.
  *
  * @param object framesTable
  * @return object
@@ -388,17 +388,20 @@ function getFrameInfo(node, options) {
       data.functionName = global.L10N.getStr("table.root");
     } else {
       data = parseLocation(node.location, node.line, node.column);
       data.hasOptimizations = node.hasOptimizations();
       data.isContent = node.isContent;
       data.isMetaCategory = node.isMetaCategory;
     }
     data.samples = node.youngestFrameSamples;
-    data.categoryData = CATEGORY_MAPPINGS[node.category] || {};
+    const hasCategory = node.category !== null && node.category !== undefined;
+    data.categoryData = hasCategory
+      ? (CATEGORIES[node.category] || CATEGORIES[CATEGORY_INDEX("other")])
+      : {};
     data.nodeType = node.nodeType;
 
     // Frame name (function location or some meta information)
     if (data.isMetaCategory) {
       data.name = data.categoryData.label;
     } else if (shouldDemangle(data.functionName)) {
       data.name = demangle(data.functionName);
     } else {
--- a/devtools/client/performance/test/browser_perf-tree-view-02.js
+++ b/devtools/client/performance/test/browser_perf-tree-view-02.js
@@ -119,17 +119,17 @@ add_task(function() {
   is($fun(".call-tree-url", $$(".call-tree-item")[2]).textContent.trim(), "baz",
     "The .A.B node's function cell displays the correct url.");
   ok($fun(".call-tree-url", $$(".call-tree-item")[2]).getAttribute("tooltiptext").includes("http://foo/bar/baz"),
     "The .A.B node's function cell displays the correct url tooltiptext.");
   is($fun(".call-tree-line", $$(".call-tree-item")[2]).textContent.trim(), ":34",
     "The .A.B node's function cell displays the correct line.");
   is($fun(".call-tree-host", $$(".call-tree-item")[2]).textContent.trim(), "foo",
     "The .A.B node's function cell displays the correct host.");
-  is($fun(".call-tree-category", $$(".call-tree-item")[2]).textContent.trim(), "Styles",
+  is($fun(".call-tree-category", $$(".call-tree-item")[2]).textContent.trim(), "Layout",
     "The .A.B node's function cell displays the correct category.");
 
   is($$dur(3).textContent.trim(), "5 ms",
     "The .A.E node's duration cell displays the correct value.");
   is($$per(3).textContent.trim(), "25%",
     "The .A.E node's percentage cell displays the correct value.");
   is($$sam(3).textContent.trim(), "0",
     "The .A.E node's samples cell displays the correct value.");
--- a/devtools/client/performance/test/browser_perf-tree-view-08.js
+++ b/devtools/client/performance/test/browser_perf-tree-view-08.js
@@ -4,17 +4,17 @@
 
 /**
  * Tests that the profiler's tree view renders generalized platform data
  * when `contentOnly` is on correctly.
  */
 
 const { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
 const { CallView } = require("devtools/client/performance/modules/widgets/tree-view");
-const { CATEGORY_MASK } = require("devtools/client/performance/modules/categories");
+const { CATEGORY_INDEX } = require("devtools/client/performance/modules/categories");
 const RecordingUtils = require("devtools/shared/performance/recording-utils");
 
 add_task(function() {
   const threadNode = new ThreadNode(gProfile.threads[0], { startTime: 0, endTime: 20,
                                                            contentOnly: true });
 
   // Don't display the synthesized (root) and the real (root) node twice.
   threadNode.calls = threadNode.calls[0].calls;
@@ -84,26 +84,26 @@ const gProfile = RecordingUtils.deflateP
       ]
     }, {
       time: 1 + 1 + 2,
       frames: [
         { location: "(root)" },
         { location: "http://content/A" },
         { location: "http://content/E" },
         { location: "http://content/F" },
-        { location: "platform_JS", category: CATEGORY_MASK("js") },
+        { location: "platform_JS", category: CATEGORY_INDEX("js") },
       ]
     }, {
       time: 1 + 1 + 2 + 3,
       frames: [
         { location: "(root)" },
-        { location: "platform_JS2", category: CATEGORY_MASK("js") },
+        { location: "platform_JS2", category: CATEGORY_INDEX("js") },
       ]
     }, {
       time: 1 + 1 + 2 + 3 + 5,
       frames: [
         { location: "(root)" },
         { location: "http://content/A" },
-        { location: "platform_GC", category: CATEGORY_MASK("gc", 1) },
+        { location: "platform_GC", category: CATEGORY_INDEX("gc") },
       ]
     }]
   }]
 });
--- a/devtools/client/performance/test/helpers/synth-utils.js
+++ b/devtools/client/performance/test/helpers/synth-utils.js
@@ -1,53 +1,53 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
 /**
  * Generates a generalized profile with some samples.
  */
 exports.synthesizeProfile = () => {
-  const { CATEGORY_MASK } = require("devtools/client/performance/modules/categories");
+  const { CATEGORY_INDEX } = require("devtools/client/performance/modules/categories");
   const RecordingUtils = require("devtools/shared/performance/recording-utils");
 
   return RecordingUtils.deflateProfile({
     meta: { version: 2 },
     threads: [{
       samples: [{
         time: 1,
         frames: [
-          { category: CATEGORY_MASK("other"), location: "(root)" },
-          { category: CATEGORY_MASK("other"), location: "A (http://foo/bar/baz:12)" },
-          { category: CATEGORY_MASK("css"), location: "B (http://foo/bar/baz:34)" },
-          { category: CATEGORY_MASK("js"), location: "C (http://foo/bar/baz:56)" }
+          { category: CATEGORY_INDEX("other"), location: "(root)" },
+          { category: CATEGORY_INDEX("other"), location: "A (http://foo/bar/baz:12)" },
+          { category: CATEGORY_INDEX("layout"), location: "B (http://foo/bar/baz:34)" },
+          { category: CATEGORY_INDEX("js"), location: "C (http://foo/bar/baz:56)" }
         ]
       }, {
         time: 1 + 1,
         frames: [
-          { category: CATEGORY_MASK("other"), location: "(root)" },
-          { category: CATEGORY_MASK("other"), location: "A (http://foo/bar/baz:12)" },
-          { category: CATEGORY_MASK("css"), location: "B (http://foo/bar/baz:34)" },
-          { category: CATEGORY_MASK("gc", 1), location: "D (http://foo/bar/baz:78:9)" }
+          { category: CATEGORY_INDEX("other"), location: "(root)" },
+          { category: CATEGORY_INDEX("other"), location: "A (http://foo/bar/baz:12)" },
+          { category: CATEGORY_INDEX("layout"), location: "B (http://foo/bar/baz:34)" },
+          { category: CATEGORY_INDEX("gc"), location: "D (http://foo/bar/baz:78:9)" }
         ]
       }, {
         time: 1 + 1 + 2,
         frames: [
-          { category: CATEGORY_MASK("other"), location: "(root)" },
-          { category: CATEGORY_MASK("other"), location: "A (http://foo/bar/baz:12)" },
-          { category: CATEGORY_MASK("css"), location: "B (http://foo/bar/baz:34)" },
-          { category: CATEGORY_MASK("gc", 1), location: "D (http://foo/bar/baz:78:9)" }
+          { category: CATEGORY_INDEX("other"), location: "(root)" },
+          { category: CATEGORY_INDEX("other"), location: "A (http://foo/bar/baz:12)" },
+          { category: CATEGORY_INDEX("layout"), location: "B (http://foo/bar/baz:34)" },
+          { category: CATEGORY_INDEX("gc"), location: "D (http://foo/bar/baz:78:9)" }
         ]
       }, {
         time: 1 + 1 + 2 + 3,
         frames: [
-          { category: CATEGORY_MASK("other"), location: "(root)" },
-          { category: CATEGORY_MASK("other"), location: "A (http://foo/bar/baz:12)" },
-          { category: CATEGORY_MASK("gc", 2), location: "E (http://foo/bar/baz:90)" },
-          { category: CATEGORY_MASK("network"), location: "F (http://foo/bar/baz:99)" }
+          { category: CATEGORY_INDEX("other"), location: "(root)" },
+          { category: CATEGORY_INDEX("other"), location: "A (http://foo/bar/baz:12)" },
+          { category: CATEGORY_INDEX("gc"), location: "E (http://foo/bar/baz:90)" },
+          { category: CATEGORY_INDEX("network"), location: "F (http://foo/bar/baz:99)" }
         ]
       }]
     }]
   });
 };
 
 /**
  * Generates a simple implementation for a tree class.
--- a/devtools/client/performance/test/unit/test_profiler-categories.js
+++ b/devtools/client/performance/test/unit/test_profiler-categories.js
@@ -2,32 +2,24 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
 /**
  * Tests if the profiler categories are mapped correctly.
  */
 
 add_task(function() {
-  const { CATEGORIES, CATEGORY_MAPPINGS } = require("devtools/client/performance/modules/categories");
+  const { CATEGORIES } = require("devtools/client/performance/modules/categories");
   const { L10N } = require("devtools/client/performance/modules/global");
   const 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.includes(CATEGORY_MAPPINGS[e])),
-    "All bitmask mappings point to a category.");
 });
--- a/devtools/client/performance/test/unit/test_tree-model-07.js
+++ b/devtools/client/performance/test/unit/test_tree-model-07.js
@@ -1,17 +1,17 @@
 /* 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");
+var { CATEGORY_INDEX } = require("devtools/client/performance/modules/categories");
 
 add_task(function test() {
   const { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
   const url = (n) => `http://content/${n}`;
 
   // Create a root node from a given samples array.
 
   const root = getFrameNodePath(new ThreadNode(gThread, { startTime: 5, endTime: 30,
@@ -30,32 +30,39 @@ add_task(function test() {
    *       - 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,
+  ok(getFrameNodePath(root, `${CATEGORY_INDEX("js")}`),
+     "root has platform generalized child");
+  equal(getFrameNodePath(root, `${CATEGORY_INDEX("js")}`).calls.length, 0,
         "platform generalized child is a leaf.");
 
-  ok(getFrameNodePath(root, `${url("A")} > 128`),
+  ok(getFrameNodePath(root, `${url("A")} > ${CATEGORY_INDEX("gc")}`),
      "A has platform generalized child of another type");
-  equal(getFrameNodePath(root, `${url("A")} > 128`).calls.length, 0,
+  equal(getFrameNodePath(root, `${url("A")} > ${CATEGORY_INDEX("gc")}`).calls.length, 0,
         "second generalized type is a leaf.");
 
-  ok(getFrameNodePath(root, `${url("A")} > ${url("E")} > ${url("F")} > 64`),
+  ok(getFrameNodePath(
+       root,
+       `${url("A")} > ${url("E")} > ${url("F")} > ${CATEGORY_INDEX("js")}`
+     ),
      "a second leaf of the first generalized type exists deep in the tree.");
-  ok(getFrameNodePath(root, `${url("A")} > 128`),
+  ok(getFrameNodePath(root, `${url("A")} > ${CATEGORY_INDEX("gc")}`),
      "A has platform generalized child of another type");
 
-  equal(getFrameNodePath(root, "64").category,
-     getFrameNodePath(root, `${url("A")} > ${url("E")} > ${url("F")} > 64`).category,
+  equal(getFrameNodePath(root, `${CATEGORY_INDEX("js")}`).category,
+     getFrameNodePath(
+       root,
+       `${url("A")} > ${url("E")} > ${url("F")} > ${CATEGORY_INDEX("js")}`
+     ).category,
      "generalized frames of same type are duplicated in top-down view");
 });
 
 var gThread = synthesizeProfileForTest([{
   time: 5,
   frames: [
     { location: "(root)" },
     { location: "http://content/A" },
@@ -63,35 +70,35 @@ var gThread = synthesizeProfileForTest([
     { 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: "contentY", category: CATEGORY_INDEX("layout") },
     { location: "http://content/D" }
   ]
 }, {
   time: 5 + 6 + 7,
   frames: [
     { location: "(root)" },
     { location: "http://content/A" },
-    { location: "contentY", category: CATEGORY_MASK("css") },
+    { location: "contentY", category: CATEGORY_INDEX("layout") },
     { location: "http://content/E" },
     { location: "http://content/F" },
-    { location: "contentY", category: CATEGORY_MASK("js") },
+    { location: "contentY", category: CATEGORY_INDEX("js") },
   ]
 }, {
   time: 5 + 20,
   frames: [
     { location: "(root)" },
-    { location: "contentX", category: CATEGORY_MASK("js") },
+    { location: "contentX", category: CATEGORY_INDEX("js") },
   ]
 }, {
   time: 5 + 25,
   frames: [
     { location: "(root)" },
     { location: "http://content/A" },
-    { location: "contentZ", category: CATEGORY_MASK("gc", 1) },
+    { location: "contentZ", category: CATEGORY_INDEX("gc") },
   ]
 }]);
--- a/devtools/client/performance/test/unit/test_tree-model-08.js
+++ b/devtools/client/performance/test/unit/test_tree-model-08.js
@@ -4,17 +4,17 @@
 
 /**
  * Verifies if FrameNodes retain and parse their data appropriately.
  */
 
 add_task(function test() {
   const FrameUtils = require("devtools/client/performance/modules/logic/frame-utils");
   const { FrameNode } = require("devtools/client/performance/modules/logic/tree-model");
-  const { CATEGORY_MASK } = require("devtools/client/performance/modules/categories");
+  const { CATEGORY_INDEX } = require("devtools/client/performance/modules/categories");
   const compute = frame => {
     FrameUtils.computeIsContentAndCategory(frame);
     return frame;
   };
 
   const frames = [
     new FrameNode("hello/<.world (http://foo/bar.js:123:987)", compute({
       location: "hello/<.world (http://foo/bar.js:123:987)",
@@ -34,17 +34,17 @@ add_task(function test() {
     }), 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"),
+      category: CATEGORY_INDEX("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
--- a/devtools/client/performance/test/unit/test_tree-model-09.js
+++ b/devtools/client/performance/test/unit/test_tree-model-09.js
@@ -1,16 +1,18 @@
 /* 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_INDEX } = require("devtools/client/performance/modules/categories");
+
 add_task(function test() {
   const { ThreadNode } = require("devtools/client/performance/modules/logic/tree-model");
   const url = (n) => `http://content/${n}`;
 
   // Create a root node from a given samples array.
 
   const root = getFrameNodePath(new ThreadNode(gThread, { startTime: 5, endTime: 25,
                                                           contentOnly: true }), "(root)");
@@ -27,26 +29,29 @@ add_task(function test() {
    *       - 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"),
+  ok(getFrameNodePath(root, `${CATEGORY_INDEX("tools")}`),
     "root has platform generalized child from Chrome JS");
-  equal(getFrameNodePath(root, "9000").calls.length, 0,
+  equal(getFrameNodePath(root, `${CATEGORY_INDEX("tools")}`).calls.length, 0,
     "platform generalized child is a leaf.");
 
-  ok(getFrameNodePath(root, `${url("A")} > ${url("E")} > ${url("F")} > 9000`),
+  ok(getFrameNodePath(root,
+       `${url("A")} > ${url("E")} > ${url("F")} > ${CATEGORY_INDEX("tools")}`),
      "a second leaf of the generalized Chrome JS exists.");
 
-  equal(getFrameNodePath(root, "9000").category,
-     getFrameNodePath(root, `${url("A")} > ${url("E")} > ${url("F")} > 9000`).category,
+  equal(getFrameNodePath(root, `${CATEGORY_INDEX("tools")}`).category,
+     getFrameNodePath(root,
+       `${url("A")} > ${url("E")} > ${url("F")} > ${CATEGORY_INDEX("tools")}`
+     ).category,
      "generalized frames of same type are duplicated in top-down view");
 });
 
 var gThread = synthesizeProfileForTest([{
   time: 5,
   frames: [
     { location: "(root)" },
     { location: "http://content/A" },
--- a/devtools/client/shared/widgets/FlameGraph.js
+++ b/devtools/client/shared/widgets/FlameGraph.js
@@ -8,17 +8,19 @@ const { ELLIPSIS } = require("devtools/s
 
 loader.lazyRequireGetter(this, "defer", "devtools/shared/defer");
 loader.lazyRequireGetter(this, "EventEmitter",
   "devtools/shared/event-emitter");
 
 loader.lazyRequireGetter(this, "getColor",
   "devtools/client/shared/theme", true);
 
-loader.lazyRequireGetter(this, "CATEGORY_MAPPINGS",
+loader.lazyRequireGetter(this, "CATEGORIES",
+  "devtools/client/performance/modules/categories", true);
+loader.lazyRequireGetter(this, "CATEGORY_INDEX",
   "devtools/client/performance/modules/categories", true);
 loader.lazyRequireGetter(this, "FrameUtils",
   "devtools/client/performance/modules/logic/frame-utils");
 loader.lazyRequireGetter(this, "demangle",
   "devtools/client/shared/demangle");
 
 loader.lazyRequireGetter(this, "AbstractCanvasGraph",
   "devtools/client/shared/widgets/Graphs", true);
@@ -1287,17 +1289,18 @@ var FlameGraphUtils = {
         mutableFrameKeyOptions.isLeaf = stackDepth === 0;
         let frameKey = inflatedFrame.getFrameKey(mutableFrameKeyOptions);
 
         // If not skipping the frame, add it to the current level. The (root)
         // node isn't useful for flame graphs.
         if (frameKey !== "" && frameKey !== "(root)") {
           // If the frame is a meta category, use the category label.
           if (mutableFrameKeyOptions.isMetaCategoryOut) {
-            frameKey = CATEGORY_MAPPINGS[frameKey].label;
+            const category = CATEGORIES[frameKey] || CATEGORIES[CATEGORY_INDEX("other")];
+            frameKey = category.label;
           }
 
           sampleFrames[stackDepth] = inflatedFrame;
           sampleFrameKeys[stackDepth] = frameKey;
 
           // If we shouldn't flatten the current frame into the previous one,
           // increment the stack depth.
           if (!flattenRecursion || frameKey !== prevFrameKey) {