Visualize CPU time versus wall time for thread slices.

Thread slices now have a highlighted section to show how much of the
duration represents CPU time (darker) and how much represents wall
time (dark + lighter).

Bug: 141530588
Change-Id: I1c61cdb62e599b5782c2a91ca2da869f787e7a72
diff --git a/CHANGELOG b/CHANGELOG
index ff0a59d..a44b463 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -4,7 +4,8 @@
   Trace Processor:
     *
   UI:
-    *
+    * Added a highlighted section to thread slices to visualize CPU time 
+      (darker) verses wall time (lighter).
   SDK:
     *
 
diff --git a/docs/contributing/testing.md b/docs/contributing/testing.md
index 98f0812..6eabc54 100644
--- a/docs/contributing/testing.md
+++ b/docs/contributing/testing.md
@@ -124,6 +124,35 @@
 be ignored (for example,
 `SELECT RUN_METRIC('metric file') as surpress_query_output`)
 
+UI pixel diff tests
+-----------------
+The pixel tests are used to ensure core user journeys work by verifying they
+are the same pixel to pixel against a golden screenshot. They use a headless
+chrome to load the webpage and take a screenshot and compare pixel by pixel a
+golden screenshot. You can run these tests by using `ui/run-integrationtests`.
+
+
+These test fail when a certain number of pixels are different. If these tests
+fail, you'll need to investigate the diff and determine if its intentional. If
+its a desired change you will need to update the screenshots on a linux machine
+to get the CI to pass. You can update them by generating and uploading a new
+baseline (this requires access to a google bucket through gcloud which only
+googlers have access to, googlers can install gcloud
+[here](https://g3doc.corp.google.com/cloud/sdk/g3doc/index.md#installing-and-using-the-cloud-sdk)).
+
+```
+ui/run-integrationtests --rebaseline
+tools/add_test_data test/data/ui-screenshots
+```
+
+Once finished you can commit and upload as part of your CL to cause the CI to
+use your new screenshots.
+
+NOTE: If you see a failing diff test you can see the pixel differences on the CI
+by using the link to the UI and replace `/ui/index.html` with
+`/ui-test-artifacts/<name_of_failing_png_test_from_logs>.png`. This allows you
+to tell where in the picture the change was introduced.
+
 Android CTS tests
 -----------------
 CTS tests ensure that any vendors who modify Android remain compliant with the
diff --git a/tools/install-build-deps b/tools/install-build-deps
index 8399fce..d7c0797 100755
--- a/tools/install-build-deps
+++ b/tools/install-build-deps
@@ -234,8 +234,8 @@
     # Example traces for regression tests.
     Dependency(
         'test/data.zip',
-        'https://storage.googleapis.com/perfetto/test-data-20210715-134242.zip',
-        '4d79b592f66ee7c57811407a0e91f34491121aaab989804e137859d03b54e934',
+        'https://storage.googleapis.com/perfetto/test-data-20210715-171257.zip',
+        '7389d64a15b2ec2598340beec4db87f96da6457d39651f70eb9a759c5537b594',
         'all', 'all',
     ),
 
diff --git a/ui/src/common/colorizer.ts b/ui/src/common/colorizer.ts
index 9adde38..5ff5e78 100644
--- a/ui/src/common/colorizer.ts
+++ b/ui/src/common/colorizer.ts
@@ -159,3 +159,16 @@
   const lightness = isSelected ? 30 : hash(sliceName + 'x', 40) + 40;
   return [hue, saturation, lightness];
 }
+
+// Lightens the color for thread slices to represent wall time.
+export function hslForThreadIdleSlice(
+    hue: number,
+    saturation: number,
+    lightness: number,
+    isSelected: boolean|null): [number, number, number] {
+  // Increase lightness by 80% when selected and 40% otherwise,
+  // without exceeding 88.
+  let newLightness = isSelected ? lightness * 1.8 : lightness * 1.4;
+  newLightness = Math.min(newLightness, 88);
+  return [hue, saturation, newLightness];
+}
diff --git a/ui/src/controller/track_decider.ts b/ui/src/controller/track_decider.ts
index dd69835..f62b45f 100644
--- a/ui/src/controller/track_decider.ts
+++ b/ui/src/controller/track_decider.ts
@@ -816,13 +816,15 @@
           thread_track.name as trackName,
           tid,
           thread.name as threadName,
-          max(depth) as maxDepth,
+          max(slice.depth) as maxDepth,
+          count(thread_slice.id) > 0 as hasThreadSlice,
           process.upid as upid,
           process.pid as pid
         from slice
         join thread_track on slice.track_id = thread_track.id
         join thread using(utid)
         left join process using(upid)
+        left join thread_slice on slice.id = thread_slice.id
         group by thread_track.id
   `);
 
@@ -835,6 +837,7 @@
       maxDepth: NUM,
       upid: NUM_NULL,
       pid: NUM_NULL,
+      hasThreadSlice: NUM,
     });
     for (; it.valid(); it.next()) {
       const utid = it.utid;
@@ -845,6 +848,7 @@
       const upid = it.upid;
       const pid = it.pid;
       const maxDepth = it.maxDepth;
+      const hasThreadSlice = it.hasThreadSlice;
       const trackKindPriority =
           TrackDecider.inferTrackKindPriority(threadName, tid, pid);
 
@@ -859,7 +863,7 @@
         name,
         trackGroup: uuid,
         trackKindPriority,
-        config: {trackId, maxDepth, tid}
+        config: {trackId, maxDepth, tid, isThreadSlice: hasThreadSlice === 1}
       });
     }
   }
diff --git a/ui/src/tracks/chrome_slices/common.ts b/ui/src/tracks/chrome_slices/common.ts
index 41fc2f8..7b80743 100644
--- a/ui/src/tracks/chrome_slices/common.ts
+++ b/ui/src/tracks/chrome_slices/common.ts
@@ -20,6 +20,7 @@
   maxDepth: number;
   namespace: string;
   trackId: number;
+  isThreadSlice?: boolean;
 }
 
 export interface Data extends TrackData {
@@ -33,4 +34,5 @@
   colors?: Uint16Array;  // Index into strings.
   isInstant: Uint16Array;
   isIncomplete: Uint16Array;
+  cpuTimeRatio?: Float64Array;
 }
diff --git a/ui/src/tracks/chrome_slices/controller.ts b/ui/src/tracks/chrome_slices/controller.ts
index 3c6822a..1a41def 100644
--- a/ui/src/tracks/chrome_slices/controller.ts
+++ b/ui/src/tracks/chrome_slices/controller.ts
@@ -36,7 +36,14 @@
     // ns per quantization bucket (i.e. ns per pixel). /2 * 2 is to force it to
     // be an even number, so we can snap in the middle.
     const bucketNs = Math.max(Math.round(resolution * 1e9 * pxSize / 2) * 2, 1);
-    const tableName = this.namespaceTable('slice');
+
+    const isThreadSlice = this.config.isThreadSlice;
+    let tableName = this.namespaceTable('slice');
+    let threadDurQuery = ', dur';
+    if (isThreadSlice) {
+      tableName = this.namespaceTable('thread_slice');
+      threadDurQuery = ', iif(thread_dur IS NULL, dur, thread_dur)';
+    }
 
     if (this.maxDurNs === 0) {
       const query = `
@@ -56,6 +63,7 @@
         name,
         dur = 0 as isInstant,
         dur = -1 as isIncomplete
+        ${threadDurQuery} as threadDur
       FROM ${tableName}
       WHERE track_id = ${this.config.trackId} AND
         ts >= (${startNs - this.maxDurNs}) AND
@@ -77,6 +85,7 @@
       titles: new Uint16Array(numRows),
       isInstant: new Uint16Array(numRows),
       isIncomplete: new Uint16Array(numRows),
+      cpuTimeRatio: new Float64Array(numRows)
     };
 
     const stringIndexes = new Map<string, number>();
@@ -97,7 +106,8 @@
       sliceId: NUM,
       name: STR,
       isInstant: NUM,
-      isIncomplete: NUM
+      isIncomplete: NUM,
+      threadDur: NUM
     });
     for (let row = 0; it.valid(); it.next(), row++) {
       const startNsQ = it.tsq;
@@ -123,6 +133,16 @@
       slices.titles[row] = internString(it.name);
       slices.isInstant[row] = it.isInstant;
       slices.isIncomplete[row] = it.isIncomplete;
+
+      let cpuTimeRatio = 1;
+      if (!it.isInstant && !it.isIncomplete) {
+        // Rounding the CPU time ratio to two decimal places and ensuring
+        // it is less than or equal to one, incase the thread duration exceeds
+        // the total duration.
+        cpuTimeRatio =
+            Math.min(Math.round((it.threadDur / it.dur) * 100) / 100, 1);
+      }
+      slices.cpuTimeRatio![row] = cpuTimeRatio;
     }
     return slices;
   }
diff --git a/ui/src/tracks/chrome_slices/frontend.ts b/ui/src/tracks/chrome_slices/frontend.ts
index 403f01b..0bc9ab4 100644
--- a/ui/src/tracks/chrome_slices/frontend.ts
+++ b/ui/src/tracks/chrome_slices/frontend.ts
@@ -16,7 +16,7 @@
 
 import {Actions} from '../../common/actions';
 import {cropText, drawIncompleteSlice} from '../../common/canvas_utils';
-import {hslForSlice} from '../../common/colorizer';
+import {hslForSlice, hslForThreadIdleSlice} from '../../common/colorizer';
 import {TRACE_MARGIN_TIME_S} from '../../common/constants';
 import {TrackState} from '../../common/state';
 import {checkerboardExcept} from '../../frontend/checkerboard';
@@ -85,6 +85,7 @@
       const isIncomplete = data.isIncomplete[i];
       const title = data.strings[titleId];
       const colorOverride = data.colors && data.strings[data.colors[i]];
+      const isThreadSlice = this.config.isThreadSlice;
       if (isIncomplete) {  // incomplete slice
         tEnd = visibleWindowTime.end;
       }
@@ -103,8 +104,9 @@
       const highlighted = titleId === this.hoveredTitleId ||
           globals.frontendLocalState.highlightedSliceId === sliceId;
 
-      const [hue, saturation, lightness] =
-          hslForSlice(name, highlighted || isSelected);
+      const hasFocus = highlighted || isSelected;
+
+      const [hue, saturation, lightness] = hslForSlice(name, hasFocus);
 
       let color: string;
       if (colorOverride === undefined) {
@@ -114,6 +116,16 @@
       }
       ctx.fillStyle = color;
 
+      if (isThreadSlice) {
+        const cpuTimeRatio = data.cpuTimeRatio![i];
+        const [GradientHue, GradientSaturation, GradientLightness] =
+            hslForThreadIdleSlice(hue, saturation, lightness, hasFocus);
+        const gradientColor =
+            hsluvToHex([GradientHue, GradientSaturation, GradientLightness]);
+        ctx.fillStyle = this.createGradientForThreadSlice(
+            tStart, tEnd, cpuTimeRatio, color, gradientColor, ctx);
+      }
+
       // We draw instant events as upward facing chevrons starting at A:
       //     A
       //    ###
@@ -270,6 +282,23 @@
           !(tEnd <= visibleWindowTime.start || tStart >= visibleWindowTime.end)
     };
   }
+
+  createGradientForThreadSlice(
+      tStart: number, tEnd: number, cpuTimeRatio: number, color: string,
+      gradientColor: string, ctx: CanvasRenderingContext2D): CanvasGradient {
+    const timeScale = globals.frontendLocalState.timeScale;
+    const {start: windowStart, end: windowEnd} =
+        globals.frontendLocalState.visibleWindowTime;
+    const start = isFinite(timeScale.timeToPx(tStart)) ?
+        timeScale.timeToPx(tStart) :
+        windowStart;
+    const end = isFinite(timeScale.timeToPx(tEnd)) ? timeScale.timeToPx(tEnd) :
+                                                     windowEnd;
+    const gradient = ctx.createLinearGradient(start, 0, end, 0);
+    gradient.addColorStop(cpuTimeRatio, color);
+    gradient.addColorStop(cpuTimeRatio, gradientColor);
+    return gradient;
+  }
 }
 
 trackRegistry.register(ChromeSliceTrack);