perfetto-ui: Improvements in hovering text in flamegraph

Show the name of the mapping in hover.
Instead of showing e.g. "4479141B", show "4.5 MB" for size.
Show self size for frames that allocate.
Move hovering text goes if it goes over the edge.
Show the total size in root frame.

Covered with tests.

Change-Id: I04f21d70ff95bbe5985e17e19d03c4a8330e9076
diff --git a/ui/src/frontend/flamegraph.ts b/ui/src/frontend/flamegraph.ts
index 12b0785..704d14f 100644
--- a/ui/src/frontend/flamegraph.ts
+++ b/ui/src/frontend/flamegraph.ts
@@ -132,6 +132,15 @@
     // Draw root node.
     ctx.fillStyle = this.generateColor('root', false);
     ctx.fillRect(x, currentY, width, nodeHeight);
+    ctx.font = `${this.textSize}px Google Sans`;
+    const text = cropText(
+        `root: ${
+            this.displaySize(
+                this.totalSize, unit, unit === 'B' ? 1024 : 1000)}`,
+        charWidth,
+        width - 2);
+    ctx.fillStyle = 'black';
+    ctx.fillText(text, x + 5, currentY + nodeHeight - 4);
     currentY += nodeHeight;
 
 
@@ -219,23 +228,67 @@
 
     if (this.hoveredX > -1 && this.hoveredY > -1 && this.hoveredCallsite) {
       // Draw the tooltip.
-      const line1 = this.getCallsiteName(this.hoveredCallsite);
+      const lines: string[] = [];
+      let lineSplitter: LineSplitter;
+      const nameText = this.getCallsiteName(this.hoveredCallsite);
+      lineSplitter =
+          splitIfTooBig(nameText, width, ctx.measureText(nameText).width);
+      const nameTextWidth = lineSplitter.lineWidth;
+      lines.push(...lineSplitter.lines);
+
+      const mappingText = this.hoveredCallsite.mapping;
+      lineSplitter =
+          splitIfTooBig(mappingText, width, ctx.measureText(mappingText).width);
+      const mappingTextWidth = lineSplitter.lineWidth;
+      lines.push(...lineSplitter.lines);
+
       const percentage = this.hoveredCallsite.totalSize / this.totalSize * 100;
-      const line2 = `total: ${this.hoveredCallsite.totalSize}${unit} (${
-          percentage.toFixed(2)}%)`;
+      const totalSizeText = `total: ${
+          this.displaySize(
+              this.hoveredCallsite.totalSize,
+              unit,
+              unit === 'B' ? 1024 : 1000)} (${percentage.toFixed(2)}%)`;
+      lineSplitter = splitIfTooBig(
+          totalSizeText, width, ctx.measureText(totalSizeText).width);
+      const totalSizeTextWidth = lineSplitter.lineWidth;
+      lines.push(...lineSplitter.lines);
+
+      let selfSizeWidth = 0;
+      if (this.hoveredCallsite.selfSize > 0) {
+        const selfSizeText = `self: ${
+            this.displaySize(
+                this.hoveredCallsite.selfSize,
+                unit,
+                unit === 'B' ? 1024 : 1000)} (${percentage.toFixed(2)}%)`;
+        lineSplitter = splitIfTooBig(
+            selfSizeText, width, ctx.measureText(selfSizeText).width);
+        selfSizeWidth = lineSplitter.lineWidth;
+        lines.push(...lineSplitter.lines);
+      }
+
+      const rectWidth = Math.max(
+                            nameTextWidth,
+                            mappingTextWidth,
+                            totalSizeTextWidth,
+                            selfSizeWidth) +
+          16;
+      const rectXStart = this.hoveredX + 8 + rectWidth > width ?
+          width - rectWidth - 8 :
+          this.hoveredX + 8;
+      const rectHeight = nodeHeight * (lines.length + 1);
+      const rectYStart = this.hoveredY + 4 + rectHeight > height ?
+          height - rectHeight - 8 :
+          this.hoveredY + 4;
+
       ctx.font = '12px Google Sans';
-      const line1Width = ctx.measureText(line1).width;
-      const line2Width = ctx.measureText(line2).width;
-      const rectWidth = Math.max(line1Width, line2Width);
-      const rectYStart = this.hoveredY + 10;
-      const rectHeight = nodeHeight * 3;
-      const rectYEnd = rectYStart + rectHeight;
       ctx.fillStyle = 'rgba(255, 255, 255, 0.9)';
-      ctx.fillRect(this.hoveredX + 5, rectYStart, rectWidth + 16, rectHeight);
+      ctx.fillRect(rectXStart, rectYStart, rectWidth, rectHeight);
       ctx.fillStyle = 'hsl(200, 50%, 40%)';
       ctx.textAlign = 'left';
-      ctx.fillText(line1, this.hoveredX + 8, rectYStart + 18 /* 8 + 10s */);
-      ctx.fillText(line2, this.hoveredX + 8, rectYEnd - 8);
+      for (let i = 0; i < lines.length; i++) {
+        const line = lines[i];
+        ctx.fillText(line, rectXStart + 4, rectYStart + (i + 1) * 18);
+      }
     }
   }
 
@@ -244,6 +297,21 @@
                                                            value.name;
   }
 
+  private displaySize(totalSize: number, unit: string, step = 1024): string {
+    if (unit === '') return totalSize.toLocaleString();
+    if (totalSize === 0) return `0 ${unit}`;
+    const units = [
+      ['', 0],
+      ['K', step],
+      ['M', Math.pow(step, 2)],
+      ['G', Math.pow(step, 3)]
+    ];
+    let unitsIndex = Math.trunc(Math.log(totalSize) / Math.log(step));
+    unitsIndex = unitsIndex > units.length - 1 ? units.length - 1 : unitsIndex;
+    return `${(totalSize / +units[unitsIndex][1]).toLocaleString()} ${
+        units[unitsIndex][0]}${unit}`;
+  }
+
   onMouseMove({x, y}: {x: number, y: number}) {
     this.hoveredX = x;
     this.hoveredY = y;
@@ -303,3 +371,23 @@
     this.isThumbnail = isThumbnail;
   }
 }
+
+export interface LineSplitter {
+  lineWidth: number;
+  lines: string[];
+}
+
+export function splitIfTooBig(
+    line: string, width: number, lineWidth: number): LineSplitter {
+  if (line === '') return {lineWidth, lines: []};
+  const lines: string[] = [];
+  const charWidth = lineWidth / line.length;
+  const maxWidth = width - 32;
+  const maxLineLen = Math.trunc(maxWidth / charWidth);
+  while (line.length > 0) {
+    lines.push(line.slice(0, maxLineLen));
+    line = line.slice(maxLineLen);
+  }
+  lineWidth = Math.min(maxWidth, lineWidth);
+  return {lineWidth, lines};
+}
diff --git a/ui/src/frontend/flamegraph_unittest.ts b/ui/src/frontend/flamegraph_unittest.ts
new file mode 100644
index 0000000..87a327d
--- /dev/null
+++ b/ui/src/frontend/flamegraph_unittest.ts
@@ -0,0 +1,53 @@
+// Copyright (C) 2019 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {splitIfTooBig} from './flamegraph';
+
+test('textGoingToMultipleLines', () => {
+  const text = 'Dummy text to go to multiple lines.';
+
+  const lineSplitter = splitIfTooBig(text, 7 + 32, text.length);
+
+  expect(lineSplitter).toEqual({
+    lines: ['Dummy t', 'ext to ', 'go to m', 'ultiple', ' lines.'],
+    lineWidth: 7
+  });
+});
+
+test('emptyText', () => {
+  const text = '';
+
+  const lineSplitter = splitIfTooBig(text, 10, 5);
+
+  expect(lineSplitter).toEqual({lines: [], lineWidth: 5});
+});
+
+test('textEnoughForOneLine', () => {
+  const text = 'Dummy text to go to one lines.';
+
+  const lineSplitter = splitIfTooBig(text, text.length + 32, text.length);
+
+  expect(lineSplitter).toEqual({lines: [text], lineWidth: text.length});
+});
+
+test('textGoingToTwoLines', () => {
+  const text = 'Dummy text to go to two lines.';
+
+  const lineSplitter = splitIfTooBig(text, text.length / 2 + 32, text.length);
+
+  expect(lineSplitter).toEqual({
+    lines: ['Dummy text to g', 'o to two lines.'],
+    lineWidth: text.length / 2
+  });
+});
diff --git a/ui/src/frontend/globals.ts b/ui/src/frontend/globals.ts
index c78fc11..81ae8f6 100644
--- a/ui/src/frontend/globals.ts
+++ b/ui/src/frontend/globals.ts
@@ -51,6 +51,8 @@
   depth: number;
   name?: string;
   totalSize: number;
+  selfSize: number;
+  mapping: string;
 }
 
 export interface HeapProfileDetails {
diff --git a/ui/src/tracks/heap_profile_flamegraph/controller.ts b/ui/src/tracks/heap_profile_flamegraph/controller.ts
index b84c688..d4cf389 100644
--- a/ui/src/tracks/heap_profile_flamegraph/controller.ts
+++ b/ui/src/tracks/heap_profile_flamegraph/controller.ts
@@ -108,7 +108,9 @@
     parentId: callsite.parentId,
     depth: callsite.depth,
     name: callsite.name,
-    totalSize: callsite.totalSize
+    totalSize: callsite.totalSize,
+    mapping: callsite.mapping,
+    selfSize: callsite.selfSize
   };
 }
 
@@ -330,7 +332,7 @@
 
     const callsites = await this.query(
         `SELECT hash, name, parent_hash, depth, size, alloc_size, count,
-        alloc_count from ${tableName} ${orderBy}`);
+        alloc_count, map_name, self_size from ${tableName} ${orderBy}`);
 
     const flamegraphData: CallsiteInfo[] = new Array();
     const hashToindex: Map<number, number> = new Map();
@@ -338,8 +340,10 @@
       const hash = callsites.columns[0].longValues![i];
       const name = callsites.columns[1].stringValues![i];
       const parentHash = callsites.columns[2].longValues![i];
-      const depth = callsites.columns[3].longValues![i];
-      const totalSize = callsites.columns[sizeIndex].longValues![i];
+      const depth = +callsites.columns[3].longValues![i];
+      const totalSize = +callsites.columns[sizeIndex].longValues![i];
+      const mapping = callsites.columns[8].stringValues![i];
+      const selfSize = +callsites.columns[9].longValues![i];
       const parentId =
           hashToindex.has(+parentHash) ? hashToindex.get(+parentHash)! : -1;
       hashToindex.set(+hash, i);
@@ -347,7 +351,7 @@
       // as an id of callsite. That way, we have quicker access to parent and it
       // will stay unique.
       flamegraphData.push(
-          {id: i, totalSize: +totalSize, depth: +depth, parentId, name});
+          {id: i, totalSize, depth, parentId, name, selfSize, mapping});
     }
     return flamegraphData;
   }
@@ -396,27 +400,27 @@
     // hash.  Slices will be merged into a big slice.
     await this.query(
         `create view if not exists ${tableNameCallsiteHashNameSize} as
-      with recursive callsite_table_names(
-        id, hash, name, size, alloc_size, count, alloc_count, parent_hash,
-        depth) AS (
-      select id, hash(name) as hash, name, size, alloc_size, count, alloc_count,
-      -1, depth
-      from ${tableNameCallsiteNameSize}
-      where depth = 0
-      UNION ALL
-      SELECT cs.id, hash(cs.name, ctn.hash) as hash, cs.name, cs.size,
-      cs.alloc_size, cs.count, cs.alloc_count, ctn.hash,
-      cs.depth
-      FROM callsite_table_names ctn
-      INNER JOIN ${tableNameCallsiteNameSize} cs ON ctn.id = cs.parent_id
-      )
-      SELECT hash, name, parent_hash, depth,
-      SUM(size) as size,
-      SUM(case when alloc_size > 0 then alloc_size else 0 end) as alloc_size,
-      SUM(count) as count,
-      SUM(case when alloc_count > 0 then alloc_count else 0 end) as alloc_count
-      FROM callsite_table_names
-      group by hash`);
+        with recursive callsite_table_names(
+          id, hash, name, map_name, size, alloc_size, count, alloc_count,
+          parent_hash, depth) AS (
+        select id, hash(name) as hash, name, map_name, size, alloc_size, count,
+          alloc_count, -1, depth
+        from ${tableNameCallsiteNameSize}
+        where depth = 0
+        union all
+        select cs.id, hash(cs.name, ctn.hash) as hash, cs.name, cs.map_name,
+          cs.size, cs.alloc_size, cs.count, cs.alloc_count, ctn.hash, cs.depth
+        from callsite_table_names ctn
+        inner join ${tableNameCallsiteNameSize} cs ON ctn.id = cs.parent_id
+        )
+        select hash, name, map_name, parent_hash, depth, SUM(size) as size,
+          SUM(case when alloc_size > 0 then alloc_size else 0 end)
+            as alloc_size, SUM(count) as count,
+          SUM(case when alloc_count > 0 then alloc_count else 0 end)
+            as alloc_count
+        from callsite_table_names
+        group by hash`);
+
     // Recursive query to compute the cumulative size of each callsite.
     // Base case: We get all the callsites where the size is non-zero.
     // Recursive case: We get the callsite which is the parent of the current
@@ -428,23 +432,27 @@
     //  callsite hash.
     await this.query(`create temp table if not exists ${
         tableNameGroupedCallsitesForFlamegraph}
-        as with recursive callsite_children(hash, name, parent_hash, depth,
-          size, alloc_size, count, alloc_count) AS (
-        select hash, name, parent_hash, depth, size, alloc_size,
-          count, alloc_count
+        as with recursive callsite_children(
+          hash, name, map_name, parent_hash, depth, size, alloc_size, count,
+          alloc_count, self_size, self_alloc_size, self_count, self_alloc_count)
+        as (
+        select hash, name, map_name, parent_hash, depth, size, alloc_size,
+          count, alloc_count, size as self_size, alloc_size as self_alloc_size,
+          count as self_count, alloc_count as self_alloc_count
         from ${tableNameCallsiteHashNameSize}
         union all
-        select chns.hash, chns.name, chns.parent_hash, chns.depth, cc.size,
-        cc.alloc_size, cc.count, cc.alloc_count
+        select chns.hash, chns.name, chns.map_name, chns.parent_hash,
+          chns.depth, cc.size, cc.alloc_size, cc.count, cc.alloc_count,
+          chns.size, chns.alloc_size, chns.count, chns.alloc_count
         from ${tableNameCallsiteHashNameSize} chns
         inner join callsite_children cc on chns.hash = cc.parent_hash
         )
-        SELECT hash, name, parent_hash, depth,
-        SUM(size) as size,
-        SUM(case when alloc_size > 0 then alloc_size else 0 end) as alloc_size,
-        SUM(count) as count,
-        SUM(case when alloc_count > 0 then alloc_count else 0 end) as
-          alloc_count
+        select hash, name, map_name, parent_hash, depth, SUM(size) as size,
+          SUM(case when alloc_size > 0 then alloc_size else 0 end)
+            as alloc_size, SUM(count) as count,
+          SUM(case when alloc_count > 0 then alloc_count else 0 end) as
+            alloc_count,
+          self_size, self_alloc_size, self_count, self_alloc_count
         from callsite_children
         group by hash`);
     return tableNameGroupedCallsitesForFlamegraph;
diff --git a/ui/src/tracks/heap_profile_flamegraph/controller_unittest.ts b/ui/src/tracks/heap_profile_flamegraph/controller_unittest.ts
index 22db892..ef110dd 100644
--- a/ui/src/tracks/heap_profile_flamegraph/controller_unittest.ts
+++ b/ui/src/tracks/heap_profile_flamegraph/controller_unittest.ts
@@ -3,10 +3,42 @@
 
 test('zeroCallsitesMerged', () => {
   const callsites: CallsiteInfo[] = [
-    {id: 1, parentId: -1, name: 'A', depth: 0, totalSize: 10},
-    {id: 2, parentId: -1, name: 'B', depth: 0, totalSize: 8},
-    {id: 3, parentId: 1, name: 'A3', depth: 1, totalSize: 4},
-    {id: 4, parentId: 2, name: 'B4', depth: 1, totalSize: 4},
+    {
+      id: 1,
+      parentId: -1,
+      name: 'A',
+      depth: 0,
+      totalSize: 10,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 2,
+      parentId: -1,
+      name: 'B',
+      depth: 0,
+      totalSize: 8,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 3,
+      parentId: 1,
+      name: 'A3',
+      depth: 1,
+      totalSize: 4,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 4,
+      parentId: 2,
+      name: 'B4',
+      depth: 1,
+      totalSize: 4,
+      selfSize: 0,
+      mapping: 'x'
+    },
   ];
 
   const mergedCallsites = mergeCallsites(callsites, 5);
@@ -17,11 +49,51 @@
 
 test('zeroCallsitesMerged2', () => {
   const callsites: CallsiteInfo[] = [
-    {id: 1, parentId: -1, name: 'A', depth: 0, totalSize: 10},
-    {id: 2, parentId: -1, name: 'B', depth: 0, totalSize: 8},
-    {id: 3, parentId: 1, name: 'A3', depth: 1, totalSize: 6},
-    {id: 4, parentId: 1, name: 'A4', depth: 1, totalSize: 4},
-    {id: 5, parentId: 2, name: 'B5', depth: 1, totalSize: 8},
+    {
+      id: 1,
+      parentId: -1,
+      name: 'A',
+      depth: 0,
+      totalSize: 10,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 2,
+      parentId: -1,
+      name: 'B',
+      depth: 0,
+      totalSize: 8,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 3,
+      parentId: 1,
+      name: 'A3',
+      depth: 1,
+      totalSize: 6,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 4,
+      parentId: 1,
+      name: 'A4',
+      depth: 1,
+      totalSize: 4,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 5,
+      parentId: 2,
+      name: 'B5',
+      depth: 1,
+      totalSize: 8,
+      selfSize: 0,
+      mapping: 'x'
+    },
   ];
 
   const mergedCallsites = mergeCallsites(callsites, 5);
@@ -32,36 +104,172 @@
 
 test('twoCallsitesMerged', () => {
   const callsites: CallsiteInfo[] = [
-    {id: 1, parentId: -1, name: 'A', depth: 0, totalSize: 10},
-    {id: 2, parentId: 1, name: 'A2', depth: 1, totalSize: 5},
-    {id: 3, parentId: 1, name: 'A3', depth: 1, totalSize: 5},
+    {
+      id: 1,
+      parentId: -1,
+      name: 'A',
+      depth: 0,
+      totalSize: 10,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 2,
+      parentId: 1,
+      name: 'A2',
+      depth: 1,
+      totalSize: 5,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 3,
+      parentId: 1,
+      name: 'A3',
+      depth: 1,
+      totalSize: 5,
+      selfSize: 0,
+      mapping: 'x'
+    },
   ];
 
   const mergedCallsites = mergeCallsites(callsites, 6);
 
   expect(mergedCallsites).toEqual([
-    {id: 1, parentId: -1, name: 'A', depth: 0, totalSize: 10},
-    {id: 2, parentId: 1, name: 'A2', depth: 1, totalSize: 10},
+    {
+      id: 1,
+      parentId: -1,
+      name: 'A',
+      depth: 0,
+      totalSize: 10,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 2,
+      parentId: 1,
+      name: 'A2',
+      depth: 1,
+      totalSize: 10,
+      selfSize: 0,
+      mapping: 'x'
+    },
   ]);
 });
 
 test('manyCallsitesMerged', () => {
   const callsites: CallsiteInfo[] = [
-    {id: 1, parentId: -1, name: 'A', depth: 0, totalSize: 10},
-    {id: 2, parentId: 1, name: 'A2', depth: 1, totalSize: 5},
-    {id: 3, parentId: 1, name: 'A3', depth: 1, totalSize: 3},
-    {id: 4, parentId: 1, name: 'A4', depth: 1, totalSize: 1},
-    {id: 5, parentId: 1, name: 'A5', depth: 1, totalSize: 1},
-    {id: 6, parentId: 3, name: 'A36', depth: 2, totalSize: 1},
-    {id: 7, parentId: 4, name: 'A47', depth: 2, totalSize: 1},
-    {id: 8, parentId: 5, name: 'A58', depth: 2, totalSize: 1},
+    {
+      id: 1,
+      parentId: -1,
+      name: 'A',
+      depth: 0,
+      totalSize: 10,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 2,
+      parentId: 1,
+      name: 'A2',
+      depth: 1,
+      totalSize: 5,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 3,
+      parentId: 1,
+      name: 'A3',
+      depth: 1,
+      totalSize: 3,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 4,
+      parentId: 1,
+      name: 'A4',
+      depth: 1,
+      totalSize: 1,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 5,
+      parentId: 1,
+      name: 'A5',
+      depth: 1,
+      totalSize: 1,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 6,
+      parentId: 3,
+      name: 'A36',
+      depth: 2,
+      totalSize: 1,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 7,
+      parentId: 4,
+      name: 'A47',
+      depth: 2,
+      totalSize: 1,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 8,
+      parentId: 5,
+      name: 'A58',
+      depth: 2,
+      totalSize: 1,
+      selfSize: 0,
+      mapping: 'x'
+    },
   ];
 
   const expectedMergedCallsites: CallsiteInfo[] = [
-    {id: 1, parentId: -1, name: 'A', depth: 0, totalSize: 10},
-    {id: 2, parentId: 1, name: 'A2', depth: 1, totalSize: 5},
-    {id: 3, parentId: 1, name: 'A3', depth: 1, totalSize: 5},
-    {id: 6, parentId: 3, name: 'A36', depth: 2, totalSize: 3},
+    {
+      id: 1,
+      parentId: -1,
+      name: 'A',
+      depth: 0,
+      totalSize: 10,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 2,
+      parentId: 1,
+      name: 'A2',
+      depth: 1,
+      totalSize: 5,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 3,
+      parentId: 1,
+      name: 'A3',
+      depth: 1,
+      totalSize: 5,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 6,
+      parentId: 3,
+      name: 'A36',
+      depth: 2,
+      totalSize: 3,
+      selfSize: 0,
+      mapping: 'x'
+    },
   ];
 
   const mergedCallsites = mergeCallsites(callsites, 4);
@@ -74,23 +282,135 @@
 
 test('manyCallsitesMergedWithoutChildren', () => {
   const callsites: CallsiteInfo[] = [
-    {id: 1, parentId: -1, name: 'A', depth: 0, totalSize: 5},
-    {id: 2, parentId: -1, name: 'B', depth: 0, totalSize: 5},
-    {id: 3, parentId: 1, name: 'A3', depth: 1, totalSize: 3},
-    {id: 4, parentId: 1, name: 'A4', depth: 1, totalSize: 1},
-    {id: 5, parentId: 1, name: 'A5', depth: 1, totalSize: 1},
-    {id: 6, parentId: 2, name: 'B6', depth: 1, totalSize: 5},
-    {id: 7, parentId: 4, name: 'A47', depth: 2, totalSize: 1},
-    {id: 8, parentId: 6, name: 'B68', depth: 2, totalSize: 1},
+    {
+      id: 1,
+      parentId: -1,
+      name: 'A',
+      depth: 0,
+      totalSize: 5,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 2,
+      parentId: -1,
+      name: 'B',
+      depth: 0,
+      totalSize: 5,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 3,
+      parentId: 1,
+      name: 'A3',
+      depth: 1,
+      totalSize: 3,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 4,
+      parentId: 1,
+      name: 'A4',
+      depth: 1,
+      totalSize: 1,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 5,
+      parentId: 1,
+      name: 'A5',
+      depth: 1,
+      totalSize: 1,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 6,
+      parentId: 2,
+      name: 'B6',
+      depth: 1,
+      totalSize: 5,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 7,
+      parentId: 4,
+      name: 'A47',
+      depth: 2,
+      totalSize: 1,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 8,
+      parentId: 6,
+      name: 'B68',
+      depth: 2,
+      totalSize: 1,
+      selfSize: 0,
+      mapping: 'x'
+    },
   ];
 
   const expectedMergedCallsites: CallsiteInfo[] = [
-    {id: 1, parentId: -1, name: 'A', depth: 0, totalSize: 5},
-    {id: 2, parentId: -1, name: 'B', depth: 0, totalSize: 5},
-    {id: 3, parentId: 1, name: 'A3', depth: 1, totalSize: 5},
-    {id: 6, parentId: 2, name: 'B6', depth: 1, totalSize: 5},
-    {id: 7, parentId: 3, name: 'A47', depth: 2, totalSize: 1},
-    {id: 8, parentId: 6, name: 'B68', depth: 2, totalSize: 1},
+    {
+      id: 1,
+      parentId: -1,
+      name: 'A',
+      depth: 0,
+      totalSize: 5,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 2,
+      parentId: -1,
+      name: 'B',
+      depth: 0,
+      totalSize: 5,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 3,
+      parentId: 1,
+      name: 'A3',
+      depth: 1,
+      totalSize: 5,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 6,
+      parentId: 2,
+      name: 'B6',
+      depth: 1,
+      totalSize: 5,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 7,
+      parentId: 3,
+      name: 'A47',
+      depth: 2,
+      totalSize: 1,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 8,
+      parentId: 6,
+      name: 'B68',
+      depth: 2,
+      totalSize: 1,
+      selfSize: 0,
+      mapping: 'x'
+    },
   ];
 
   const mergedCallsites = mergeCallsites(callsites, 4);
@@ -104,18 +424,90 @@
 
 test('smallCallsitesNotNextToEachOtherInArray', () => {
   const callsites: CallsiteInfo[] = [
-    {id: 1, parentId: -1, name: 'A', depth: 0, totalSize: 20},
-    {id: 2, parentId: 1, name: 'A2', depth: 1, totalSize: 8},
-    {id: 3, parentId: 1, name: 'A3', depth: 1, totalSize: 1},
-    {id: 4, parentId: 1, name: 'A4', depth: 1, totalSize: 8},
-    {id: 5, parentId: 1, name: 'A5', depth: 1, totalSize: 3},
+    {
+      id: 1,
+      parentId: -1,
+      name: 'A',
+      depth: 0,
+      totalSize: 20,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 2,
+      parentId: 1,
+      name: 'A2',
+      depth: 1,
+      totalSize: 8,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 3,
+      parentId: 1,
+      name: 'A3',
+      depth: 1,
+      totalSize: 1,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 4,
+      parentId: 1,
+      name: 'A4',
+      depth: 1,
+      totalSize: 8,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 5,
+      parentId: 1,
+      name: 'A5',
+      depth: 1,
+      totalSize: 3,
+      selfSize: 0,
+      mapping: 'x'
+    },
   ];
 
   const expectedMergedCallsites: CallsiteInfo[] = [
-    {id: 1, parentId: -1, name: 'A', depth: 0, totalSize: 20},
-    {id: 2, parentId: 1, name: 'A2', depth: 1, totalSize: 8},
-    {id: 3, parentId: 1, name: 'A3', depth: 1, totalSize: 4},
-    {id: 4, parentId: 1, name: 'A4', depth: 1, totalSize: 8},
+    {
+      id: 1,
+      parentId: -1,
+      name: 'A',
+      depth: 0,
+      totalSize: 20,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 2,
+      parentId: 1,
+      name: 'A2',
+      depth: 1,
+      totalSize: 8,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 3,
+      parentId: 1,
+      name: 'A3',
+      depth: 1,
+      totalSize: 4,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 4,
+      parentId: 1,
+      name: 'A4',
+      depth: 1,
+      totalSize: 8,
+      selfSize: 0,
+      mapping: 'x'
+    },
   ];
 
   const mergedCallsites = mergeCallsites(callsites, 4);
@@ -129,9 +521,33 @@
 
 test('smallCallsitesNotMerged', () => {
   const callsites: CallsiteInfo[] = [
-    {id: 1, parentId: -1, name: 'A', depth: 0, totalSize: 10},
-    {id: 2, parentId: 1, name: 'A2', depth: 1, totalSize: 2},
-    {id: 3, parentId: 1, name: 'A3', depth: 1, totalSize: 2},
+    {
+      id: 1,
+      parentId: -1,
+      name: 'A',
+      depth: 0,
+      totalSize: 10,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 2,
+      parentId: 1,
+      name: 'A2',
+      depth: 1,
+      totalSize: 2,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 3,
+      parentId: 1,
+      name: 'A3',
+      depth: 1,
+      totalSize: 2,
+      selfSize: 0,
+      mapping: 'x'
+    },
   ];
 
   const mergedCallsites = mergeCallsites(callsites, 1);
@@ -141,53 +557,325 @@
 
 test('mergingRootCallsites', () => {
   const callsites: CallsiteInfo[] = [
-    {id: 1, parentId: -1, name: 'A', depth: 0, totalSize: 10},
-    {id: 2, parentId: -1, name: 'B', depth: 0, totalSize: 2},
+    {
+      id: 1,
+      parentId: -1,
+      name: 'A',
+      depth: 0,
+      totalSize: 10,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 2,
+      parentId: -1,
+      name: 'B',
+      depth: 0,
+      totalSize: 2,
+      selfSize: 0,
+      mapping: 'x'
+    },
   ];
 
   const mergedCallsites = mergeCallsites(callsites, 20);
 
   expect(mergedCallsites).toEqual([
-    {id: 1, parentId: -1, name: 'A', depth: 0, totalSize: 12},
+    {
+      id: 1,
+      parentId: -1,
+      name: 'A',
+      depth: 0,
+      totalSize: 12,
+      selfSize: 0,
+      mapping: 'x'
+    },
   ]);
 });
 
 test('largerFlamegraph', () => {
   const data: CallsiteInfo[] = [
-    {id: 1, parentId: -1, name: 'A', depth: 0, totalSize: 60},
-    {id: 2, parentId: -1, name: 'B', depth: 0, totalSize: 40},
-    {id: 3, parentId: 1, name: 'A3', depth: 1, totalSize: 25},
-    {id: 4, parentId: 1, name: 'A4', depth: 1, totalSize: 15},
-    {id: 5, parentId: 1, name: 'A5', depth: 1, totalSize: 10},
-    {id: 6, parentId: 1, name: 'A6', depth: 1, totalSize: 10},
-    {id: 7, parentId: 2, name: 'B7', depth: 1, totalSize: 30},
-    {id: 8, parentId: 2, name: 'B8', depth: 1, totalSize: 10},
-    {id: 9, parentId: 3, name: 'A39', depth: 2, totalSize: 20},
-    {id: 10, parentId: 4, name: 'A410', depth: 2, totalSize: 10},
-    {id: 11, parentId: 4, name: 'A411', depth: 2, totalSize: 3},
-    {id: 12, parentId: 4, name: 'A412', depth: 2, totalSize: 2},
-    {id: 13, parentId: 5, name: 'A513', depth: 2, totalSize: 5},
-    {id: 14, parentId: 5, name: 'A514', depth: 2, totalSize: 5},
-    {id: 15, parentId: 7, name: 'A715', depth: 2, totalSize: 10},
-    {id: 16, parentId: 7, name: 'A716', depth: 2, totalSize: 5},
-    {id: 17, parentId: 7, name: 'A717', depth: 2, totalSize: 5},
-    {id: 18, parentId: 7, name: 'A718', depth: 2, totalSize: 5},
-    {id: 19, parentId: 9, name: 'A919', depth: 3, totalSize: 10},
-    {id: 20, parentId: 17, name: 'A1720', depth: 3, totalSize: 2},
+    {
+      id: 1,
+      parentId: -1,
+      name: 'A',
+      depth: 0,
+      totalSize: 60,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 2,
+      parentId: -1,
+      name: 'B',
+      depth: 0,
+      totalSize: 40,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 3,
+      parentId: 1,
+      name: 'A3',
+      depth: 1,
+      totalSize: 25,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 4,
+      parentId: 1,
+      name: 'A4',
+      depth: 1,
+      totalSize: 15,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 5,
+      parentId: 1,
+      name: 'A5',
+      depth: 1,
+      totalSize: 10,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 6,
+      parentId: 1,
+      name: 'A6',
+      depth: 1,
+      totalSize: 10,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 7,
+      parentId: 2,
+      name: 'B7',
+      depth: 1,
+      totalSize: 30,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 8,
+      parentId: 2,
+      name: 'B8',
+      depth: 1,
+      totalSize: 10,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 9,
+      parentId: 3,
+      name: 'A39',
+      depth: 2,
+      totalSize: 20,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 10,
+      parentId: 4,
+      name: 'A410',
+      depth: 2,
+      totalSize: 10,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 11,
+      parentId: 4,
+      name: 'A411',
+      depth: 2,
+      totalSize: 3,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 12,
+      parentId: 4,
+      name: 'A412',
+      depth: 2,
+      totalSize: 2,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 13,
+      parentId: 5,
+      name: 'A513',
+      depth: 2,
+      totalSize: 5,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 14,
+      parentId: 5,
+      name: 'A514',
+      depth: 2,
+      totalSize: 5,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 15,
+      parentId: 7,
+      name: 'A715',
+      depth: 2,
+      totalSize: 10,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 16,
+      parentId: 7,
+      name: 'A716',
+      depth: 2,
+      totalSize: 5,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 17,
+      parentId: 7,
+      name: 'A717',
+      depth: 2,
+      totalSize: 5,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 18,
+      parentId: 7,
+      name: 'A718',
+      depth: 2,
+      totalSize: 5,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 19,
+      parentId: 9,
+      name: 'A919',
+      depth: 3,
+      totalSize: 10,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 20,
+      parentId: 17,
+      name: 'A1720',
+      depth: 3,
+      totalSize: 2,
+      selfSize: 0,
+      mapping: 'x'
+    },
   ];
 
   const expectedData: CallsiteInfo[] = [
-    {id: 1, parentId: -1, name: 'A', depth: 0, totalSize: 60},
-    {id: 2, parentId: -1, name: 'B', depth: 0, totalSize: 40},
-    {id: 3, parentId: 1, name: 'A3', depth: 1, totalSize: 25},
-    {id: 4, parentId: 1, name: 'A4', depth: 1, totalSize: 35},
-    {id: 7, parentId: 2, name: 'B7', depth: 1, totalSize: 30},
-    {id: 8, parentId: 2, name: 'B8', depth: 1, totalSize: 10},
-    {id: 9, parentId: 3, name: 'A39', depth: 2, totalSize: 20},
-    {id: 10, parentId: 4, name: 'A410', depth: 2, totalSize: 25},
-    {id: 15, parentId: 7, name: 'A715', depth: 2, totalSize: 25},
-    {id: 19, parentId: 9, name: 'A919', depth: 3, totalSize: 10},
-    {id: 20, parentId: 15, name: 'A1720', depth: 3, totalSize: 2},
+    {
+      id: 1,
+      parentId: -1,
+      name: 'A',
+      depth: 0,
+      totalSize: 60,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 2,
+      parentId: -1,
+      name: 'B',
+      depth: 0,
+      totalSize: 40,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 3,
+      parentId: 1,
+      name: 'A3',
+      depth: 1,
+      totalSize: 25,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 4,
+      parentId: 1,
+      name: 'A4',
+      depth: 1,
+      totalSize: 35,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 7,
+      parentId: 2,
+      name: 'B7',
+      depth: 1,
+      totalSize: 30,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 8,
+      parentId: 2,
+      name: 'B8',
+      depth: 1,
+      totalSize: 10,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 9,
+      parentId: 3,
+      name: 'A39',
+      depth: 2,
+      totalSize: 20,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 10,
+      parentId: 4,
+      name: 'A410',
+      depth: 2,
+      totalSize: 25,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 15,
+      parentId: 7,
+      name: 'A715',
+      depth: 2,
+      totalSize: 25,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 19,
+      parentId: 9,
+      name: 'A919',
+      depth: 3,
+      totalSize: 10,
+      selfSize: 0,
+      mapping: 'x'
+    },
+    {
+      id: 20,
+      parentId: 15,
+      name: 'A1720',
+      depth: 3,
+      totalSize: 2,
+      selfSize: 0,
+      mapping: 'x'
+    },
   ];
 
   // In this case, on depth 1, callsites A4, A5 and A6 should be merged and