| // Copyright (C) 2019 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use size file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| import m from 'mithril'; |
| |
| import {findRef} from '../base/dom_utils'; |
| import {assertExists, assertTrue} from '../base/logging'; |
| import {Time} from '../base/time'; |
| import {Actions} from '../common/actions'; |
| import {viewingOptions} from '../common/flamegraph_util'; |
| import { |
| CallsiteInfo, |
| FlamegraphStateViewingOption, |
| ProfileType, |
| } from '../common/state'; |
| import {profileType} from '../controller/flamegraph_controller'; |
| import {raf} from '../core/raf_scheduler'; |
| import {Button} from '../widgets/button'; |
| import {Icon} from '../widgets/icon'; |
| import {Modal, ModalAttrs} from '../widgets/modal'; |
| import {Popup} from '../widgets/popup'; |
| import {EmptyState} from '../widgets/empty_state'; |
| import {Spinner} from '../widgets/spinner'; |
| |
| import {Flamegraph, NodeRendering} from './flamegraph'; |
| import {globals} from './globals'; |
| import {debounce} from './rate_limiters'; |
| import {Router} from './router'; |
| import {getCurrentTrace} from './sidebar'; |
| import {convertTraceToPprofAndDownload} from './trace_converter'; |
| import {ButtonBar} from '../widgets/button'; |
| import {DurationWidget} from './widgets/duration'; |
| import {DetailsShell} from '../widgets/details_shell'; |
| import {Intent} from '../widgets/common'; |
| |
| const HEADER_HEIGHT = 30; |
| |
| function toSelectedCallsite(c: CallsiteInfo | undefined): string { |
| if (c !== undefined && c.name !== undefined) { |
| return c.name; |
| } |
| return '(none)'; |
| } |
| |
| const RENDER_SELF_AND_TOTAL: NodeRendering = { |
| selfSize: 'Self', |
| totalSize: 'Total', |
| }; |
| const RENDER_OBJ_COUNT: NodeRendering = { |
| selfSize: 'Self objects', |
| totalSize: 'Subtree objects', |
| }; |
| |
| export class FlamegraphDetailsPanel implements m.ClassComponent { |
| private profileType?: ProfileType = undefined; |
| private ts = Time.ZERO; |
| private pids: number[] = []; |
| private flamegraph: Flamegraph = new Flamegraph([]); |
| private focusRegex = ''; |
| private updateFocusRegexDebounced = debounce(() => { |
| this.updateFocusRegex(); |
| }, 20); |
| private canvas?: HTMLCanvasElement; |
| |
| view() { |
| const flamegraphDetails = globals.flamegraphDetails; |
| if ( |
| flamegraphDetails.type !== undefined && |
| flamegraphDetails.start !== undefined && |
| flamegraphDetails.dur !== undefined && |
| flamegraphDetails.pids !== undefined && |
| flamegraphDetails.upids !== undefined |
| ) { |
| this.profileType = profileType(flamegraphDetails.type); |
| this.ts = Time.add(flamegraphDetails.start, flamegraphDetails.dur); |
| this.pids = flamegraphDetails.pids; |
| if (flamegraphDetails.flamegraph) { |
| this.flamegraph.updateDataIfChanged( |
| this.nodeRendering(), |
| flamegraphDetails.flamegraph, |
| ); |
| } |
| const height = flamegraphDetails.flamegraph |
| ? this.flamegraph.getHeight() + HEADER_HEIGHT |
| : 0; |
| return m( |
| '.flamegraph-profile', |
| this.maybeShowModal(flamegraphDetails.graphIncomplete), |
| m( |
| DetailsShell, |
| { |
| fillParent: true, |
| title: m( |
| 'div.title', |
| this.getTitle(), |
| this.profileType === ProfileType.MIXED_HEAP_PROFILE && |
| m( |
| Popup, |
| { |
| trigger: m(Icon, {icon: 'warning'}), |
| }, |
| m( |
| '', |
| {style: {width: '300px'}}, |
| 'This is a mixed java/native heap profile, free()s are not visualized. To visualize free()s, remove "all_heaps: true" from the config.', |
| ), |
| ), |
| ':', |
| ), |
| description: this.getViewingOptionButtons(), |
| buttons: [ |
| m( |
| 'div.selected', |
| `Selected function: ${toSelectedCallsite( |
| flamegraphDetails.expandedCallsite, |
| )}`, |
| ), |
| m( |
| 'div.time', |
| `Snapshot time: `, |
| m(DurationWidget, {dur: flamegraphDetails.dur}), |
| ), |
| m('input[type=text][placeholder=Focus]', { |
| oninput: (e: Event) => { |
| const target = e.target as HTMLInputElement; |
| this.focusRegex = target.value; |
| this.updateFocusRegexDebounced(); |
| }, |
| // Required to stop hot-key handling: |
| onkeydown: (e: Event) => e.stopPropagation(), |
| }), |
| (this.profileType === ProfileType.NATIVE_HEAP_PROFILE || |
| this.profileType === ProfileType.JAVA_HEAP_SAMPLES) && |
| m(Button, { |
| icon: 'file_download', |
| intent: Intent.Primary, |
| onclick: () => { |
| this.downloadPprof(); |
| }, |
| }), |
| ], |
| }, |
| m( |
| '.flamegraph-content', |
| flamegraphDetails.graphLoading |
| ? m( |
| '.loading-container', |
| m( |
| EmptyState, |
| { |
| icon: 'bar_chart', |
| title: 'Computing graph ...', |
| className: 'flamegraph-loading', |
| }, |
| m(Spinner, {easing: true}), |
| ), |
| ) |
| : m(`canvas[ref=canvas]`, { |
| style: `height:${height}px; width:100%`, |
| onmousemove: (e: MouseEvent) => { |
| const {offsetX, offsetY} = e; |
| this.onMouseMove({x: offsetX, y: offsetY}); |
| }, |
| onmouseout: () => { |
| this.onMouseOut(); |
| }, |
| onclick: (e: MouseEvent) => { |
| const {offsetX, offsetY} = e; |
| this.onMouseClick({x: offsetX, y: offsetY}); |
| }, |
| }), |
| ), |
| ), |
| ); |
| } else { |
| return m( |
| '.details-panel', |
| m('.details-panel-heading', m('h2', `Flamegraph Profile`)), |
| ); |
| } |
| } |
| |
| private maybeShowModal(graphIncomplete?: boolean) { |
| if (!graphIncomplete || globals.state.flamegraphModalDismissed) { |
| return undefined; |
| } |
| return m(Modal, { |
| title: 'The flamegraph is incomplete', |
| vAlign: 'TOP', |
| content: m( |
| 'div', |
| 'The current trace does not have a fully formed flamegraph', |
| ), |
| buttons: [ |
| { |
| text: 'Show the errors', |
| primary: true, |
| action: () => Router.navigate('#!/info'), |
| }, |
| { |
| text: 'Skip', |
| action: () => { |
| globals.dispatch(Actions.dismissFlamegraphModal({})); |
| raf.scheduleFullRedraw(); |
| }, |
| }, |
| ], |
| } as ModalAttrs); |
| } |
| |
| private getTitle(): string { |
| const profileType = this.profileType!; |
| switch (profileType) { |
| case ProfileType.MIXED_HEAP_PROFILE: |
| return 'Mixed heap profile'; |
| case ProfileType.HEAP_PROFILE: |
| return 'Heap profile'; |
| case ProfileType.NATIVE_HEAP_PROFILE: |
| return 'Native heap profile'; |
| case ProfileType.JAVA_HEAP_SAMPLES: |
| return 'Java heap samples'; |
| case ProfileType.JAVA_HEAP_GRAPH: |
| return 'Java heap graph'; |
| case ProfileType.PERF_SAMPLE: |
| return 'Profile'; |
| default: |
| throw new Error('unknown type'); |
| } |
| } |
| |
| private nodeRendering(): NodeRendering { |
| if (this.profileType === undefined) { |
| return {}; |
| } |
| const profileType = this.profileType; |
| const viewingOption: FlamegraphStateViewingOption = |
| globals.state.currentFlamegraphState!.viewingOption; |
| switch (profileType) { |
| case ProfileType.JAVA_HEAP_GRAPH: |
| if ( |
| viewingOption === |
| FlamegraphStateViewingOption.OBJECTS_ALLOCATED_NOT_FREED_KEY || |
| viewingOption === |
| FlamegraphStateViewingOption.DOMINATOR_TREE_OBJ_COUNT_KEY |
| ) { |
| return RENDER_OBJ_COUNT; |
| } else { |
| return RENDER_SELF_AND_TOTAL; |
| } |
| case ProfileType.MIXED_HEAP_PROFILE: |
| case ProfileType.HEAP_PROFILE: |
| case ProfileType.NATIVE_HEAP_PROFILE: |
| case ProfileType.JAVA_HEAP_SAMPLES: |
| case ProfileType.PERF_SAMPLE: |
| return RENDER_SELF_AND_TOTAL; |
| default: |
| const exhaustiveCheck: never = profileType; |
| throw new Error(`Unhandled case: ${exhaustiveCheck}`); |
| } |
| } |
| |
| private updateFocusRegex() { |
| globals.dispatch( |
| Actions.changeFocusFlamegraphState({ |
| focusRegex: this.focusRegex, |
| }), |
| ); |
| } |
| |
| getViewingOptionButtons(): m.Children { |
| return m( |
| ButtonBar, |
| ...FlamegraphDetailsPanel.selectViewingOptions( |
| assertExists(this.profileType), |
| ), |
| ); |
| } |
| |
| downloadPprof() { |
| const engine = globals.getCurrentEngine(); |
| if (!engine) return; |
| getCurrentTrace() |
| .then((file) => { |
| assertTrue( |
| this.pids.length === 1, |
| 'Native profiles can only contain one pid.', |
| ); |
| convertTraceToPprofAndDownload(file, this.pids[0], this.ts); |
| }) |
| .catch((error) => { |
| throw new Error(`Failed to get current trace ${error}`); |
| }); |
| } |
| |
| private changeFlamegraphData() { |
| const data = globals.flamegraphDetails; |
| const flamegraphData = data.flamegraph === undefined ? [] : data.flamegraph; |
| this.flamegraph.updateDataIfChanged( |
| this.nodeRendering(), |
| flamegraphData, |
| data.expandedCallsite, |
| ); |
| } |
| |
| oncreate({dom}: m.CVnodeDOM) { |
| this.canvas = FlamegraphDetailsPanel.findCanvasElement(dom); |
| // TODO(stevegolton): If we truely want to be standalone, then we shouldn't |
| // rely on someone else calling the rafScheduler when the window is resized, |
| // but it's good enough for now as we know the ViewerPage will do it. |
| raf.addRedrawCallback(this.rafRedrawCallback); |
| } |
| |
| onupdate({dom}: m.CVnodeDOM) { |
| this.canvas = FlamegraphDetailsPanel.findCanvasElement(dom); |
| } |
| |
| onremove(_vnode: m.CVnodeDOM) { |
| raf.removeRedrawCallback(this.rafRedrawCallback); |
| } |
| |
| private static findCanvasElement( |
| dom: Element, |
| ): HTMLCanvasElement | undefined { |
| const canvas = findRef(dom, 'canvas'); |
| if (canvas && canvas instanceof HTMLCanvasElement) { |
| return canvas; |
| } else { |
| return undefined; |
| } |
| } |
| |
| private rafRedrawCallback = () => { |
| if (this.canvas) { |
| const canvas = this.canvas; |
| canvas.width = canvas.offsetWidth * devicePixelRatio; |
| canvas.height = canvas.offsetHeight * devicePixelRatio; |
| const ctx = canvas.getContext('2d'); |
| if (ctx) { |
| ctx.clearRect(0, 0, canvas.width, canvas.height); |
| ctx.save(); |
| ctx.scale(devicePixelRatio, devicePixelRatio); |
| const {offsetWidth: width, offsetHeight: height} = canvas; |
| this.renderLocalCanvas(ctx, width, height); |
| ctx.restore(); |
| } |
| } |
| }; |
| |
| private renderLocalCanvas( |
| ctx: CanvasRenderingContext2D, |
| width: number, |
| height: number, |
| ) { |
| this.changeFlamegraphData(); |
| const current = globals.state.currentFlamegraphState; |
| if (current === null) return; |
| const unit = |
| current.viewingOption === |
| FlamegraphStateViewingOption.SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY || |
| current.viewingOption === |
| FlamegraphStateViewingOption.ALLOC_SPACE_MEMORY_ALLOCATED_KEY || |
| current.viewingOption === |
| FlamegraphStateViewingOption.DOMINATOR_TREE_OBJ_SIZE_KEY |
| ? 'B' |
| : ''; |
| this.flamegraph.draw(ctx, width, height, 0, 0, unit); |
| } |
| |
| private onMouseClick({x, y}: {x: number; y: number}): boolean { |
| const expandedCallsite = this.flamegraph.onMouseClick({x, y}); |
| globals.state.currentFlamegraphState && |
| globals.dispatch( |
| Actions.expandFlamegraphState({ |
| expandedCallsite, |
| viewingOption: globals.state.currentFlamegraphState.viewingOption, |
| }), |
| ); |
| return true; |
| } |
| |
| private onMouseMove({x, y}: {x: number; y: number}): boolean { |
| this.flamegraph.onMouseMove({x, y}); |
| raf.scheduleFullRedraw(); |
| return true; |
| } |
| |
| private onMouseOut() { |
| this.flamegraph.onMouseOut(); |
| raf.scheduleFullRedraw(); |
| } |
| |
| private static selectViewingOptions(profileType: ProfileType) { |
| const ret = []; |
| for (const {option, name} of viewingOptions(profileType)) { |
| ret.push(this.buildButtonComponent(option, name)); |
| } |
| return ret; |
| } |
| |
| private static buildButtonComponent( |
| viewingOption: FlamegraphStateViewingOption, |
| text: string, |
| ) { |
| const active = |
| globals.state.currentFlamegraphState !== null && |
| globals.state.currentFlamegraphState.viewingOption === viewingOption; |
| return m(Button, { |
| label: text, |
| active, |
| onclick: () => { |
| globals.dispatch(Actions.changeViewFlamegraphState({viewingOption})); |
| }, |
| }); |
| } |
| } |