Home | History | Annotate | Download | only in cpu_slices
      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 
     15 import {search, searchEq} from '../../base/binary_search';
     16 import {assertTrue} from '../../base/logging';
     17 import {Actions} from '../../common/actions';
     18 import {cropText, drawDoubleHeadedArrow} from '../../common/canvas_utils';
     19 import {TrackState} from '../../common/state';
     20 import {timeToString} from '../../common/time';
     21 import {checkerboardExcept} from '../../frontend/checkerboard';
     22 import {colorForThread, hueForCpu} from '../../frontend/colorizer';
     23 import {globals} from '../../frontend/globals';
     24 import {Track} from '../../frontend/track';
     25 import {trackRegistry} from '../../frontend/track_registry';
     26 
     27 import {
     28   Config,
     29   CPU_SLICE_TRACK_KIND,
     30   Data,
     31   SliceData,
     32   SummaryData
     33 } from './common';
     34 
     35 
     36 const MARGIN_TOP = 5;
     37 const RECT_HEIGHT = 30;
     38 
     39 class 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 
    321 trackRegistry.register(CpuSliceTrack);
    322