ui: Crop incomplete slices

For incomplete slices, use fixed pixel width (20px) instead of
|visibleWindowTime.end|. The default is disabled.

Change-Id: I6f3ef6a47ef98233d8116595968fccb04b6593e7
diff --git a/ui/src/common/canvas_utils.ts b/ui/src/common/canvas_utils.ts
index 2d09617..ad78f96 100644
--- a/ui/src/common/canvas_utils.ts
+++ b/ui/src/common/canvas_utils.ts
@@ -75,7 +75,11 @@
     x: number,
     y: number,
     width: number,
-    height: number) {
+    height: number,
+    showGradient: boolean = true) {
+  if (width <= 0 || height <= 0) {
+    return;
+  }
   ctx.beginPath();
   const triangleSize = height / 4;
   ctx.moveTo(x, y);
@@ -92,10 +96,12 @@
 
   const fillStyle = ctx.fillStyle;
   if (isString(fillStyle)) {
-    const gradient = ctx.createLinearGradient(x, y, x + width, y + height);
-    gradient.addColorStop(0.66, fillStyle);
-    gradient.addColorStop(1, '#FFFFFF');
-    ctx.fillStyle = gradient;
+    if (showGradient) {
+      const gradient = ctx.createLinearGradient(x, y, x + width, y + height);
+      gradient.addColorStop(0.66, fillStyle);
+      gradient.addColorStop(1, '#FFFFFF');
+      ctx.fillStyle = gradient;
+    }
   } else {
     throw new Error(
         `drawIncompleteSlice() expects fillStyle to be a simple color not ${
diff --git a/ui/src/frontend/base_slice_track.ts b/ui/src/frontend/base_slice_track.ts
index c0c637e..0995ff2 100644
--- a/ui/src/frontend/base_slice_track.ts
+++ b/ui/src/frontend/base_slice_track.ts
@@ -41,6 +41,7 @@
 import {PxSpan, TimeScale} from './time_scale';
 import {NewTrackArgs, TrackBase} from './track';
 import {BUCKETS_PER_PIXEL, CacheKey, TrackCache} from './track_cache';
+import {featureFlags} from '../core/feature_flags';
 
 // The common class that underpins all tracks drawing slices.
 
@@ -52,6 +53,14 @@
 const SLICE_MIN_WIDTH_PX = 1 / BUCKETS_PER_PIXEL;
 const CHEVRON_WIDTH_PX = 10;
 const DEFAULT_SLICE_COLOR = UNEXPECTED_PINK;
+const INCOMPLETE_SLICE_WIDTH_PX = 20;
+
+export const CROP_INCOMPLETE_SLICE_FLAG = featureFlags.register({
+  id: 'cropIncompleteSlice',
+  name: 'Crop incomplete Slice',
+  description: 'Display incomplete slice in short form',
+  defaultValue: false,
+});
 
 // Exposed and standalone to allow for testing without making this
 // visible to subclasses.
@@ -391,8 +400,16 @@
         slice.x -= CHEVRON_WIDTH_PX / 2;
         slice.w = CHEVRON_WIDTH_PX;
       } else if (slice.flags & SLICE_FLAGS_INCOMPLETE) {
-        slice.x = Math.max(slice.x, 0);
-        slice.w = pxEnd - slice.x;
+        let widthPx;
+        if (CROP_INCOMPLETE_SLICE_FLAG.get()) {
+          widthPx = slice.x > 0 ? Math.min(pxEnd, INCOMPLETE_SLICE_WIDTH_PX) :
+              Math.max(0, INCOMPLETE_SLICE_WIDTH_PX + slice.x);
+          slice.x = Math.max(slice.x, 0);
+        } else {
+          slice.x = Math.max(slice.x, 0);
+          widthPx = pxEnd - slice.x;
+        }
+        slice.w = widthPx;
       } else {
         // If the slice is an actual slice, intersect the slice geometry with
         // the visible viewport (this affects only the first and last slice).
@@ -429,8 +446,8 @@
       if (slice.flags & SLICE_FLAGS_INSTANT) {
         this.drawChevron(ctx, slice.x, y, sliceHeight);
       } else if (slice.flags & SLICE_FLAGS_INCOMPLETE) {
-        const w = Math.max(slice.w - 2, 2);
-        drawIncompleteSlice(ctx, slice.x, y, w, sliceHeight);
+        const w = CROP_INCOMPLETE_SLICE_FLAG.get() ? slice.w : Math.max(slice.w - 2, 2);
+        drawIncompleteSlice(ctx, slice.x, y, w, sliceHeight, !CROP_INCOMPLETE_SLICE_FLAG.get());
       } else {
         const w = Math.max(slice.w, SLICE_MIN_WIDTH_PX);
         ctx.fillRect(slice.x, y, w, sliceHeight);
@@ -798,7 +815,12 @@
     }
 
     for (const slice of this.incomplete) {
-      if (slice.depth === depth && slice.x <= x) {
+      const startPx = CROP_INCOMPLETE_SLICE_FLAG.get() ?
+        globals.frontendLocalState.visibleTimeScale.timeToPx(slice.startNsQ) : slice.x;
+      const cropUnfinishedSlicesCondition = CROP_INCOMPLETE_SLICE_FLAG.get() ?
+        startPx + INCOMPLETE_SLICE_WIDTH_PX >= x : true;
+
+      if (slice.depth === depth && startPx <= x && cropUnfinishedSlicesCondition) {
         return slice;
       }
     }
diff --git a/ui/src/frontend/slice_track_base.ts b/ui/src/frontend/slice_track_base.ts
index bf66782..ddf8874 100644
--- a/ui/src/frontend/slice_track_base.ts
+++ b/ui/src/frontend/slice_track_base.ts
@@ -24,12 +24,14 @@
 import {checkerboardExcept} from './checkerboard';
 import {globals} from './globals';
 import {PxSpan, TimeScale} from './time_scale';
+import {CROP_INCOMPLETE_SLICE_FLAG} from './base_slice_track';
 
 export const SLICE_TRACK_KIND = 'ChromeSliceTrack';
 const SLICE_HEIGHT = 18;
 const TRACK_PADDING = 2;
 const CHEVRON_WIDTH_PX = 10;
 const HALF_CHEVRON_WIDTH_PX = CHEVRON_WIDTH_PX / 2;
+const INCOMPLETE_SLICE_WIDTH_PX = 20;
 
 export interface SliceData extends TrackData {
   // Slices are stored in a columnar fashion.
@@ -115,7 +117,7 @@
       if (isIncomplete) {  // incomplete slice
         // TODO(stevegolton): This isn't exactly equivalent, ideally we should
         // choose tEnd once we've converted to screen space coords.
-        tEnd = visibleWindowTime.end.toTime('ceil');
+        tEnd = this.getEndTimeIfInComplete(tStart);
       }
 
       if (!visibleTimeSpan.intersects(tStart, tEnd)) {
@@ -187,7 +189,8 @@
       }
 
       if (isIncomplete && rect.width > SLICE_HEIGHT / 4) {
-        drawIncompleteSlice(ctx, rect.left, rect.top, rect.width, SLICE_HEIGHT);
+        drawIncompleteSlice(ctx, rect.left, rect.top, rect.width,
+            SLICE_HEIGHT, !CROP_INCOMPLETE_SLICE_FLAG.get());
       } else if (
           data.cpuTimeRatio !== undefined && data.cpuTimeRatio[i] < 1 - 1e-9) {
         // We draw two rectangles, representing the ratio between wall time and
@@ -248,7 +251,6 @@
     if (data === undefined) return;
     const {
       visibleTimeScale: timeScale,
-      visibleWindowTime: visibleHPTimeSpan,
     } = globals.frontendLocalState;
     if (y < TRACK_PADDING) return;
     const instantWidthTime = timeScale.pxDeltaToDuration(HALF_CHEVRON_WIDTH_PX);
@@ -269,7 +271,8 @@
         const end = Time.fromRaw(data.ends[i]);
         let tEnd = HighPrecisionTime.fromTime(end);
         if (data.isIncomplete[i]) {
-          tEnd = visibleHPTimeSpan.end;
+          const endTime = this.getEndTimeIfInComplete(start);
+          tEnd = HighPrecisionTime.fromTime(endTime);
         }
         if (tStart.lte(t) && t.lte(tEnd)) {
           return i;
@@ -278,6 +281,18 @@
     }
   }
 
+  getEndTimeIfInComplete(start: time) : time {
+    const {visibleTimeScale, visibleWindowTime} = globals.frontendLocalState;
+
+    let end = visibleWindowTime.end.toTime('ceil');
+    if (CROP_INCOMPLETE_SLICE_FLAG.get()) {
+      const widthTime = visibleTimeScale.pxDeltaToDuration(INCOMPLETE_SLICE_WIDTH_PX).toTime();
+      end = Time.add(start, widthTime);
+    }
+
+    return end;
+  }
+
   onMouseMove({x, y}: {x: number, y: number}) {
     this.hoveredTitleId = -1;
     globals.dispatch(Actions.setHighlightedSliceId({sliceId: -1}));