perfetto-ui: Show slice wakeup info

Click on a slice and see the wakeup information, on the main UI
and in the details panel.

See here: https://taylori-dot-perfetto-ui.appspot.com/#!/?s=b1830d45dc57166937cb2f12d6cde9114265363ef7312eb50c6616f77bd39

Also fixed a small bug in the drawing of vertical lines.

Bug:118895197
Change-Id: Ic7d6df601281102402a2cf6e48d8628bd9ab4679
diff --git a/ui/src/assets/common.scss b/ui/src/assets/common.scss
index 3c2abfd..a1abd66 100644
--- a/ui/src/assets/common.scss
+++ b/ui/src/assets/common.scss
@@ -399,13 +399,19 @@
 
   table {
     font-size: 14px;
-    width: 100%;
-    td:first-child {
-      width: 20%;
-    }
+    width: 50%;
+    min-width: 200px;
+    max-width: 50%;
+    table-layout: fixed;
+    word-wrap: break-word;
     tr:hover {
       background-color: hsl(214, 22%, 90%);
     }
+    th {
+      text-align: left;
+      width: 30%;
+      font-weight: normal;
+    }
   }
 
 }
diff --git a/ui/src/common/canvas_utils.ts b/ui/src/common/canvas_utils.ts
new file mode 100644
index 0000000..198375f
--- /dev/null
+++ b/ui/src/common/canvas_utils.ts
@@ -0,0 +1,62 @@
+// 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.
+
+export function cropText(str: string, charWidth: number, rectWidth: number) {
+  const maxTextWidth = rectWidth - 4;
+  let displayText = '';
+  const nameLength = str.length * charWidth;
+  if (nameLength < maxTextWidth) {
+    displayText = str;
+  } else {
+    // -3 for the 3 ellipsis.
+    const displayedChars = Math.floor(maxTextWidth / charWidth) - 3;
+    if (displayedChars > 3) {
+      displayText = str.substring(0, displayedChars) + '...';
+    }
+  }
+  return displayText;
+}
+
+export function drawDoubleHeadedArrow(
+    ctx: CanvasRenderingContext2D,
+    x: number,
+    y: number,
+    length: number,
+    showArrowHeads: boolean,
+    width = 2,
+    color = 'black') {
+  ctx.beginPath();
+  ctx.lineWidth = width;
+  ctx.lineCap = 'round';
+  ctx.strokeStyle = color;
+  ctx.moveTo(x, y);
+  ctx.lineTo(x + length, y);
+  ctx.stroke();
+  ctx.closePath();
+  // Arrowheads on the each end of the line.
+  if (showArrowHeads) {
+    ctx.beginPath();
+    ctx.moveTo(x + length - 8, y - 4);
+    ctx.lineTo(x + length, y);
+    ctx.lineTo(x + length - 8, y + 4);
+    ctx.stroke();
+    ctx.closePath();
+    ctx.beginPath();
+    ctx.moveTo(x + 8, y - 4);
+    ctx.lineTo(x, y);
+    ctx.lineTo(x + 8, y + 4);
+    ctx.stroke();
+    ctx.closePath();
+  }
+}
\ No newline at end of file
diff --git a/ui/src/common/time.ts b/ui/src/common/time.ts
index 4b2b32f..33e13b8 100644
--- a/ui/src/common/time.ts
+++ b/ui/src/common/time.ts
@@ -44,7 +44,7 @@
 export function timeToCode(sec: number) {
   let result = '';
   let ns = Math.round(sec * 1e9);
-  if (ns < 1) return '0s ';
+  if (ns < 1) return '0s';
   const unitAndValue = [
     ['m', 60000000000],
     ['s', 1000000000],
@@ -61,7 +61,7 @@
       result += i.toLocaleString() + unit + ' ';
     }
   });
-  return result;
+  return result.slice(0, -1);
 }
 
 export class TimeSpan {
diff --git a/ui/src/common/time_unittest.ts b/ui/src/common/time_unittest.ts
index 479e77c..bbaacd9 100644
--- a/ui/src/common/time_unittest.ts
+++ b/ui/src/common/time_unittest.ts
@@ -15,16 +15,16 @@
 import {timeToCode} from './time';
 
 test('seconds to code', () => {
-  expect(timeToCode(3)).toEqual('3s ');
-  expect(timeToCode(60)).toEqual('1m ');
-  expect(timeToCode(63)).toEqual('1m 3s ');
-  expect(timeToCode(63.2)).toEqual('1m 3s 200ms ');
-  expect(timeToCode(63.2221)).toEqual('1m 3s 222ms 100us ');
-  expect(timeToCode(63.2221111)).toEqual('1m 3s 222ms 111us 100ns ');
-  expect(timeToCode(0.2221111)).toEqual('222ms 111us 100ns ');
-  expect(timeToCode(0.000001)).toEqual('1us ');
-  expect(timeToCode(0.000003)).toEqual('3us ');
-  expect(timeToCode(1.000001)).toEqual('1s 1us ');
-  expect(timeToCode(200.00000003)).toEqual('3m 20s 30ns ');
-  expect(timeToCode(0)).toEqual('0s ');
+  expect(timeToCode(3)).toEqual('3s');
+  expect(timeToCode(60)).toEqual('1m');
+  expect(timeToCode(63)).toEqual('1m 3s');
+  expect(timeToCode(63.2)).toEqual('1m 3s 200ms');
+  expect(timeToCode(63.2221)).toEqual('1m 3s 222ms 100us');
+  expect(timeToCode(63.2221111)).toEqual('1m 3s 222ms 111us 100ns');
+  expect(timeToCode(0.2221111)).toEqual('222ms 111us 100ns');
+  expect(timeToCode(0.000001)).toEqual('1us');
+  expect(timeToCode(0.000003)).toEqual('3us');
+  expect(timeToCode(1.000001)).toEqual('1s 1us');
+  expect(timeToCode(200.00000003)).toEqual('3m 20s 30ns');
+  expect(timeToCode(0)).toEqual('0s');
 });
diff --git a/ui/src/common/track_utils.ts b/ui/src/common/track_utils.ts
deleted file mode 100644
index 0d406d5..0000000
--- a/ui/src/common/track_utils.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-// 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.
-
-export function cropText(str: string, charWidth: number, rectWidth: number) {
-  const maxTextWidth = rectWidth - 4;
-  let displayText = '';
-  const nameLength = str.length * charWidth;
-  if (nameLength < maxTextWidth) {
-    displayText = str;
-  } else {
-    // -3 for the 3 ellipsis.
-    const displayedChars = Math.floor(maxTextWidth / charWidth) - 3;
-    if (displayedChars > 3) {
-      displayText = str.substring(0, displayedChars) + '...';
-    }
-  }
-  return displayText;
-}
\ No newline at end of file
diff --git a/ui/src/controller/selection_controller.ts b/ui/src/controller/selection_controller.ts
index acdea6c..308b09e 100644
--- a/ui/src/controller/selection_controller.ts
+++ b/ui/src/controller/selection_controller.ts
@@ -12,10 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {Controller} from './controller';
-import {globals} from './globals';
 import {Engine} from '../common/engine';
 import {fromNs} from '../common/time';
+import {SliceDetails} from '../frontend/globals';
+
+import {Controller} from './controller';
+import {globals} from './globals';
 
 export interface SelectionControllerArgs {
   engine: Engine;
@@ -40,7 +42,7 @@
     this.lastSelectedSlice = selectedSlice;
 
     if (selectedSlice !== undefined) {
-      const sqlQuery = `SELECT ts, dur, priority, end_state FROM sched
+      const sqlQuery = `SELECT ts, dur, priority, end_state, utid FROM sched
                         WHERE row_id = ${selectedSlice}`;
       this.args.engine.query(sqlQuery).then(result => {
         // Check selection is still the same on completion of query.
@@ -49,15 +51,52 @@
             selection &&
             selection.kind === 'SLICE' &&
             selection.id === selectedSlice) {
-          const ts = fromNs(result.columns[0].longValues![0] as number);
-          const timeFromStart = ts - globals.state.traceTime.startSec;
+          const ts = result.columns[0].longValues![0] as number;
+          const timeFromStart = fromNs(ts) - globals.state.traceTime.startSec;
           const dur = fromNs(result.columns[1].longValues![0] as number);
           const priority = result.columns[2].longValues![0] as number;
           const endState = result.columns[3].stringValues![0];
-          const selected = {ts: timeFromStart, dur, priority, endState};
-          globals.publish('SliceDetails', selected);
+          const selected:
+              SliceDetails = {ts: timeFromStart, dur, priority, endState};
+          const utid = result.columns[4].longValues![0];
+          this.schedulingDetails(ts, utid).then(wakeResult => {
+            Object.assign(selected, wakeResult);
+            globals.publish('SliceDetails', selected);
+          });
         }
       });
     }
   }
+
+  async schedulingDetails(ts: number, utid: number|Long) {
+    // Find the ts of the first sched_wakeup before the current slice.
+    const queryWakeupTs = `select ts from instants where name = 'sched_wakeup'
+    and ref = ${utid} and ts < ${ts} order by ts desc limit 1`;
+    const wakeupRow = await this.args.engine.queryOneRow(queryWakeupTs);
+    // Find the previous sched slice for the current utid.
+    const queryPrevSched = `select ts from sched where utid = ${utid}
+    and ts < ${ts} order by ts desc limit 1`;
+    const prevSchedRow = await this.args.engine.queryOneRow(queryPrevSched);
+    // If this is the first sched slice for this utid or if the wakeup found
+    // was after the previous slice then we know the wakeup was for this slice.
+    if (prevSchedRow[0] && wakeupRow[0] < prevSchedRow[0]) {
+      return undefined;
+    }
+    const wakeupTs = wakeupRow[0];
+    // Find the sched slice with the utid of the waker running when the
+    // sched wakeup occurred. This is the waker.
+    const queryWaker = `select utid, cpu from sched where utid =
+    (select utid from raw where name = 'sched_wakeup' and ts = ${wakeupTs})
+    and ts < ${wakeupTs} and ts + dur >= ${wakeupTs};`;
+    const wakerRow = await this.args.engine.queryOneRow(queryWaker);
+    if (wakerRow) {
+      return {
+        wakeupTs: fromNs(wakeupTs),
+        wakerUtid: wakerRow[0],
+        wakerCpu: wakerRow[1]
+      };
+    } else {
+      return undefined;
+    }
+  }
 }
diff --git a/ui/src/frontend/globals.ts b/ui/src/frontend/globals.ts
index 905110d..747c8db 100644
--- a/ui/src/frontend/globals.ts
+++ b/ui/src/frontend/globals.ts
@@ -27,6 +27,9 @@
   dur?: number;
   priority?: number;
   endState?: string;
+  wakeupTs?: number;
+  wakerUtid?: number;
+  wakerCpu?: number;
 }
 
 export interface QuantizedLoad {
diff --git a/ui/src/frontend/notes_panel.ts b/ui/src/frontend/notes_panel.ts
index e765c66..5a9605c 100644
--- a/ui/src/frontend/notes_panel.ts
+++ b/ui/src/frontend/notes_panel.ts
@@ -24,7 +24,7 @@
 import {hsl} from 'color-convert';
 
 const FLAG_WIDTH = 16;
-const MOUSE_OFFSET = 4;
+const MOUSE_OFFSET = 6;
 const FLAG = `\uE153`;
 
 function toSummary(s: string) {
diff --git a/ui/src/frontend/slice_panel.ts b/ui/src/frontend/slice_panel.ts
index 73b24cd..ca0cc19 100644
--- a/ui/src/frontend/slice_panel.ts
+++ b/ui/src/frontend/slice_panel.ts
@@ -14,8 +14,10 @@
 
 import * as m from 'mithril';
 
+import {drawDoubleHeadedArrow} from '../common/canvas_utils';
 import {translateState} from '../common/thread_state';
 import {timeToCode} from '../common/time';
+
 import {globals} from './globals';
 import {Panel, PanelSize} from './panel';
 
@@ -32,23 +34,23 @@
           '.details-panel',
           m('.details-panel-heading', `Slice Details:`),
           m('.details-table', [m('table', [
-              m('tr', m('td', `PID`), m('td', `${threadInfo.pid}`)),
+              m('tr', m('th', `PID`), m('td', `${threadInfo.pid}`)),
               m('tr',
-                m('td', `Process name`),
+                m('th', `Process name`),
                 m('td', `${threadInfo.procName}`)),
-              m('tr', m('td', `TID`), m('td', `${threadInfo.tid}`)),
+              m('tr', m('th', `TID`), m('td', `${threadInfo.tid}`)),
               m('tr',
-                m('td', `Thread name`),
+                m('th', `Thread name`),
                 m('td', `${threadInfo.threadName}`)),
               m('tr',
-                m('td', `Start time`),
+                m('th', `Start time`),
                 m('td', `${timeToCode(sliceInfo.ts)}`)),
               m('tr',
-                m('td', `Duration`),
+                m('th', `Duration`),
                 m('td', `${timeToCode(sliceInfo.dur)}`)),
-              m('tr', m('td', `Prio`), m('td', `${sliceInfo.priority}`)),
+              m('tr', m('th', `Prio`), m('td', `${sliceInfo.priority}`)),
               m('tr',
-                m('td', `End State`),
+                m('th', `End State`),
                 m('td', `${translateState(sliceInfo.endState)}`))
             ])], ));
     }
@@ -57,5 +59,65 @@
           '.details-panel', m('.details-panel-heading', `Slice Details:`, ));
   }
 }
-  renderCanvas(_ctx: CanvasRenderingContext2D, _size: PanelSize) {}
+renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize) {
+  const details = globals.sliceDetails;
+  // Show expanded details on the scheduling of the currently selected slice.
+  if (details.wakeupTs && details.wakerUtid !== undefined) {
+    const threadInfo = globals.threads.get(details.wakerUtid);
+    // Draw separation line.
+    ctx.fillStyle = '#3c4b5d';
+    ctx.fillRect(size.width / 2, 10, 1, size.height - 10);
+    ctx.font = '16px Google Sans';
+    ctx.fillText('Scheduling Latency:', size.width / 2 + 30, 30);
+    // Draw diamond and vertical line.
+    const startDraw = {x: size.width / 2 + 30, y: 52};
+    ctx.beginPath();
+    ctx.moveTo(startDraw.x, startDraw.y + 28);
+    ctx.fillStyle = 'black';
+    ctx.lineTo(startDraw.x + 6, startDraw.y + 20);
+    ctx.lineTo(startDraw.x, startDraw.y + 12);
+    ctx.lineTo(startDraw.x - 6, startDraw.y + 20);
+    ctx.fill();
+    ctx.closePath();
+    ctx.fillRect(startDraw.x - 1, startDraw.y, 2, 100);
+
+    // Wakeup explanation text.
+    ctx.font = '13px Google Sans';
+    ctx.fillStyle = '#3c4b5d';
+    if (threadInfo) {
+      const displayText =
+          `Wakeup @ ${
+                      timeToCode(
+                          details.wakeupTs - globals.state.traceTime.startSec)
+                    } on CPU ${details.wakerCpu} by`;
+      const processText = `P: ${threadInfo.procName} [${threadInfo.pid}]`;
+      const threadText = `T: ${threadInfo.threadName} [${threadInfo.tid}]`;
+      ctx.fillText(displayText, startDraw.x + 20, startDraw.y + 20);
+      ctx.fillText(processText, startDraw.x + 20, startDraw.y + 37);
+      ctx.fillText(threadText, startDraw.x + 20, startDraw.y + 55);
+    }
+
+    // Draw latency arrow and explanation text.
+    drawDoubleHeadedArrow(ctx, startDraw.x, startDraw.y + 80, 60, true);
+    if (details.ts) {
+      const displayLatency =
+          `Scheduling latency: ${
+                                 timeToCode(
+                                     details.ts -
+                                     (details.wakeupTs -
+                                      globals.state.traceTime.startSec))
+                               }`;
+      ctx.fillText(displayLatency, startDraw.x + 70, startDraw.y + 86);
+      const explain1 =
+          'This is the interval from when the task became eligible to run';
+      const explain2 =
+          '(e.g. because of notifying a wait queue it was suspended on) to';
+      const explain3 = 'when it started running.';
+      ctx.font = '10px Google Sans';
+      ctx.fillText(explain1, startDraw.x + 70, startDraw.y + 86 + 16);
+      ctx.fillText(explain2, startDraw.x + 70, startDraw.y + 86 + 16 + 12);
+      ctx.fillText(explain3, startDraw.x + 70, startDraw.y + 86 + 16 + 24);
+    }
+  }
+}
 }
diff --git a/ui/src/frontend/thread_state_panel.ts b/ui/src/frontend/thread_state_panel.ts
index 12c0055..a65cea9 100644
--- a/ui/src/frontend/thread_state_panel.ts
+++ b/ui/src/frontend/thread_state_panel.ts
@@ -36,17 +36,17 @@
           m('.details-panel-heading', 'Thread State'),
           m('.details-table', [m('table', [
               m('tr',
-                m('td', `Start time`),
+                m('th', `Start time`),
                 m('td',
                   `${
                      timeToCode(attrs.ts - globals.state.traceTime.startSec)
                    }`)),
-              m('tr', m('td', `Duration`), m('td', `${timeToCode(attrs.dur)}`)),
+              m('tr', m('th', `Duration`), m('td', `${timeToCode(attrs.dur)}`)),
               m('tr',
-                m('td', `State`),
+                m('th', `State`),
                 m('td', `${translateState(attrs.state)}`)),
               m('tr',
-                m('td', `Process`),
+                m('th', `Process`),
                 m('td', `${threadInfo.procName} [${threadInfo.pid}]`)),
             ])]));
     } else {
diff --git a/ui/src/frontend/track_group_panel.ts b/ui/src/frontend/track_group_panel.ts
index 4893f94..6dc195c 100644
--- a/ui/src/frontend/track_group_panel.ts
+++ b/ui/src/frontend/track_group_panel.ts
@@ -147,6 +147,15 @@
                               size.height,
                               `rgba(52,69,150,0.3)`);
       }
+      if (globals.state.currentSelection.kind === 'SLICE' &&
+          globals.sliceDetails.wakeupTs !== undefined) {
+        drawVerticalLineAtTime(
+            ctx,
+            localState.timeScale,
+            globals.sliceDetails.wakeupTs,
+            size.height,
+            `black`);
+      }
     }
   }
 }
diff --git a/ui/src/frontend/track_panel.ts b/ui/src/frontend/track_panel.ts
index ca75f50..950fed9 100644
--- a/ui/src/frontend/track_panel.ts
+++ b/ui/src/frontend/track_panel.ts
@@ -259,6 +259,15 @@
                               size.height,
                               `rgba(52,69,150,0.3)`);
       }
+      if (globals.state.currentSelection.kind === 'SLICE' &&
+          globals.sliceDetails.wakeupTs !== undefined) {
+        drawVerticalLineAtTime(
+            ctx,
+            localState.timeScale,
+            globals.sliceDetails.wakeupTs,
+            size.height,
+            `black`);
+      }
     }
   }
 }
diff --git a/ui/src/frontend/vertical_line_helper.ts b/ui/src/frontend/vertical_line_helper.ts
index 9f91650..59828b6 100644
--- a/ui/src/frontend/vertical_line_helper.ts
+++ b/ui/src/frontend/vertical_line_helper.ts
@@ -34,8 +34,8 @@
     ctx.strokeStyle = color;
     const prevLineWidth = ctx.lineWidth;
     ctx.lineWidth = lineWidth;
-    ctx.moveTo(xPos + (lineWidth / 2), 0);
-    ctx.lineTo(xPos + (lineWidth / 2), height);
+    ctx.moveTo(xPos, 0);
+    ctx.lineTo(xPos, height);
     ctx.stroke();
     ctx.closePath();
     ctx.lineWidth = prevLineWidth;
diff --git a/ui/src/tracks/chrome_slices/frontend.ts b/ui/src/tracks/chrome_slices/frontend.ts
index 38f937f..d83da53 100644
--- a/ui/src/tracks/chrome_slices/frontend.ts
+++ b/ui/src/tracks/chrome_slices/frontend.ts
@@ -12,8 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import {cropText} from '../../common/canvas_utils';
 import {TrackState} from '../../common/state';
-import {cropText} from '../../common/track_utils';
 import {checkerboardExcept} from '../../frontend/checkerboard';
 import {globals} from '../../frontend/globals';
 import {Track} from '../../frontend/track';
diff --git a/ui/src/tracks/cpu_slices/frontend.ts b/ui/src/tracks/cpu_slices/frontend.ts
index f24f3b8..aceba82 100644
--- a/ui/src/tracks/cpu_slices/frontend.ts
+++ b/ui/src/tracks/cpu_slices/frontend.ts
@@ -15,8 +15,9 @@
 import {search, searchEq} from '../../base/binary_search';
 import {assertTrue} from '../../base/logging';
 import {Actions} from '../../common/actions';
+import {cropText, drawDoubleHeadedArrow} from '../../common/canvas_utils';
 import {TrackState} from '../../common/state';
-import {cropText} from '../../common/track_utils';
+import {timeToString} from '../../common/time';
 import {checkerboardExcept} from '../../frontend/checkerboard';
 import {colorForThread, hueForCpu} from '../../frontend/colorizer';
 import {globals} from '../../frontend/globals';
@@ -180,26 +181,64 @@
       ctx.fillText(subTitle, rectXCenter, MARGIN_TOP + RECT_HEIGHT / 2 + 11);
     }
 
-    // Draw a rectangle around the slice that is currently selected.
     const selection = globals.state.currentSelection;
+    const details = globals.sliceDetails;
     if (selection !== null && selection.kind === 'SLICE') {
-      const sliceIndex = searchEq(data.ids, selection.id);
-      if (sliceIndex[0] !== sliceIndex[1]) {
-        const tStart = data.starts[sliceIndex[0]];
-        const tEnd = data.ends[sliceIndex[0]];
-        const utid = data.utids[sliceIndex[0]];
+      const [startIndex, endIndex] = searchEq(data.ids, selection.id);
+      if (startIndex !== endIndex) {
+        const tStart = data.starts[startIndex];
+        const tEnd = data.ends[startIndex];
+        const utid = data.utids[startIndex];
         const color = colorForThread(globals.threads.get(utid));
         const rectStart = timeScale.timeToPx(tStart);
         const rectEnd = timeScale.timeToPx(tEnd);
+        // Draw a rectangle around the slice that is currently selected.
         ctx.strokeStyle = `hsl(${color.h}, ${color.s}%, 30%)`;
         ctx.beginPath();
         ctx.lineWidth = 3;
-        ctx.moveTo(rectStart, MARGIN_TOP - 1.5);
-        ctx.lineTo(rectEnd, MARGIN_TOP - 1.5);
-        ctx.lineTo(rectEnd, MARGIN_TOP + RECT_HEIGHT + 1.5);
-        ctx.lineTo(rectStart, MARGIN_TOP + RECT_HEIGHT + 1.5);
-        ctx.lineTo(rectStart, MARGIN_TOP - 1.5);
-        ctx.stroke();
+        ctx.strokeRect(
+            rectStart, MARGIN_TOP - 1.5, rectEnd - rectStart, RECT_HEIGHT + 3);
+        ctx.closePath();
+        // Draw arrow from wakeup time of current slice.
+        if (details.wakeupTs) {
+          const wakeupPos = timeScale.timeToPx(details.wakeupTs);
+          const latencyWidth = rectStart - wakeupPos;
+          drawDoubleHeadedArrow(
+              ctx,
+              wakeupPos,
+              MARGIN_TOP + RECT_HEIGHT,
+              latencyWidth,
+              latencyWidth >= 20);
+          // Latency time with a white semi-transparent background.
+          const displayText = timeToString(tStart - details.wakeupTs);
+          const measured = ctx.measureText(displayText);
+          if (latencyWidth >= measured.width + 2) {
+            ctx.fillStyle = 'rgba(255,255,255,0.7)';
+            ctx.fillRect(
+                wakeupPos + latencyWidth / 2 - measured.width / 2 - 1,
+                MARGIN_TOP + RECT_HEIGHT - 12,
+                measured.width + 2,
+                11);
+            ctx.textBaseline = 'bottom';
+            ctx.fillStyle = 'black';
+            ctx.fillText(
+                displayText,
+                wakeupPos + (latencyWidth) / 2,
+                MARGIN_TOP + RECT_HEIGHT - 1);
+          }
+        }
+      }
+
+      // Draw diamond if the track being drawn is the cpu of the waker.
+      if (this.config.cpu === details.wakerCpu && details.wakeupTs) {
+        const wakeupPos = timeScale.timeToPx(details.wakeupTs);
+        ctx.beginPath();
+        ctx.moveTo(wakeupPos, MARGIN_TOP + RECT_HEIGHT / 2 + 8);
+        ctx.fillStyle = 'black';
+        ctx.lineTo(wakeupPos + 6, MARGIN_TOP + RECT_HEIGHT / 2);
+        ctx.lineTo(wakeupPos, MARGIN_TOP + RECT_HEIGHT / 2 - 8);
+        ctx.lineTo(wakeupPos - 6, MARGIN_TOP + RECT_HEIGHT / 2);
+        ctx.fill();
         ctx.closePath();
       }
     }
diff --git a/ui/src/tracks/thread_state/frontend.ts b/ui/src/tracks/thread_state/frontend.ts
index 03ccbbf..ebae655 100644
--- a/ui/src/tracks/thread_state/frontend.ts
+++ b/ui/src/tracks/thread_state/frontend.ts
@@ -15,9 +15,9 @@
 
 import {search, searchEq} from '../../base/binary_search';
 import {Actions} from '../../common/actions';
+import {cropText} from '../../common/canvas_utils';
 import {TrackState} from '../../common/state';
 import {translateState} from '../../common/thread_state';
-import {cropText} from '../../common/track_utils';
 import {colorForState} from '../../frontend/colorizer';
 import {globals} from '../../frontend/globals';
 import {Track} from '../../frontend/track';