1// Copyright (C) 2018 The Android Open Source Project
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//      http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15import {search, searchEq} from '../../base/binary_search';
16import {assertTrue} from '../../base/logging';
17import {Actions} from '../../common/actions';
18import {cropText, drawDoubleHeadedArrow} from '../../common/canvas_utils';
19import {TrackState} from '../../common/state';
20import {timeToString} from '../../common/time';
21import {checkerboardExcept} from '../../frontend/checkerboard';
22import {colorForThread, hueForCpu} from '../../frontend/colorizer';
23import {globals} from '../../frontend/globals';
24import {Track} from '../../frontend/track';
25import {trackRegistry} from '../../frontend/track_registry';
26
27import {
28  Config,
29  CPU_SLICE_TRACK_KIND,
30  Data,
31  SliceData,
32  SummaryData
33} from './common';
34
35
36const MARGIN_TOP = 5;
37const RECT_HEIGHT = 30;
38
39class CpuSliceTrack extends Track<Config, Data> {
40  static readonly kind = CPU_SLICE_TRACK_KIND;
41  static create(trackState: TrackState): CpuSliceTrack {
42    return new CpuSliceTrack(trackState);
43  }
44
45  private mouseXpos?: number;
46  private hue: number;
47  private utidHoveredInThisTrack = -1;
48
49  constructor(trackState: TrackState) {
50    super(trackState);
51    this.hue = hueForCpu(this.config.cpu);
52  }
53
54  renderCanvas(ctx: CanvasRenderingContext2D): void {
55    // TODO: fonts and colors should come from the CSS and not hardcoded here.
56    const {timeScale, visibleWindowTime} = globals.frontendLocalState;
57    const data = this.data();
58
59    // If there aren't enough cached slices data in |data| request more to
60    // the controller.
61    const inRange = data !== undefined &&
62        (visibleWindowTime.start >= data.start &&
63         visibleWindowTime.end <= data.end);
64    if (!inRange || data === undefined ||
65        data.resolution !== globals.getCurResolution()) {
66      globals.requestTrackData(this.trackState.id);
67    }
68    if (data === undefined) return;  // Can't possibly draw anything.
69
70    // If the cached trace slices don't fully cover the visible time range,
71    // show a gray rectangle with a "Loading..." label.
72    checkerboardExcept(
73        ctx,
74        timeScale.timeToPx(visibleWindowTime.start),
75        timeScale.timeToPx(visibleWindowTime.end),
76        timeScale.timeToPx(data.start),
77        timeScale.timeToPx(data.end));
78
79    if (data.kind === 'summary') {
80      this.renderSummary(ctx, data);
81    } else if (data.kind === 'slice') {
82      this.renderSlices(ctx, data);
83    }
84  }
85
86  renderSummary(ctx: CanvasRenderingContext2D, data: SummaryData): void {
87    const {timeScale, visibleWindowTime} = globals.frontendLocalState;
88    const startPx = Math.floor(timeScale.timeToPx(visibleWindowTime.start));
89    const bottomY = MARGIN_TOP + RECT_HEIGHT;
90
91    let lastX = startPx;
92    let lastY = bottomY;
93
94    ctx.fillStyle = `hsl(${this.hue}, 50%, 60%)`;
95    ctx.beginPath();
96    ctx.moveTo(lastX, lastY);
97    for (let i = 0; i < data.utilizations.length; i++) {
98      const utilization = data.utilizations[i];
99      const startTime = i * data.bucketSizeSeconds + data.start;
100
101      lastX = Math.floor(timeScale.timeToPx(startTime));
102
103      ctx.lineTo(lastX, lastY);
104      lastY = MARGIN_TOP + Math.round(RECT_HEIGHT * (1 - utilization));
105      ctx.lineTo(lastX, lastY);
106    }
107    ctx.lineTo(lastX, bottomY);
108    ctx.closePath();
109    ctx.fill();
110  }
111
112  renderSlices(ctx: CanvasRenderingContext2D, data: SliceData): void {
113    const {timeScale, visibleWindowTime} = globals.frontendLocalState;
114    assertTrue(data.starts.length === data.ends.length);
115    assertTrue(data.starts.length === data.utids.length);
116
117    ctx.textAlign = 'center';
118    ctx.font = '12px Google Sans';
119    const charWidth = ctx.measureText('dbpqaouk').width / 8;
120
121    for (let i = 0; i < data.starts.length; i++) {
122      const tStart = data.starts[i];
123      const tEnd = data.ends[i];
124      const utid = data.utids[i];
125      if (tEnd <= visibleWindowTime.start || tStart >= visibleWindowTime.end) {
126        continue;
127      }
128      const rectStart = timeScale.timeToPx(tStart);
129      const rectEnd = timeScale.timeToPx(tEnd);
130      const rectWidth = rectEnd - rectStart;
131      if (rectWidth < 0.3) continue;
132
133      const threadInfo = globals.threads.get(utid);
134
135      // TODO: consider de-duplicating this code with the copied one from
136      // chrome_slices/frontend.ts.
137      let title = `[utid:${utid}]`;
138      let subTitle = '';
139      let pid = -1;
140      if (threadInfo) {
141        if (threadInfo.pid) {
142          pid = threadInfo.pid;
143          const procName = threadInfo.procName || '';
144          title = `${procName} [${threadInfo.pid}]`;
145          subTitle = `${threadInfo.threadName} [${threadInfo.tid}]`;
146        } else {
147          title = `${threadInfo.threadName} [${threadInfo.tid}]`;
148        }
149      }
150
151      const isHovering = globals.frontendLocalState.hoveredUtid !== -1;
152      const isThreadHovered = globals.frontendLocalState.hoveredUtid === utid;
153      const isProcessHovered = globals.frontendLocalState.hoveredPid === pid;
154      const color = colorForThread(threadInfo);
155      if (isHovering && !isThreadHovered) {
156        if (!isProcessHovered) {
157          color.l = 90;
158          color.s = 0;
159        } else {
160          color.l = Math.min(color.l + 30, 80);
161          color.s -= 20;
162        }
163      } else {
164        color.l = Math.min(color.l + 10, 60);
165        color.s -= 20;
166      }
167      ctx.fillStyle = `hsl(${color.h}, ${color.s}%, ${color.l}%)`;
168      ctx.fillRect(rectStart, MARGIN_TOP, rectEnd - rectStart, RECT_HEIGHT);
169
170      // Don't render text when we have less than 5px to play with.
171      if (rectWidth < 5) continue;
172
173      title = cropText(title, charWidth, rectWidth);
174      subTitle = cropText(subTitle, charWidth, rectWidth);
175      const rectXCenter = rectStart + rectWidth / 2;
176      ctx.fillStyle = '#fff';
177      ctx.font = '12px Google Sans';
178      ctx.fillText(title, rectXCenter, MARGIN_TOP + RECT_HEIGHT / 2 - 3);
179      ctx.fillStyle = 'rgba(255, 255, 255, 0.6)';
180      ctx.font = '10px Google Sans';
181      ctx.fillText(subTitle, rectXCenter, MARGIN_TOP + RECT_HEIGHT / 2 + 11);
182    }
183
184    const selection = globals.state.currentSelection;
185    const details = globals.sliceDetails;
186    if (selection !== null && selection.kind === 'SLICE') {
187      const [startIndex, endIndex] = searchEq(data.ids, selection.id);
188      if (startIndex !== endIndex) {
189        const tStart = data.starts[startIndex];
190        const tEnd = data.ends[startIndex];
191        const utid = data.utids[startIndex];
192        const color = colorForThread(globals.threads.get(utid));
193        const rectStart = timeScale.timeToPx(tStart);
194        const rectEnd = timeScale.timeToPx(tEnd);
195        // Draw a rectangle around the slice that is currently selected.
196        ctx.strokeStyle = `hsl(${color.h}, ${color.s}%, 30%)`;
197        ctx.beginPath();
198        ctx.lineWidth = 3;
199        ctx.strokeRect(
200            rectStart, MARGIN_TOP - 1.5, rectEnd - rectStart, RECT_HEIGHT + 3);
201        ctx.closePath();
202        // Draw arrow from wakeup time of current slice.
203        if (details.wakeupTs) {
204          const wakeupPos = timeScale.timeToPx(details.wakeupTs);
205          const latencyWidth = rectStart - wakeupPos;
206          drawDoubleHeadedArrow(
207              ctx,
208              wakeupPos,
209              MARGIN_TOP + RECT_HEIGHT,
210              latencyWidth,
211              latencyWidth >= 20);
212          // Latency time with a white semi-transparent background.
213          const displayText = timeToString(tStart - details.wakeupTs);
214          const measured = ctx.measureText(displayText);
215          if (latencyWidth >= measured.width + 2) {
216            ctx.fillStyle = 'rgba(255,255,255,0.7)';
217            ctx.fillRect(
218                wakeupPos + latencyWidth / 2 - measured.width / 2 - 1,
219                MARGIN_TOP + RECT_HEIGHT - 12,
220                measured.width + 2,
221                11);
222            ctx.textBaseline = 'bottom';
223            ctx.fillStyle = 'black';
224            ctx.fillText(
225                displayText,
226                wakeupPos + (latencyWidth) / 2,
227                MARGIN_TOP + RECT_HEIGHT - 1);
228          }
229        }
230      }
231
232      // Draw diamond if the track being drawn is the cpu of the waker.
233      if (this.config.cpu === details.wakerCpu && details.wakeupTs) {
234        const wakeupPos = timeScale.timeToPx(details.wakeupTs);
235        ctx.beginPath();
236        ctx.moveTo(wakeupPos, MARGIN_TOP + RECT_HEIGHT / 2 + 8);
237        ctx.fillStyle = 'black';
238        ctx.lineTo(wakeupPos + 6, MARGIN_TOP + RECT_HEIGHT / 2);
239        ctx.lineTo(wakeupPos, MARGIN_TOP + RECT_HEIGHT / 2 - 8);
240        ctx.lineTo(wakeupPos - 6, MARGIN_TOP + RECT_HEIGHT / 2);
241        ctx.fill();
242        ctx.closePath();
243      }
244    }
245
246    const hoveredThread = globals.threads.get(this.utidHoveredInThisTrack);
247    if (hoveredThread !== undefined) {
248      let line1 = '';
249      let line2 = '';
250      if (hoveredThread.pid) {
251        line1 = `P: ${hoveredThread.procName} [${hoveredThread.pid}]`;
252        line2 = `T: ${hoveredThread.threadName} [${hoveredThread.tid}]`;
253      } else {
254        line1 = `T: ${hoveredThread.threadName} [${hoveredThread.tid}]`;
255      }
256
257      ctx.font = '10px Google Sans';
258      const line1Width = ctx.measureText(line1).width;
259      const line2Width = ctx.measureText(line2).width;
260      const width = Math.max(line1Width, line2Width);
261
262      ctx.fillStyle = 'rgba(255, 255, 255, 0.9)';
263      ctx.fillRect(this.mouseXpos!, MARGIN_TOP, width + 16, RECT_HEIGHT);
264      ctx.fillStyle = 'hsl(200, 50%, 40%)';
265      ctx.textAlign = 'left';
266      ctx.fillText(line1, this.mouseXpos! + 8, 18);
267      ctx.fillText(line2, this.mouseXpos! + 8, 28);
268    }
269  }
270
271  onMouseMove({x, y}: {x: number, y: number}) {
272    const data = this.data();
273    this.mouseXpos = x;
274    if (data === undefined || data.kind === 'summary') return;
275    const {timeScale} = globals.frontendLocalState;
276    if (y < MARGIN_TOP || y > MARGIN_TOP + RECT_HEIGHT) {
277      this.utidHoveredInThisTrack = -1;
278      globals.frontendLocalState.setHoveredUtidAndPid(-1, -1);
279      return;
280    }
281    const t = timeScale.pxToTime(x);
282    let hoveredUtid = -1;
283
284    for (let i = 0; i < data.starts.length; i++) {
285      const tStart = data.starts[i];
286      const tEnd = data.ends[i];
287      const utid = data.utids[i];
288      if (tStart <= t && t <= tEnd) {
289        hoveredUtid = utid;
290        break;
291      }
292    }
293    this.utidHoveredInThisTrack = hoveredUtid;
294    const threadInfo = globals.threads.get(hoveredUtid);
295    const hoveredPid = threadInfo ? (threadInfo.pid ? threadInfo.pid : -1) : -1;
296    globals.frontendLocalState.setHoveredUtidAndPid(hoveredUtid, hoveredPid);
297  }
298
299  onMouseOut() {
300    this.utidHoveredInThisTrack = -1;
301    globals.frontendLocalState.setHoveredUtidAndPid(-1, -1);
302    this.mouseXpos = 0;
303  }
304
305  onMouseClick({x}: {x: number}) {
306    const data = this.data();
307    if (data === undefined || data.kind === 'summary') return false;
308    const {timeScale} = globals.frontendLocalState;
309    const time = timeScale.pxToTime(x);
310    const index = search(data.starts, time);
311    const id = index === -1 ? undefined : data.ids[index];
312    if (id && this.utidHoveredInThisTrack !== -1) {
313      globals.dispatch(Actions.selectSlice(
314        {utid: this.utidHoveredInThisTrack, id}));
315      return true;
316    }
317    return false;
318  }
319}
320
321trackRegistry.register(CpuSliceTrack);
322