Merge "tp: migrate chrome metrics descriptor generation to build time"
diff --git a/CHANGELOG b/CHANGELOG
index ee47b46..a80bf0d 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -4,6 +4,7 @@
   Trace Processor:
     *
   UI:
+    * Flow events are now drawn as arrows for the currently selected slice.
     *
 
 
diff --git a/src/trace_processor/metrics/android/java_heap_histogram.sql b/src/trace_processor/metrics/android/java_heap_histogram.sql
index f7cddf2..bc81600 100644
--- a/src/trace_processor/metrics/android/java_heap_histogram.sql
+++ b/src/trace_processor/metrics/android/java_heap_histogram.sql
@@ -26,8 +26,13 @@
     'android.content.ContentProvider',
     'android.content.BroadcastReceiver',
     'android.content.Context',
+    'android.content.Intent',
+    'android.content.res.ApkAssets',
     'android.os.Handler',
-    'android.graphics.Bitmap')
+    'android.os.Parcel',
+    'android.graphics.Bitmap',
+    'android.graphics.BaseCanvas',
+    'com.android.server.am.PendingIntentRecord')
   UNION ALL
   SELECT child.id, parent.category
   FROM heap_graph_class child JOIN cls_visitor parent ON parent.cls_id = child.superclass_id
diff --git a/tools/gen_cc_proto_descriptor.py b/tools/gen_cc_proto_descriptor.py
index 458fc5f..0c2dc66 100755
--- a/tools/gen_cc_proto_descriptor.py
+++ b/tools/gen_cc_proto_descriptor.py
@@ -39,8 +39,8 @@
       binary, width=80, initial_indent='    ', subsequent_indent='     ')
 
   relative_target = os.path.relpath(target, gendir)
-  include_guard = relative_target.replace('/', '_').replace('.',
-                                                            '_').upper() + '_'
+  include_guard = relative_target.replace('\\', '_').replace('/', '_').replace(
+      '.', '_').upper() + '_'
 
   with open(target, 'wb') as f:
     f.write("""/*
diff --git a/ui/src/assets/common.scss b/ui/src/assets/common.scss
index 3cd754d..b56cfcb 100644
--- a/ui/src/assets/common.scss
+++ b/ui/src/assets/common.scss
@@ -534,6 +534,7 @@
   --collapsed-background: hsla(190, 49%, 97%, 1);
   --collapsed-transparent: hsla(190, 49%, 97%, 0);
   --expanded-background: hsl(215, 22%, 19%);
+  --expanded-transparent: hsl(215, 22%, 19%, 0);
   display: grid;
   grid-template-columns: auto 1fr;
   grid-template-rows: 1fr;
@@ -547,7 +548,7 @@
     margin-top: -1px;
   }
   &[collapsed=true] {
-    background-color: var(--collapsed-background-transparent);
+    background-color: var(--collapsed-transparent);
     .shell {
       border-right: 1px solid #c7d0db;
       background-color: var(--collapsed-background);
@@ -557,7 +558,7 @@
     };
   }
   &[collapsed=false] {
-    background-color: var(--expanded-background);
+    background-color: var(--expanded-transparent);
     color: white;
     font-weight: bold;
     .shell.flash {
diff --git a/ui/src/common/colorizer.ts b/ui/src/common/colorizer.ts
index 601f9b0..79105cf 100644
--- a/ui/src/common/colorizer.ts
+++ b/ui/src/common/colorizer.ts
@@ -127,6 +127,10 @@
   return Object.assign({}, MD_PALETTE[colorIdx]);
 }
 
+export function hueForSlice(sliceName: string): number {
+  return hash(sliceName, 360);
+}
+
 export function colorForThread(thread?: {pid?: number, tid: number}): Color {
   if (thread === undefined) {
     return Object.assign({}, GREY_COLOR);
diff --git a/ui/src/controller/flow_events_controller.ts b/ui/src/controller/flow_events_controller.ts
index f6f3dce..277af85 100644
--- a/ui/src/controller/flow_events_controller.ts
+++ b/ui/src/controller/flow_events_controller.ts
@@ -52,8 +52,8 @@
 
     const query = `
       select
-        f.slice_out, t1.track_id, t1.name, (t1.ts+t1.dur),
-        f.slice_in, t2.track_id, t2.name, t2.ts
+        f.slice_out, t1.track_id, t1.name, t1.ts, (t1.ts+t1.dur), t1.depth,
+        f.slice_in, t2.track_id, t2.name, t2.ts, (t2.ts+t2.dur), t2.depth
       from flow f
       join slice t1 on f.slice_out = t1.slice_id
       join slice t2 on f.slice_in = t2.slice_id
@@ -66,25 +66,33 @@
         const beginSliceId = res.columns[0].longValues![i];
         const beginTrackId = res.columns[1].longValues![i];
         const beginSliceName = res.columns[2].stringValues![i];
-        const beginTs = fromNs(res.columns[3].longValues![i]);
+        const beginSliceStartTs = fromNs(res.columns[3].longValues![i]);
+        const beginSliceEndTs = fromNs(res.columns[4].longValues![i]);
+        const beginDepth = res.columns[5].longValues![i];
 
-        const endSliceId = res.columns[4].longValues![i];
-        const endTrackId = res.columns[5].longValues![i];
-        const endSliceName = res.columns[6].stringValues![i];
-        const endTs = fromNs(res.columns[7].longValues![i]);
+        const endSliceId = res.columns[6].longValues![i];
+        const endTrackId = res.columns[7].longValues![i];
+        const endSliceName = res.columns[8].stringValues![i];
+        const endSliceStartTs = fromNs(res.columns[9].longValues![i]);
+        const endSliceEndTs = fromNs(res.columns[10].longValues![i]);
+        const endDepth = res.columns[11].longValues![i];
 
         flows.push({
           begin: {
             trackId: beginTrackId,
             sliceId: beginSliceId,
             sliceName: beginSliceName,
-            ts: beginTs
+            sliceStartTs: beginSliceStartTs,
+            sliceEndTs: beginSliceEndTs,
+            depth: beginDepth
           },
           end: {
             trackId: endTrackId,
             sliceId: endSliceId,
             sliceName: endSliceName,
-            ts: endTs
+            sliceStartTs: endSliceStartTs,
+            sliceEndTs: endSliceEndTs,
+            depth: endDepth
           }
         });
       }
diff --git a/ui/src/frontend/flow_events_renderer.ts b/ui/src/frontend/flow_events_renderer.ts
new file mode 100644
index 0000000..51d12f7
--- /dev/null
+++ b/ui/src/frontend/flow_events_renderer.ts
@@ -0,0 +1,272 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use size 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 {hueForSlice} from '../common/colorizer';
+import {TRACK_SHELL_WIDTH} from './css_constants';
+import {FlowPoint, globals} from './globals';
+import {PanelVNode} from './panel';
+import {findUiTrackId} from './scroll_helper';
+import {SliceRect} from './track';
+import {TrackGroupPanel} from './track_group_panel';
+import {TrackPanel} from './track_panel';
+
+const TRACK_GROUP_CONNECTION_OFFSET = 5;
+const TRIANGLE_SIZE = 5;
+const CIRCLE_RADIUS = 3;
+const BEZIER_OFFSET = 30;
+
+type LineDirection = 'LEFT'|'RIGHT'|'UP'|'DOWN';
+type ConnectionType = 'TRACK'|'TRACK_GROUP';
+
+interface TrackPanelInfo {
+  panel: TrackPanel;
+  yStart: number;
+}
+
+interface TrackGroupPanelInfo {
+  panel: TrackGroupPanel;
+  yStart: number;
+  height: number;
+}
+
+function HasTrackId(obj: {}): obj is {trackId: number} {
+  return (obj as {trackId?: number}).trackId !== undefined;
+}
+
+function HasId(obj: {}): obj is {id: number} {
+  return (obj as {id?: number}).id !== undefined;
+}
+
+function HasTrackGroupId(obj: {}): obj is {trackGroupId: string} {
+  return (obj as {trackGroupId?: string}).trackGroupId !== undefined;
+}
+
+export class FlowEventsRendererArgs {
+  trackIdToTrackPanel: Map<number, TrackPanelInfo>;
+  groupIdToTrackGroupPanel: Map<string, TrackGroupPanelInfo>;
+
+  constructor(public canvasWidth: number, public canvasHeight: number) {
+    this.trackIdToTrackPanel = new Map<number, TrackPanelInfo>();
+    this.groupIdToTrackGroupPanel = new Map<string, TrackGroupPanelInfo>();
+  }
+
+  registerPanel(panel: PanelVNode, yStart: number, height: number) {
+    if (panel.state instanceof TrackPanel && HasId(panel.attrs)) {
+      const config = globals.state.tracks[panel.attrs.id].config;
+      if (HasTrackId(config)) {
+        this.trackIdToTrackPanel.set(
+            config.trackId, {panel: panel.state, yStart});
+      }
+    } else if (
+        panel.state instanceof TrackGroupPanel &&
+        HasTrackGroupId(panel.attrs)) {
+      this.groupIdToTrackGroupPanel.set(
+          panel.attrs.trackGroupId, {panel: panel.state, yStart, height});
+    }
+  }
+}
+
+export class FlowEventsRenderer {
+  private getTrackGroupIdByTrackId(trackId: number): string|undefined {
+    const uiTrackId = findUiTrackId(trackId);
+    return uiTrackId ? globals.state.tracks[uiTrackId].trackGroup : undefined;
+  }
+
+  private getTrackGroupYCoordinate(
+      args: FlowEventsRendererArgs, trackId: number): number|undefined {
+    const trackGroupId = this.getTrackGroupIdByTrackId(trackId);
+    if (!trackGroupId) {
+      return undefined;
+    }
+    const trackGroupInfo = args.groupIdToTrackGroupPanel.get(trackGroupId);
+    if (!trackGroupInfo) {
+      return undefined;
+    }
+    return trackGroupInfo.yStart + trackGroupInfo.height -
+        TRACK_GROUP_CONNECTION_OFFSET;
+  }
+
+  private getTrackYCoordinate(args: FlowEventsRendererArgs, trackId: number):
+      number|undefined {
+    return args.trackIdToTrackPanel.get(trackId) ?.yStart;
+  }
+
+  private getYConnection(
+      args: FlowEventsRendererArgs, trackId: number,
+      rect?: SliceRect): {y: number, connection: ConnectionType}|undefined {
+    if (!rect) {
+      const y = this.getTrackGroupYCoordinate(args, trackId);
+      if (y === undefined) {
+        return undefined;
+      }
+      return {y, connection: 'TRACK_GROUP'};
+    }
+    const y = (this.getTrackYCoordinate(args, trackId) || 0) + rect.top +
+        rect.height * 0.5;
+
+    return {
+      y: Math.min(Math.max(0, y), args.canvasHeight),
+      connection: 'TRACK'
+    };
+  }
+
+  private getXCoordinate(ts: number): number {
+    return globals.frontendLocalState.timeScale.timeToPx(ts);
+  }
+
+  private getSliceRect(args: FlowEventsRendererArgs, point: FlowPoint):
+      SliceRect|undefined {
+    const trackPanel = args.trackIdToTrackPanel.get(point.trackId) ?.panel;
+    if (!trackPanel) {
+      return undefined;
+    }
+    return trackPanel.getSliceRect(
+        point.sliceStartTs, point.sliceEndTs, point.depth);
+  }
+
+  render(ctx: CanvasRenderingContext2D, args: FlowEventsRendererArgs) {
+    ctx.save();
+    ctx.translate(TRACK_SHELL_WIDTH, 0);
+    ctx.rect(0, 0, args.canvasWidth - TRACK_SHELL_WIDTH, args.canvasHeight);
+    ctx.clip();
+
+    globals.boundFlows.forEach(flow => {
+      const beginSliceRect = this.getSliceRect(args, flow.begin);
+      const endSliceRect = this.getSliceRect(args, flow.end);
+
+      const beginYConnection =
+          this.getYConnection(args, flow.begin.trackId, beginSliceRect);
+      const endYConnection =
+          this.getYConnection(args, flow.end.trackId, endSliceRect);
+
+      if (!beginYConnection || !endYConnection) {
+        return;
+      }
+
+      let beginDir: LineDirection = 'LEFT';
+      let endDir: LineDirection = 'RIGHT';
+      if (beginYConnection.connection === 'TRACK_GROUP') {
+        beginDir = beginYConnection.y > endYConnection.y ? 'DOWN' : 'UP';
+      }
+      if (endYConnection.connection === 'TRACK_GROUP') {
+        endDir = endYConnection.y > beginYConnection.y ? 'DOWN' : 'UP';
+      }
+
+      const begin = {
+        x: this.getXCoordinate(flow.begin.sliceEndTs),
+        y: beginYConnection.y,
+        hue: hueForSlice(flow.begin.sliceName),
+        dir: beginDir
+      };
+      const end = {
+        x: this.getXCoordinate(flow.end.sliceStartTs),
+        y: endYConnection.y,
+        hue: hueForSlice(flow.end.sliceName),
+        dir: endDir
+      };
+      this.drawFlowArrow(ctx, begin, end);
+    });
+
+    ctx.restore();
+  }
+
+  private getDeltaX(dir: LineDirection, offset: number): number {
+    switch (dir) {
+      case 'LEFT':
+        return -offset;
+      case 'RIGHT':
+        return offset;
+      case 'UP':
+        return 0;
+      case 'DOWN':
+        return 0;
+      default:
+        return 0;
+    }
+  }
+
+  private getDeltaY(dir: LineDirection, offset: number): number {
+    switch (dir) {
+      case 'LEFT':
+        return 0;
+      case 'RIGHT':
+        return 0;
+      case 'UP':
+        return -offset;
+      case 'DOWN':
+        return offset;
+      default:
+        return 0;
+    }
+  }
+
+  private drawFlowArrow(
+      ctx: CanvasRenderingContext2D,
+      begin: {x: number, y: number, hue: number, dir: LineDirection},
+      end: {x: number, y: number, hue: number, dir: LineDirection}) {
+    const grad = ctx.createLinearGradient(begin.x, begin.y, end.x, end.y);
+    grad.addColorStop(0, `hsl(${begin.hue}, 50%, 65%)`);
+    grad.addColorStop(1, `hsl(${end.hue}, 50%, 65%)`);
+
+    const END_OFFSET =
+        (end.dir === 'RIGHT' || end.dir === 'LEFT' ? TRIANGLE_SIZE : 0);
+
+    // draw curved line from begin to end (bezier curve)
+    ctx.strokeStyle = grad;
+    ctx.lineWidth = 2;
+    ctx.beginPath();
+    ctx.moveTo(begin.x, begin.y);
+    ctx.bezierCurveTo(
+        begin.x - this.getDeltaX(begin.dir, BEZIER_OFFSET),
+        begin.y - this.getDeltaY(begin.dir, BEZIER_OFFSET),
+        end.x - this.getDeltaX(end.dir, BEZIER_OFFSET + END_OFFSET),
+        end.y - this.getDeltaY(end.dir, BEZIER_OFFSET + END_OFFSET),
+        end.x - this.getDeltaX(end.dir, END_OFFSET),
+        end.y - this.getDeltaY(end.dir, END_OFFSET));
+    ctx.stroke();
+
+    // TODO (andrewbb): probably we should add a parameter 'MarkerType' to be
+    // able to choose what marker we want to draw _before_ the function call.
+    // e.g. triangle, circle, square?
+    if (begin.dir !== 'RIGHT' && begin.dir !== 'LEFT') {
+      // draw a circle if we the line has a vertical connection
+      ctx.fillStyle = `hsl(${begin.hue}, 50%, 65%)`;
+      ctx.beginPath();
+      ctx.arc(begin.x, begin.y, 3, 0, 2 * Math.PI);
+      ctx.closePath();
+      ctx.fill();
+    }
+
+
+    if (end.dir !== 'RIGHT' && end.dir !== 'LEFT') {
+      // draw a circle if we the line has a vertical connection
+      ctx.fillStyle = `hsl(${begin.hue}, 50%, 65%)`;
+      ctx.beginPath();
+      ctx.arc(end.x, end.y, CIRCLE_RADIUS, 0, 2 * Math.PI);
+      ctx.closePath();
+      ctx.fill();
+    } else {
+      const dx = this.getDeltaX(end.dir, TRIANGLE_SIZE);
+      const dy = this.getDeltaY(end.dir, TRIANGLE_SIZE);
+      // draw small triangle
+      ctx.fillStyle = `hsl(${end.hue}, 50%, 65%)`;
+      ctx.beginPath();
+      ctx.moveTo(end.x, end.y);
+      ctx.lineTo(end.x - dx - dy, end.y + dx - dy);
+      ctx.lineTo(end.x - dx + dy, end.y - dx - dy);
+      ctx.closePath();
+      ctx.fill();
+    }
+  }
+}
\ No newline at end of file
diff --git a/ui/src/frontend/globals.ts b/ui/src/frontend/globals.ts
index 97a4727..83e9096 100644
--- a/ui/src/frontend/globals.ts
+++ b/ui/src/frontend/globals.ts
@@ -50,10 +50,14 @@
 }
 
 export interface FlowPoint {
+  trackId: number;
+
   sliceName: string;
   sliceId: number;
-  trackId: number;
-  ts: number;
+  sliceStartTs: number;
+  sliceEndTs: number;
+
+  depth: number;
 }
 
 export interface Flow {
@@ -250,8 +254,8 @@
     return assertExists(this._boundFlows);
   }
 
-  set boundFlows(click: Flow[]) {
-    this._boundFlows = assertExists(click);
+  set boundFlows(boundFlows: Flow[]) {
+    this._boundFlows = assertExists(boundFlows);
   }
 
   get counterDetails() {
diff --git a/ui/src/frontend/panel_container.ts b/ui/src/frontend/panel_container.ts
index d7199a6..4de44aa 100644
--- a/ui/src/frontend/panel_container.ts
+++ b/ui/src/frontend/panel_container.ts
@@ -17,6 +17,10 @@
 import {assertExists, assertTrue} from '../base/logging';
 
 import {TOPBAR_HEIGHT, TRACK_SHELL_WIDTH} from './css_constants';
+import {
+  FlowEventsRenderer,
+  FlowEventsRendererArgs
+} from './flow_events_renderer';
 import {globals} from './globals';
 import {isPanelVNode, Panel, PanelSize, PanelVNode} from './panel';
 import {
@@ -60,6 +64,8 @@
   private totalPanelHeight = 0;
   private canvasHeight = 0;
 
+  private flowEventsRenderer: FlowEventsRenderer;
+
   private panelPerfStats = new WeakMap<Panel, RunningStatistics>();
   private perfStats = {
     totalPanels: 0,
@@ -157,6 +163,7 @@
     this.canvasRedrawer = () => this.redrawCanvas();
     globals.rafScheduler.addRedrawCallback(this.canvasRedrawer);
     perfDisplay.addContainer(this);
+    this.flowEventsRenderer = new FlowEventsRenderer();
   }
 
   oncreate(vnodeDom: m.CVnodeDOM<Attrs>) {
@@ -326,24 +333,27 @@
     const panels = assertExists(this.attrs).panels;
     assertTrue(panels.length === this.panelPositions.length);
     let totalOnCanvas = 0;
+    const flowEventsRendererArgs =
+        new FlowEventsRendererArgs(this.parentWidth, this.canvasHeight);
     for (let i = 0; i < panels.length; i++) {
       const panel = panels[i];
       const panelHeight = this.panelPositions[i].height;
       const yStartOnCanvas = panelYStart - canvasYStart;
 
-      if (!this.overlapsCanvas(yStartOnCanvas, yStartOnCanvas + panelHeight)) {
-        panelYStart += panelHeight;
-        continue;
-      }
-
-      totalOnCanvas++;
-
       if (!isPanelVNode(panel)) {
         throw new Error('Vnode passed to panel container is not a panel');
       }
 
       // TODO(hjd): This cast should be unnecessary given the type guard above.
       const p = panel as PanelVNode<{}>;
+      flowEventsRendererArgs.registerPanel(p, yStartOnCanvas, panelHeight);
+
+      if (!this.overlapsCanvas(yStartOnCanvas, yStartOnCanvas + panelHeight)) {
+        panelYStart += panelHeight;
+        continue;
+      }
+
+      totalOnCanvas++;
 
       this.ctx.save();
       this.ctx.translate(0, yStartOnCanvas);
@@ -362,6 +372,7 @@
     this.drawTopLayerOnCanvas();
     const redrawDur = debugNow() - redrawStart;
     this.updatePerfStats(redrawDur, panels.length, totalOnCanvas);
+    this.flowEventsRenderer.render(this.ctx, flowEventsRendererArgs);
   }
 
   // The panels each draw on the canvas but some details need to be drawn across
diff --git a/ui/src/frontend/track.ts b/ui/src/frontend/track.ts
index 4df2730..c5af2f2 100644
--- a/ui/src/frontend/track.ts
+++ b/ui/src/frontend/track.ts
@@ -35,6 +35,14 @@
   create(TrackState: TrackState): Track;
 }
 
+export interface SliceRect {
+  left: number;
+  width: number;
+  top: number;
+  height: number;
+  visible: boolean;
+}
+
 /**
  * The abstract class that needs to be implemented by all tracks.
  */
@@ -116,4 +124,15 @@
       ctx.fillText(text2, xPos + 8, this.getHeight() / 2 + 6);
     }
   }
+
+  /**
+   * Returns a place where a given slice should be drawn. Should be implemented
+   * only for track types that support slices e.g. chrome_slice, async_slices
+   * tStart - slice start time in seconds, tEnd - slice end time in seconds,
+   * depth - slice depth
+   */
+  getSliceRect(_tStart: number, _tEnd: number, _depth: number): SliceRect
+      |undefined {
+    return undefined;
+  }
 }
diff --git a/ui/src/frontend/track_group_panel.ts b/ui/src/frontend/track_group_panel.ts
index 91c0933..c65af87 100644
--- a/ui/src/frontend/track_group_panel.ts
+++ b/ui/src/frontend/track_group_panel.ts
@@ -146,8 +146,14 @@
   onupdate({dom}: m.CVnodeDOM<Attrs>) {
     const shell = assertExists(dom.querySelector('.shell'));
     this.shellWidth = shell.getBoundingClientRect().width;
-    this.backgroundColor =
-        getComputedStyle(dom).getPropertyValue('--collapsed-background');
+    // TODO(andrewbb): move this to css_constants
+    if (this.trackGroupState.collapsed) {
+      this.backgroundColor =
+          getComputedStyle(dom).getPropertyValue('--collapsed-background');
+    } else {
+      this.backgroundColor =
+          getComputedStyle(dom).getPropertyValue('--expanded-background');
+    }
   }
 
   highlightIfTrackSelected(ctx: CanvasRenderingContext2D, size: PanelSize) {
@@ -169,13 +175,14 @@
 
   renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize) {
     const collapsed = this.trackGroupState.collapsed;
-    if (!collapsed) return;
-
-    ctx.save();
 
     ctx.fillStyle = this.backgroundColor;
     ctx.fillRect(0, 0, size.width, size.height);
 
+    if (!collapsed) return;
+
+    this.highlightIfTrackSelected(ctx, size);
+
     drawGridLines(
         ctx,
         globals.frontendLocalState.timeScale,
@@ -183,6 +190,7 @@
         size.width,
         size.height);
 
+    ctx.save();
     ctx.translate(this.shellWidth, 0);
     if (this.summaryTrack) {
       this.summaryTrack.render(ctx);
diff --git a/ui/src/frontend/track_panel.ts b/ui/src/frontend/track_panel.ts
index 59c7fe7..e982f99 100644
--- a/ui/src/frontend/track_panel.ts
+++ b/ui/src/frontend/track_panel.ts
@@ -25,7 +25,7 @@
 import {BLANK_CHECKBOX, CHECKBOX, STAR, STAR_BORDER} from './icons';
 import {Panel, PanelSize} from './panel';
 import {verticalScrollToTrack} from './scroll_helper';
-import {Track} from './track';
+import {SliceRect, Track} from './track';
 import {trackRegistry} from './track_registry';
 import {
   drawVerticalLineAtTime,
@@ -385,4 +385,9 @@
       }
     }
   }
+
+  getSliceRect(tStart: number, tDur: number, depth: number): SliceRect
+      |undefined {
+    return this.track.getSliceRect(tStart, tDur, depth);
+  }
 }
diff --git a/ui/src/tracks/chrome_slices/frontend.ts b/ui/src/tracks/chrome_slices/frontend.ts
index d956acf..ad7b0b7 100644
--- a/ui/src/tracks/chrome_slices/frontend.ts
+++ b/ui/src/tracks/chrome_slices/frontend.ts
@@ -14,11 +14,12 @@
 
 import {Actions} from '../../common/actions';
 import {cropText, drawIncompleteSlice} from '../../common/canvas_utils';
+import {hueForSlice} from '../../common/colorizer';
 import {TrackState} from '../../common/state';
 import {toNs} from '../../common/time';
 import {checkerboardExcept} from '../../frontend/checkerboard';
 import {globals} from '../../frontend/globals';
-import {Track} from '../../frontend/track';
+import {SliceRect, Track} from '../../frontend/track';
 import {trackRegistry} from '../../frontend/track_registry';
 
 import {Config, Data, SLICE_TRACK_KIND} from './common';
@@ -29,15 +30,6 @@
 const CHEVRON_WIDTH_PX = 10;
 const HALF_CHEVRON_WIDTH_PX = CHEVRON_WIDTH_PX / 2;
 
-function hash(s: string): number {
-  let hash = 0x811c9dc5 & 0xfffffff;
-  for (let i = 0; i < s.length; i++) {
-    hash ^= s.charCodeAt(i);
-    hash = (hash * 16777619) & 0xffffffff;
-  }
-  return hash & 0xff;
-}
-
 export class ChromeSliceTrack extends Track<Config, Data> {
   static readonly kind: string = SLICE_TRACK_KIND;
   static create(trackState: TrackState): Track {
@@ -74,7 +66,6 @@
 
     // measuretext is expensive so we only use it once.
     const charWidth = ctx.measureText('ACBDLqsdfg').width / 10;
-    const pxEnd = timeScale.timeToPx(visibleWindowTime.end);
 
     // The draw of the rect on the selected slice must happen after the other
     // drawings, otherwise it would result under another rect.
@@ -89,13 +80,13 @@
       const isInstant = data.isInstant[i];
       const title = data.strings[titleId];
       let incompleteSlice = false;
-
       if (toNs(tEnd) - toNs(tStart) === -1) {  // incomplete slice
         incompleteSlice = true;
         tEnd = tStart + INCOMPLETE_SLICE_TIME_S;
       }
 
-      if (tEnd <= visibleWindowTime.start || tStart >= visibleWindowTime.end) {
+      const rect = this.getSliceRect(tStart, tEnd, depth);
+      if (!rect || !rect.visible) {
         continue;
       }
 
@@ -104,18 +95,8 @@
           currentSelection.kind === 'CHROME_SLICE' &&
           currentSelection.id !== undefined && currentSelection.id === sliceId;
 
-      const rectXStart = Math.max(timeScale.timeToPx(tStart), 0);
-      let rectXEnd = Math.min(timeScale.timeToPx(tEnd), pxEnd);
-      let rectWidth = rectXEnd - rectXStart;
-
-      // All slices should be at least 1px.
-      if (rectWidth < 1) {
-        rectWidth = 1;
-        rectXEnd = rectXStart + 1;
-      }
-      const rectYStart = TRACK_PADDING + depth * SLICE_HEIGHT;
       const name = title.replace(/( )?\d+/g, '');
-      const hue = hash(name);
+      const hue = hueForSlice(name);
       const saturation = isSelected ? 80 : 50;
       const hovered = titleId === this.hoveredTitleId;
       const color = `hsl(${hue}, ${saturation}%, ${hovered ? 30 : 65}%)`;
@@ -130,25 +111,20 @@
       // D       B
       // Then B, C, D and back to A:
       if (isInstant) {
-        ctx.moveTo(rectXStart, rectYStart);
-        ctx.lineTo(
-            rectXStart + HALF_CHEVRON_WIDTH_PX, rectYStart + SLICE_HEIGHT);
-        ctx.lineTo(
-            rectXStart, rectYStart + SLICE_HEIGHT - HALF_CHEVRON_WIDTH_PX);
-        ctx.lineTo(
-            rectXStart - HALF_CHEVRON_WIDTH_PX, rectYStart + SLICE_HEIGHT);
-        ctx.lineTo(rectXStart, rectYStart);
+        ctx.moveTo(rect.left, rect.top);
+        ctx.lineTo(rect.left + HALF_CHEVRON_WIDTH_PX, rect.top + SLICE_HEIGHT);
+        ctx.lineTo(rect.left, rect.top + SLICE_HEIGHT - HALF_CHEVRON_WIDTH_PX);
+        ctx.lineTo(rect.left - HALF_CHEVRON_WIDTH_PX, rect.top + SLICE_HEIGHT);
+        ctx.lineTo(rect.left, rect.top);
         ctx.fill();
         continue;
       }
-
-      if (incompleteSlice && rectWidth > SLICE_HEIGHT / 4) {
+      if (incompleteSlice && rect.width > SLICE_HEIGHT / 4) {
         drawIncompleteSlice(
-            ctx, rectXStart, rectYStart, rectWidth, SLICE_HEIGHT, color);
+            ctx, rect.left, rect.top, rect.width, SLICE_HEIGHT, color);
       } else {
-        ctx.fillRect(rectXStart, rectYStart, rectWidth, SLICE_HEIGHT);
+        ctx.fillRect(rect.left, rect.top, rect.width, SLICE_HEIGHT);
       }
-
       // Selected case
       if (isSelected) {
         drawRectOnSelected = () => {
@@ -156,16 +132,16 @@
           ctx.beginPath();
           ctx.lineWidth = 3;
           ctx.strokeRect(
-              rectXStart, rectYStart - 1.5, rectWidth, SLICE_HEIGHT + 3);
+              rect.left, rect.top - 1.5, rect.width, SLICE_HEIGHT + 3);
           ctx.closePath();
         };
       }
 
       ctx.fillStyle = 'white';
-      const displayText = cropText(title, charWidth, rectWidth);
-      const rectXCenter = rectXStart + rectWidth / 2;
+      const displayText = cropText(title, charWidth, rect.width);
+      const rectXCenter = rect.left + rect.width / 2;
       ctx.textBaseline = "middle";
-      ctx.fillText(displayText, rectXCenter, rectYStart + SLICE_HEIGHT / 2);
+      ctx.fillText(displayText, rectXCenter, rect.top + SLICE_HEIGHT / 2);
     }
     drawRectOnSelected();
   }
@@ -233,6 +209,22 @@
   getHeight() {
     return SLICE_HEIGHT * (this.config.maxDepth + 1) + 2 * TRACK_PADDING;
   }
+
+  getSliceRect(tStart: number, tEnd: number, depth: number): SliceRect
+      |undefined {
+    const {timeScale, visibleWindowTime} = globals.frontendLocalState;
+    const pxEnd = timeScale.timeToPx(visibleWindowTime.end);
+    const left = Math.max(timeScale.timeToPx(tStart), 0);
+    const right = Math.min(timeScale.timeToPx(tEnd), pxEnd);
+    return {
+      left,
+      width: Math.max(right - left, 1),
+      top: TRACK_PADDING + depth * SLICE_HEIGHT,
+      height: SLICE_HEIGHT,
+      visible:
+          !(tEnd <= visibleWindowTime.start || tStart >= visibleWindowTime.end)
+    };
+  }
 }
 
 trackRegistry.register(ChromeSliceTrack);