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 * as m from 'mithril'; 16 17import {Actions} from '../common/actions'; 18import {LogExists, LogExistsKey} from '../common/logs'; 19import {QueryResponse} from '../common/queries'; 20import {TimeSpan} from '../common/time'; 21 22import {copyToClipboard} from './clipboard'; 23import {DragGestureHandler} from './drag_gesture_handler'; 24import {globals} from './globals'; 25import {LogPanel} from './logs_panel'; 26import {NotesEditorPanel, NotesPanel} from './notes_panel'; 27import {OverviewTimelinePanel} from './overview_timeline_panel'; 28import {createPage} from './pages'; 29import {PanAndZoomHandler} from './pan_and_zoom_handler'; 30import {Panel} from './panel'; 31import {AnyAttrsVnode, PanelContainer} from './panel_container'; 32import {SliceDetailsPanel} from './slice_panel'; 33import {ThreadStatePanel} from './thread_state_panel'; 34import {TimeAxisPanel} from './time_axis_panel'; 35import {computeZoom} from './time_scale'; 36import {TimeSelectionPanel} from './time_selection_panel'; 37import {TRACK_SHELL_WIDTH} from './track_constants'; 38import {TrackGroupPanel} from './track_group_panel'; 39import {TrackPanel} from './track_panel'; 40 41const DRAG_HANDLE_HEIGHT_PX = 28; 42const DEFAULT_DETAILS_HEIGHT_PX = 230 + DRAG_HANDLE_HEIGHT_PX; 43const UP_ICON = 'keyboard_arrow_up'; 44const DOWN_ICON = 'keyboard_arrow_down'; 45 46function hasLogs(): boolean { 47 const data = globals.trackDataStore.get(LogExistsKey) as LogExists; 48 return data && data.exists; 49} 50 51class QueryTable extends Panel { 52 view() { 53 const resp = globals.queryResults.get('command') as QueryResponse; 54 if (resp === undefined) { 55 return m(''); 56 } 57 const cols = []; 58 for (const col of resp.columns) { 59 cols.push(m('td', col)); 60 } 61 const header = m('tr', cols); 62 63 const rows = []; 64 for (let i = 0; i < resp.rows.length; i++) { 65 const cells = []; 66 for (const col of resp.columns) { 67 cells.push(m('td', resp.rows[i][col])); 68 } 69 rows.push(m('tr', cells)); 70 } 71 return m( 72 'div', 73 m('header.overview', 74 `Query result - ${Math.round(resp.durationMs)} ms`, 75 m('span.code', resp.query), 76 resp.error ? null : 77 m('button.query-ctrl', 78 { 79 onclick: () => { 80 const lines: string[][] = []; 81 lines.push(resp.columns); 82 for (const row of resp.rows) { 83 const line = []; 84 for (const col of resp.columns) { 85 line.push(row[col].toString()); 86 } 87 lines.push(line); 88 } 89 copyToClipboard( 90 lines.map(line => line.join('\t')).join('\n')); 91 }, 92 }, 93 'Copy as .tsv'), 94 m('button.query-ctrl', 95 { 96 onclick: () => { 97 globals.queryResults.delete('command'); 98 globals.rafScheduler.scheduleFullRedraw(); 99 } 100 }, 101 'Close'), ), 102 resp.error ? 103 m('.query-error', `SQL error: ${resp.error}`) : 104 m('table.query-table', m('thead', header), m('tbody', rows))); 105 } 106 107 renderCanvas() {} 108} 109 110interface DragHandleAttrs { 111 height: number; 112 resize: (height: number) => void; 113} 114 115class DragHandle implements m.ClassComponent<DragHandleAttrs> { 116 private dragStartHeight = 0; 117 private height = 0; 118 private resize: (height: number) => void = () => {}; 119 private isClosed = this.height <= DRAG_HANDLE_HEIGHT_PX; 120 121 oncreate({dom, attrs}: m.CVnodeDOM<DragHandleAttrs>) { 122 this.resize = attrs.resize; 123 this.height = attrs.height; 124 const elem = dom as HTMLElement; 125 new DragGestureHandler( 126 elem, 127 this.onDrag.bind(this), 128 this.onDragStart.bind(this), 129 this.onDragEnd.bind(this)); 130 } 131 132 onupdate({attrs}: m.CVnodeDOM<DragHandleAttrs>) { 133 this.resize = attrs.resize; 134 this.height = attrs.height; 135 } 136 137 onDrag(_x: number, y: number) { 138 const newHeight = this.dragStartHeight + (DRAG_HANDLE_HEIGHT_PX / 2) - y; 139 this.isClosed = Math.floor(newHeight) <= DRAG_HANDLE_HEIGHT_PX; 140 this.resize(Math.floor(newHeight)); 141 globals.rafScheduler.scheduleFullRedraw(); 142 } 143 144 onDragStart(_x: number, _y: number) { 145 this.dragStartHeight = this.height; 146 } 147 148 onDragEnd() {} 149 150 view() { 151 const icon = this.isClosed ? UP_ICON : DOWN_ICON; 152 const title = this.isClosed ? 'Show panel' : 'Hide panel'; 153 return m( 154 '.handle', 155 m('.handle-title', 'Current Selection'), 156 m('i.material-icons', 157 { 158 onclick: () => { 159 if (this.height === DRAG_HANDLE_HEIGHT_PX) { 160 this.isClosed = false; 161 this.resize(DEFAULT_DETAILS_HEIGHT_PX); 162 } else { 163 this.isClosed = true; 164 this.resize(DRAG_HANDLE_HEIGHT_PX); 165 } 166 globals.rafScheduler.scheduleFullRedraw(); 167 }, 168 title 169 }, 170 icon)); 171 } 172} 173 174/** 175 * Top-most level component for the viewer page. Holds tracks, brush timeline, 176 * panels, and everything else that's part of the main trace viewer page. 177 */ 178class TraceViewer implements m.ClassComponent { 179 private onResize: () => void = () => {}; 180 private zoomContent?: PanAndZoomHandler; 181 private detailsHeight = DRAG_HANDLE_HEIGHT_PX; 182 // Used to set details panel to default height on selection. 183 private showDetailsPanel = true; 184 // Used to prevent global deselection if a pan/drag select occurred. 185 private keepCurrentSelection = false; 186 187 oncreate(vnode: m.CVnodeDOM) { 188 const frontendLocalState = globals.frontendLocalState; 189 const updateDimensions = () => { 190 const rect = vnode.dom.getBoundingClientRect(); 191 frontendLocalState.timeScale.setLimitsPx( 192 0, rect.width - TRACK_SHELL_WIDTH); 193 }; 194 195 updateDimensions(); 196 197 // TODO: Do resize handling better. 198 this.onResize = () => { 199 updateDimensions(); 200 globals.rafScheduler.scheduleFullRedraw(); 201 }; 202 203 // Once ResizeObservers are out, we can stop accessing the window here. 204 window.addEventListener('resize', this.onResize); 205 206 const panZoomEl = 207 vnode.dom.querySelector('.pan-and-zoom-content') as HTMLElement; 208 209 this.zoomContent = new PanAndZoomHandler({ 210 element: panZoomEl, 211 contentOffsetX: TRACK_SHELL_WIDTH, 212 onPanned: (pannedPx: number) => { 213 this.keepCurrentSelection = true; 214 const traceTime = globals.state.traceTime; 215 const vizTime = globals.frontendLocalState.visibleWindowTime; 216 const origDelta = vizTime.duration; 217 const tDelta = frontendLocalState.timeScale.deltaPxToDuration(pannedPx); 218 let tStart = vizTime.start + tDelta; 219 let tEnd = vizTime.end + tDelta; 220 if (tStart < traceTime.startSec) { 221 tStart = traceTime.startSec; 222 tEnd = tStart + origDelta; 223 } else if (tEnd > traceTime.endSec) { 224 tEnd = traceTime.endSec; 225 tStart = tEnd - origDelta; 226 } 227 frontendLocalState.updateVisibleTime(new TimeSpan(tStart, tEnd)); 228 globals.rafScheduler.scheduleRedraw(); 229 }, 230 onZoomed: (zoomedPositionPx: number, zoomRatio: number) => { 231 // TODO(hjd): Avoid hardcoding TRACK_SHELL_WIDTH. 232 // TODO(hjd): Improve support for zooming in overview timeline. 233 const span = frontendLocalState.visibleWindowTime; 234 const scale = frontendLocalState.timeScale; 235 const zoomPx = zoomedPositionPx - TRACK_SHELL_WIDTH; 236 const newSpan = computeZoom(scale, span, 1 - zoomRatio, zoomPx); 237 frontendLocalState.updateVisibleTime(newSpan); 238 globals.rafScheduler.scheduleRedraw(); 239 }, 240 onDragSelect: (selectStartPx: number|null, selectEndPx: number) => { 241 if (!selectStartPx) return; 242 this.keepCurrentSelection = true; 243 globals.frontendLocalState.setShowTimeSelectPreview(false); 244 const traceTime = globals.state.traceTime; 245 const scale = frontendLocalState.timeScale; 246 const startPx = Math.min(selectStartPx, selectEndPx); 247 const endPx = Math.max(selectStartPx, selectEndPx); 248 const startTs = Math.max(traceTime.startSec, 249 scale.pxToTime(startPx - TRACK_SHELL_WIDTH)); 250 const endTs = Math.min(traceTime.endSec, 251 scale.pxToTime(endPx - TRACK_SHELL_WIDTH)); 252 globals.dispatch(Actions.selectTimeSpan({startTs, endTs})); 253 globals.rafScheduler.scheduleRedraw(); 254 } 255 }); 256 } 257 258 onremove() { 259 window.removeEventListener('resize', this.onResize); 260 if (this.zoomContent) this.zoomContent.shutdown(); 261 } 262 263 view() { 264 const scrollingPanels: AnyAttrsVnode[] = 265 globals.state.scrollingTracks.map(id => m(TrackPanel, {key: id, id})); 266 267 for (const group of Object.values(globals.state.trackGroups)) { 268 scrollingPanels.push(m(TrackGroupPanel, { 269 trackGroupId: group.id, 270 key: `trackgroup-${group.id}`, 271 })); 272 if (group.collapsed) continue; 273 for (const trackId of group.tracks) { 274 scrollingPanels.push(m(TrackPanel, { 275 key: `track-${group.id}-${trackId}`, 276 id: trackId, 277 })); 278 } 279 } 280 scrollingPanels.unshift(m(QueryTable)); 281 282 const detailsPanels: AnyAttrsVnode[] = []; 283 const curSelection = globals.state.currentSelection; 284 if (curSelection) { 285 switch (curSelection.kind) { 286 case 'NOTE': 287 detailsPanels.push(m(NotesEditorPanel, { 288 key: 'notes', 289 id: curSelection.id, 290 })); 291 break; 292 case 'SLICE': 293 detailsPanels.push(m(SliceDetailsPanel, { 294 key: 'slice', 295 utid: curSelection.utid, 296 })); 297 break; 298 case 'THREAD_STATE': 299 detailsPanels.push(m(ThreadStatePanel, { 300 key: 'thread_state', 301 ts: curSelection.ts, 302 dur: curSelection.dur, 303 utid: curSelection.utid, 304 state: curSelection.state 305 })); 306 break; 307 default: 308 break; 309 } 310 } else if (hasLogs()) { 311 detailsPanels.push(m(LogPanel, {})); 312 } 313 314 this.showDetailsPanel = detailsPanels.length > 0; 315 316 return m( 317 '.page', 318 m('.pan-and-zoom-content', 319 { 320 onclick: () => { 321 // We don't want to deselect when panning/drag selecting. 322 if (this.keepCurrentSelection) { 323 this.keepCurrentSelection = false; 324 return; 325 } 326 globals.dispatch(Actions.deselect({})); 327 } 328 }, 329 m('.pinned-panel-container', m(PanelContainer, { 330 doesScroll: false, 331 panels: [ 332 m(OverviewTimelinePanel, {key: 'overview'}), 333 m(TimeAxisPanel, {key: 'timeaxis'}), 334 m(TimeSelectionPanel, {key: 'timeselection'}), 335 m(NotesPanel, {key: 'notes'}), 336 ...globals.state.pinnedTracks.map( 337 id => m(TrackPanel, {key: id, id})), 338 ], 339 })), 340 m('.scrolling-panel-container', m(PanelContainer, { 341 doesScroll: true, 342 panels: scrollingPanels, 343 }))), 344 m('.details-content', 345 { 346 style: { 347 height: `${this.detailsHeight}px`, 348 display: this.showDetailsPanel ? null : 'none' 349 } 350 }, 351 m(DragHandle, { 352 resize: (height: number) => { 353 this.detailsHeight = Math.max(height, DRAG_HANDLE_HEIGHT_PX); 354 }, 355 height: this.detailsHeight, 356 }), 357 m('.details-panel-container', 358 m(PanelContainer, {doesScroll: true, panels: detailsPanels})))); 359 } 360} 361 362export const ViewerPage = createPage({ 363 view() { 364 return m(TraceViewer); 365 } 366}); 367