1// Copyright (C) 2019 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 m from 'mithril'; 16 17import {Actions} from '../common/actions'; 18import {isEmptyData} from '../common/aggregation_data'; 19import {LogExists, LogExistsKey} from '../common/logs'; 20import {pluginManager} from '../common/plugins'; 21import {addSelectionChangeObserver} from '../common/selection_observer'; 22import {Selection} from '../common/state'; 23import {DebugSliceDetailsTab} from '../tracks/debug/details_tab'; 24import {SCROLL_JANK_PLUGIN_ID} from '../tracks/scroll_jank'; 25import {TOP_LEVEL_SCROLL_KIND} from '../tracks/scroll_jank/scroll_track'; 26 27import {AggregationPanel} from './aggregation_panel'; 28import {ChromeSliceDetailsPanel} from './chrome_slice_panel'; 29import {CounterDetailsPanel} from './counter_panel'; 30import {CpuProfileDetailsPanel} from './cpu_profile_panel'; 31import {DEFAULT_DETAILS_CONTENT_HEIGHT} from './css_constants'; 32import {DragGestureHandler} from './drag_gesture_handler'; 33import {FlamegraphDetailsPanel} from './flamegraph_panel'; 34import { 35 FlowEventsAreaSelectedPanel, 36 FlowEventsPanel, 37} from './flow_events_panel'; 38import {FtracePanel} from './ftrace_panel'; 39import {globals} from './globals'; 40import {LogPanel} from './logs_panel'; 41import {NotesEditorTab} from './notes_panel'; 42import {AnyAttrsVnode, PanelContainer} from './panel_container'; 43import {PivotTable} from './pivot_table'; 44import {SliceDetailsPanel} from './slice_details_panel'; 45import {ThreadStateTab} from './thread_state_tab'; 46 47const UP_ICON = 'keyboard_arrow_up'; 48const DOWN_ICON = 'keyboard_arrow_down'; 49const DRAG_HANDLE_HEIGHT_PX = 28; 50 51export const CURRENT_SELECTION_TAG = 'current_selection'; 52 53function getDetailsHeight() { 54 // This needs to be a function instead of a const to ensure the CSS constants 55 // have been initialized by the time we perform this calculation; 56 return DEFAULT_DETAILS_CONTENT_HEIGHT + DRAG_HANDLE_HEIGHT_PX; 57} 58 59function getFullScreenHeight() { 60 const panelContainer = 61 document.querySelector('.pan-and-zoom-content') as HTMLElement; 62 if (panelContainer !== null) { 63 return panelContainer.clientHeight; 64 } else { 65 return getDetailsHeight(); 66 } 67} 68 69function hasLogs(): boolean { 70 const data = globals.trackDataStore.get(LogExistsKey) as LogExists; 71 return data && data.exists; 72} 73 74interface Tab { 75 key: string; 76 name: string; 77} 78 79interface DragHandleAttrs { 80 height: number; 81 resize: (height: number) => void; 82 tabs: Tab[]; 83 currentTabKey?: string; 84} 85 86class DragHandle implements m.ClassComponent<DragHandleAttrs> { 87 private dragStartHeight = 0; 88 private height = 0; 89 private previousHeight = this.height; 90 private resize: (height: number) => void = () => {}; 91 private isClosed = this.height <= DRAG_HANDLE_HEIGHT_PX; 92 private isFullscreen = false; 93 // We can't get real fullscreen height until the pan_and_zoom_handler exists. 94 private fullscreenHeight = getDetailsHeight(); 95 96 oncreate({dom, attrs}: m.CVnodeDOM<DragHandleAttrs>) { 97 this.resize = attrs.resize; 98 this.height = attrs.height; 99 this.isClosed = this.height <= DRAG_HANDLE_HEIGHT_PX; 100 this.fullscreenHeight = getFullScreenHeight(); 101 const elem = dom as HTMLElement; 102 new DragGestureHandler( 103 elem, 104 this.onDrag.bind(this), 105 this.onDragStart.bind(this), 106 this.onDragEnd.bind(this)); 107 } 108 109 onupdate({attrs}: m.CVnodeDOM<DragHandleAttrs>) { 110 this.resize = attrs.resize; 111 this.height = attrs.height; 112 this.isClosed = this.height <= DRAG_HANDLE_HEIGHT_PX; 113 } 114 115 onDrag(_x: number, y: number) { 116 const newHeight = 117 Math.floor(this.dragStartHeight + (DRAG_HANDLE_HEIGHT_PX / 2) - y); 118 this.isClosed = newHeight <= DRAG_HANDLE_HEIGHT_PX; 119 this.isFullscreen = newHeight >= this.fullscreenHeight; 120 this.resize(newHeight); 121 globals.rafScheduler.scheduleFullRedraw(); 122 } 123 124 onDragStart(_x: number, _y: number) { 125 this.dragStartHeight = this.height; 126 } 127 128 onDragEnd() {} 129 130 view({attrs}: m.CVnode<DragHandleAttrs>) { 131 const icon = this.isClosed ? UP_ICON : DOWN_ICON; 132 const title = this.isClosed ? 'Show panel' : 'Hide panel'; 133 const renderTab = (tab: Tab) => { 134 if (attrs.currentTabKey === tab.key) { 135 return m('.tab[active]', tab.name); 136 } 137 return m( 138 '.tab', 139 { 140 onclick: () => { 141 globals.dispatch(Actions.setCurrentTab({tab: tab.key})); 142 }, 143 }, 144 tab.name); 145 }; 146 return m( 147 '.handle', 148 m('.tabs', attrs.tabs.map(renderTab)), 149 m('.buttons', 150 m('i.material-icons', 151 { 152 onclick: () => { 153 this.isClosed = false; 154 this.isFullscreen = true; 155 this.resize(this.fullscreenHeight); 156 globals.rafScheduler.scheduleFullRedraw(); 157 }, 158 title: 'Open fullscreen', 159 disabled: this.isFullscreen, 160 }, 161 'vertical_align_top'), 162 m('i.material-icons', 163 { 164 onclick: () => { 165 if (this.height === DRAG_HANDLE_HEIGHT_PX) { 166 this.isClosed = false; 167 if (this.previousHeight === 0) { 168 this.previousHeight = getDetailsHeight(); 169 } 170 this.resize(this.previousHeight); 171 } else { 172 this.isFullscreen = false; 173 this.isClosed = true; 174 this.previousHeight = this.height; 175 this.resize(DRAG_HANDLE_HEIGHT_PX); 176 } 177 globals.rafScheduler.scheduleFullRedraw(); 178 }, 179 title, 180 }, 181 icon))); 182 } 183} 184 185function handleSelectionChange(newSelection?: Selection, _?: Selection): void { 186 const currentSelectionTag = CURRENT_SELECTION_TAG; 187 const bottomTabList = globals.bottomTabList; 188 if (!bottomTabList) return; 189 if (newSelection === undefined) { 190 bottomTabList.closeTabByTag(currentSelectionTag); 191 return; 192 } 193 switch (newSelection.kind) { 194 case 'NOTE': 195 bottomTabList.addTab({ 196 kind: NotesEditorTab.kind, 197 tag: currentSelectionTag, 198 config: { 199 id: newSelection.id, 200 }, 201 }); 202 break; 203 case 'AREA': 204 if (newSelection.noteId !== undefined) { 205 bottomTabList.addTab({ 206 kind: NotesEditorTab.kind, 207 tag: currentSelectionTag, 208 config: { 209 id: newSelection.noteId, 210 }, 211 }); 212 } 213 break; 214 case 'THREAD_STATE': 215 bottomTabList.addTab({ 216 kind: ThreadStateTab.kind, 217 tag: currentSelectionTag, 218 config: { 219 id: newSelection.id, 220 }, 221 }); 222 break; 223 case 'DEBUG_SLICE': 224 bottomTabList.addTab({ 225 kind: DebugSliceDetailsTab.kind, 226 tag: currentSelectionTag, 227 config: { 228 sqlTableName: newSelection.sqlTableName, 229 id: newSelection.id, 230 }, 231 }); 232 break; 233 case TOP_LEVEL_SCROLL_KIND: 234 pluginManager.onDetailsPanelSelectionChange( 235 SCROLL_JANK_PLUGIN_ID, newSelection); 236 break; 237 default: 238 bottomTabList.closeTabByTag(currentSelectionTag); 239 } 240} 241addSelectionChangeObserver(handleSelectionChange); 242 243export class DetailsPanel implements m.ClassComponent { 244 private detailsHeight = getDetailsHeight(); 245 246 view() { 247 interface DetailsPanel { 248 key: string; 249 name: string; 250 vnode: AnyAttrsVnode; 251 } 252 253 const detailsPanels: DetailsPanel[] = []; 254 255 if (globals.bottomTabList) { 256 for (const tab of globals.bottomTabList.getTabs()) { 257 detailsPanels.push({ 258 key: tab.tag ?? tab.uuid, 259 name: tab.getTitle(), 260 vnode: tab.createPanelVnode(), 261 }); 262 } 263 } 264 265 const curSelection = globals.state.currentSelection; 266 if (curSelection) { 267 switch (curSelection.kind) { 268 case 'NOTE': 269 // Handled in handleSelectionChange. 270 break; 271 case 'AREA': 272 if (globals.flamegraphDetails.isInAreaSelection) { 273 detailsPanels.push({ 274 key: 'flamegraph_selection', 275 name: 'Flamegraph Selection', 276 vnode: m(FlamegraphDetailsPanel, {key: 'flamegraph'}), 277 }); 278 } 279 break; 280 case 'SLICE': 281 detailsPanels.push({ 282 key: 'current_selection', 283 name: 'Current Selection', 284 vnode: m(SliceDetailsPanel, { 285 key: 'slice', 286 }), 287 }); 288 break; 289 case 'COUNTER': 290 detailsPanels.push({ 291 key: 'current_selection', 292 name: 'Current Selection', 293 vnode: m(CounterDetailsPanel, { 294 key: 'counter', 295 }), 296 }); 297 break; 298 case 'PERF_SAMPLES': 299 case 'HEAP_PROFILE': 300 detailsPanels.push({ 301 key: 'current_selection', 302 name: 'Current Selection', 303 vnode: m(FlamegraphDetailsPanel, {key: 'flamegraph'}), 304 }); 305 break; 306 case 'CPU_PROFILE_SAMPLE': 307 detailsPanels.push({ 308 key: 'current_selection', 309 name: 'Current Selection', 310 vnode: m(CpuProfileDetailsPanel, { 311 key: 'cpu_profile_sample', 312 }), 313 }); 314 break; 315 case 'CHROME_SLICE': 316 detailsPanels.push({ 317 key: 'current_selection', 318 name: 'Current Selection', 319 vnode: m(ChromeSliceDetailsPanel, {key: 'chrome_slice'}), 320 }); 321 break; 322 default: 323 break; 324 } 325 } 326 if (hasLogs()) { 327 detailsPanels.push({ 328 key: 'android_logs', 329 name: 'Android Logs', 330 vnode: m(LogPanel, {key: 'logs_panel'}), 331 }); 332 } 333 334 const trackGroup = globals.state.trackGroups['ftrace-track-group']; 335 if (trackGroup) { 336 const {collapsed} = trackGroup; 337 if (!collapsed) { 338 detailsPanels.push({ 339 key: 'ftrace_events', 340 name: 'Ftrace Events', 341 vnode: m(FtracePanel, {key: 'ftrace_panel'}), 342 }); 343 } 344 } 345 346 if (globals.state.nonSerializableState.pivotTable.selectionArea !== 347 undefined) { 348 detailsPanels.push({ 349 key: 'pivot_table', 350 name: 'Pivot Table', 351 vnode: m(PivotTable, { 352 key: 'pivot_table', 353 selectionArea: 354 globals.state.nonSerializableState.pivotTable.selectionArea, 355 }), 356 }); 357 } 358 359 if (globals.connectedFlows.length > 0) { 360 detailsPanels.push({ 361 key: 'bound_flows', 362 name: 'Flow Events', 363 vnode: m(FlowEventsPanel, {key: 'flow_events'}), 364 }); 365 } 366 367 for (const [key, value] of globals.aggregateDataStore.entries()) { 368 if (!isEmptyData(value)) { 369 detailsPanels.push({ 370 key: value.tabName, 371 name: value.tabName, 372 vnode: m(AggregationPanel, {kind: key, key, data: value}), 373 }); 374 } 375 } 376 377 // Add this after all aggregation panels, to make it appear after 'Slices' 378 if (globals.selectedFlows.length > 0) { 379 detailsPanels.push({ 380 key: 'selected_flows', 381 name: 'Flow Events', 382 vnode: m(FlowEventsAreaSelectedPanel, {key: 'flow_events_area'}), 383 }); 384 } 385 386 let currentTabDetails = 387 detailsPanels.find((tab) => tab.key === globals.state.currentTab); 388 if (currentTabDetails === undefined && detailsPanels.length > 0) { 389 currentTabDetails = detailsPanels[0]; 390 } 391 392 const panel = currentTabDetails?.vnode; 393 const panels = panel ? [panel] : []; 394 395 return m( 396 '.details-content', 397 { 398 style: { 399 height: `${this.detailsHeight}px`, 400 display: detailsPanels.length > 0 ? null : 'none', 401 }, 402 }, 403 m(DragHandle, { 404 resize: (height: number) => { 405 this.detailsHeight = Math.max(height, DRAG_HANDLE_HEIGHT_PX); 406 }, 407 height: this.detailsHeight, 408 tabs: detailsPanels.map((tab) => { 409 return {key: tab.key, name: tab.name}; 410 }), 411 currentTabKey: currentTabDetails?.key, 412 }), 413 m('.details-panel-container.x-scrollable', 414 m(PanelContainer, {doesScroll: true, panels, kind: 'DETAILS'}))); 415 } 416} 417