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, searchSegment} from '../../base/binary_search';
16import {assertTrue} from '../../base/logging';
17import {Actions} from '../../common/actions';
18import {cropText, drawDoubleHeadedArrow} from '../../common/canvas_utils';
19import {colorForThread} from '../../common/colorizer';
20import {TrackState} from '../../common/state';
21import {timeToString} from '../../common/time';
22import {checkerboardExcept} from '../../frontend/checkerboard';
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} from './common';
32
33const MARGIN_TOP = 3;
34const RECT_HEIGHT = 24;
35const TRACK_HEIGHT = MARGIN_TOP * 2 + RECT_HEIGHT;
36
37class CpuSliceTrack extends Track<Config, Data> {
38  static readonly kind = CPU_SLICE_TRACK_KIND;
39  static create(trackState: TrackState): CpuSliceTrack {
40    return new CpuSliceTrack(trackState);
41  }
42
43  private mouseXpos?: number;
44  private utidHoveredInThisTrack = -1;
45
46  constructor(trackState: TrackState) {
47    super(trackState);
48  }
49
50  getHeight(): number {
51    return TRACK_HEIGHT;
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 (data === undefined) return;  // Can't possibly draw anything.
60
61    // If the cached trace slices don't fully cover the visible time range,
62    // show a gray rectangle with a "Loading..." label.
63    checkerboardExcept(
64        ctx,
65        this.getHeight(),
66        timeScale.timeToPx(visibleWindowTime.start),
67        timeScale.timeToPx(visibleWindowTime.end),
68        timeScale.timeToPx(data.start),
69        timeScale.timeToPx(data.end));
70
71    this.renderSlices(ctx, data);
72  }
73
74  renderSlices(ctx: CanvasRenderingContext2D, data: Data): void {
75    const {timeScale, visibleWindowTime} = globals.frontendLocalState;
76    assertTrue(data.starts.length === data.ends.length);
77    assertTrue(data.starts.length === data.utids.length);
78
79    ctx.textAlign = 'center';
80    ctx.font = '12px Roboto Condensed';
81    const charWidth = ctx.measureText('dbpqaouk').width / 8;
82
83    const rawStartIdx =
84        data.ends.findIndex(end => end >= visibleWindowTime.start);
85    const startIdx = rawStartIdx === -1 ? 0 : rawStartIdx;
86
87    const [, rawEndIdx] = searchSegment(data.starts, visibleWindowTime.end);
88    const endIdx = rawEndIdx === -1 ? data.starts.length : rawEndIdx;
89
90    for (let i = startIdx; i < endIdx; i++) {
91      const tStart = data.starts[i];
92      const tEnd = data.ends[i];
93      const utid = data.utids[i];
94
95      const rectStart = timeScale.timeToPx(tStart);
96      const rectEnd = timeScale.timeToPx(tEnd);
97      const rectWidth = Math.max(1, rectEnd - rectStart);
98
99      const threadInfo = globals.threads.get(utid);
100      const pid = threadInfo && threadInfo.pid ? threadInfo.pid : -1;
101
102      const isHovering = globals.frontendLocalState.hoveredUtid !== -1;
103      const isThreadHovered = globals.frontendLocalState.hoveredUtid === utid;
104      const isProcessHovered = globals.frontendLocalState.hoveredPid === pid;
105      const color = colorForThread(threadInfo);
106      if (isHovering && !isThreadHovered) {
107        if (!isProcessHovered) {
108          color.l = 90;
109          color.s = 0;
110        } else {
111          color.l = Math.min(color.l + 30, 80);
112          color.s -= 20;
113        }
114      } else {
115        color.l = Math.min(color.l + 10, 60);
116        color.s -= 20;
117      }
118      ctx.fillStyle = `hsl(${color.h}, ${color.s}%, ${color.l}%)`;
119      ctx.fillRect(rectStart, MARGIN_TOP, rectWidth, RECT_HEIGHT);
120
121      // Don't render text when we have less than 5px to play with.
122      if (rectWidth < 5) continue;
123
124      // TODO: consider de-duplicating this code with the copied one from
125      // chrome_slices/frontend.ts.
126      let title = `[utid:${utid}]`;
127      let subTitle = '';
128      if (threadInfo) {
129        if (threadInfo.pid) {
130          let procName = threadInfo.procName || '';
131          if (procName.startsWith('/')) {  // Remove folder paths from name
132            procName = procName.substring(procName.lastIndexOf('/') + 1);
133          }
134          title = `${procName} [${threadInfo.pid}]`;
135          subTitle = `${threadInfo.threadName} [${threadInfo.tid}]`;
136        } else {
137          title = `${threadInfo.threadName} [${threadInfo.tid}]`;
138        }
139      }
140      title = cropText(title, charWidth, rectWidth);
141      subTitle = cropText(subTitle, charWidth, rectWidth);
142      const rectXCenter = rectStart + rectWidth / 2;
143      ctx.fillStyle = '#fff';
144      ctx.font = '12px Roboto Condensed';
145      ctx.fillText(title, rectXCenter, MARGIN_TOP + RECT_HEIGHT / 2 - 1);
146      ctx.fillStyle = 'rgba(255, 255, 255, 0.6)';
147      ctx.font = '10px Roboto Condensed';
148      ctx.fillText(subTitle, rectXCenter, MARGIN_TOP + RECT_HEIGHT / 2 + 9);
149    }
150
151    const selection = globals.state.currentSelection;
152    const details = globals.sliceDetails;
153    if (selection !== null && selection.kind === 'SLICE') {
154      const [startIndex, endIndex] = searchEq(data.ids, selection.id);
155      if (startIndex !== endIndex) {
156        const tStart = data.starts[startIndex];
157        const tEnd = data.ends[startIndex];
158        const utid = data.utids[startIndex];
159        const color = colorForThread(globals.threads.get(utid));
160        const rectStart = timeScale.timeToPx(tStart);
161        const rectEnd = timeScale.timeToPx(tEnd);
162        const rectWidth = Math.max(1, rectEnd - rectStart);
163
164        // Draw a rectangle around the slice that is currently selected.
165        ctx.strokeStyle = `hsl(${color.h}, ${color.s}%, 30%)`;
166        ctx.beginPath();
167        ctx.lineWidth = 3;
168        ctx.strokeRect(rectStart, MARGIN_TOP - 1.5, rectWidth, RECT_HEIGHT + 3);
169        ctx.closePath();
170        // Draw arrow from wakeup time of current slice.
171        if (details.wakeupTs) {
172          const wakeupPos = timeScale.timeToPx(details.wakeupTs);
173          const latencyWidth = rectStart - wakeupPos;
174          drawDoubleHeadedArrow(
175              ctx,
176              wakeupPos,
177              MARGIN_TOP + RECT_HEIGHT,
178              latencyWidth,
179              latencyWidth >= 20);
180          // Latency time with a white semi-transparent background.
181          const displayText = timeToString(tStart - details.wakeupTs);
182          const measured = ctx.measureText(displayText);
183          if (latencyWidth >= measured.width + 2) {
184            ctx.fillStyle = 'rgba(255,255,255,0.7)';
185            ctx.fillRect(
186                wakeupPos + latencyWidth / 2 - measured.width / 2 - 1,
187                MARGIN_TOP + RECT_HEIGHT - 12,
188                measured.width + 2,
189                11);
190            ctx.textBaseline = 'bottom';
191            ctx.fillStyle = 'black';
192            ctx.fillText(
193                displayText,
194                wakeupPos + (latencyWidth) / 2,
195                MARGIN_TOP + RECT_HEIGHT - 1);
196          }
197        }
198      }
199
200      // Draw diamond if the track being drawn is the cpu of the waker.
201      if (this.config.cpu === details.wakerCpu && details.wakeupTs) {
202        const wakeupPos = Math.floor(timeScale.timeToPx(details.wakeupTs));
203        ctx.beginPath();
204        ctx.moveTo(wakeupPos, MARGIN_TOP + RECT_HEIGHT / 2 + 8);
205        ctx.fillStyle = 'black';
206        ctx.lineTo(wakeupPos + 6, MARGIN_TOP + RECT_HEIGHT / 2);
207        ctx.lineTo(wakeupPos, MARGIN_TOP + RECT_HEIGHT / 2 - 8);
208        ctx.lineTo(wakeupPos - 6, MARGIN_TOP + RECT_HEIGHT / 2);
209        ctx.fill();
210        ctx.closePath();
211      }
212    }
213
214    const hoveredThread = globals.threads.get(this.utidHoveredInThisTrack);
215    if (hoveredThread !== undefined && this.mouseXpos !== undefined) {
216      const tidText = `T: ${hoveredThread.threadName} [${hoveredThread.tid}]`;
217      if (hoveredThread.pid) {
218        const pidText = `P: ${hoveredThread.procName} [${hoveredThread.pid}]`;
219        this.drawTrackHoverTooltip(ctx, this.mouseXpos, pidText, tidText);
220      } else {
221        this.drawTrackHoverTooltip(ctx, this.mouseXpos, tidText);
222      }
223    }
224  }
225
226  onMouseMove({x, y}: {x: number, y: number}) {
227    const data = this.data();
228    this.mouseXpos = x;
229    if (data === undefined) return;
230    const {timeScale} = globals.frontendLocalState;
231    if (y < MARGIN_TOP || y > MARGIN_TOP + RECT_HEIGHT) {
232      this.utidHoveredInThisTrack = -1;
233      globals.frontendLocalState.setHoveredUtidAndPid(-1, -1);
234      return;
235    }
236    const t = timeScale.pxToTime(x);
237    let hoveredUtid = -1;
238
239    for (let i = 0; i < data.starts.length; i++) {
240      const tStart = data.starts[i];
241      const tEnd = data.ends[i];
242      const utid = data.utids[i];
243      if (tStart <= t && t <= tEnd) {
244        hoveredUtid = utid;
245        break;
246      }
247    }
248    this.utidHoveredInThisTrack = hoveredUtid;
249    const threadInfo = globals.threads.get(hoveredUtid);
250    const hoveredPid = threadInfo ? (threadInfo.pid ? threadInfo.pid : -1) : -1;
251    globals.frontendLocalState.setHoveredUtidAndPid(hoveredUtid, hoveredPid);
252  }
253
254  onMouseOut() {
255    this.utidHoveredInThisTrack = -1;
256    globals.frontendLocalState.setHoveredUtidAndPid(-1, -1);
257    this.mouseXpos = 0;
258  }
259
260  onMouseClick({x}: {x: number}) {
261    const data = this.data();
262    if (data === undefined) return false;
263    const {timeScale} = globals.frontendLocalState;
264    const time = timeScale.pxToTime(x);
265    const index = search(data.starts, time);
266    const id = index === -1 ? undefined : data.ids[index];
267    if (!id || this.utidHoveredInThisTrack === -1) return false;
268    globals.makeSelection(
269        Actions.selectSlice({id, trackId: this.trackState.id}));
270    return true;
271  }
272}
273
274trackRegistry.register(CpuSliceTrack);
275