• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 size 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 {assertExists, assertTrue} from '../base/logging';
18import {Actions} from '../common/actions';
19import {
20  ALLOC_SPACE_MEMORY_ALLOCATED_KEY,
21  OBJECTS_ALLOCATED_KEY,
22  OBJECTS_ALLOCATED_NOT_FREED_KEY,
23  PERF_SAMPLES_KEY,
24  SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY,
25} from '../common/flamegraph_util';
26import {CallsiteInfo, FlamegraphStateViewingOption} from '../common/state';
27import {timeToCode} from '../common/time';
28
29import {PerfettoMouseEvent} from './events';
30import {Flamegraph, NodeRendering} from './flamegraph';
31import {globals} from './globals';
32import {showPartialModal} from './modal';
33import {Panel, PanelSize} from './panel';
34import {debounce} from './rate_limiters';
35import {Router} from './router';
36import {getCurrentTrace} from './sidebar';
37import {convertTraceToPprofAndDownload} from './trace_converter';
38
39interface FlamegraphDetailsPanelAttrs {}
40
41const HEADER_HEIGHT = 30;
42
43enum ProfileType {
44  NATIVE_HEAP_PROFILE = 'native',
45  JAVA_HEAP_GRAPH = 'graph',
46  PERF_SAMPLE = 'perf'
47}
48
49function isProfileType(s: string): s is ProfileType {
50  return Object.values(ProfileType).includes(s as ProfileType);
51}
52
53function toProfileType(s: string): ProfileType {
54  if (!isProfileType(s)) {
55    throw new Error('Unknown type ${s}');
56  }
57  return s;
58}
59
60function toSelectedCallsite(c: CallsiteInfo|undefined): string {
61  if (c !== undefined && c.name !== undefined) {
62    return c.name;
63  }
64  return '(none)';
65}
66
67const RENDER_SELF_AND_TOTAL: NodeRendering = {
68  selfSize: 'Self',
69  totalSize: 'Total',
70};
71const RENDER_OBJ_COUNT: NodeRendering = {
72  selfSize: 'Self objects',
73  totalSize: 'Subtree objects',
74};
75
76export class FlamegraphDetailsPanel extends Panel<FlamegraphDetailsPanelAttrs> {
77  private profileType?: ProfileType = undefined;
78  private ts = 0;
79  private pids: number[] = [];
80  private flamegraph: Flamegraph = new Flamegraph([]);
81  private focusRegex = '';
82  private updateFocusRegexDebounced = debounce(() => {
83    this.updateFocusRegex();
84  }, 20);
85
86  view() {
87    const flamegraphDetails = globals.flamegraphDetails;
88    if (flamegraphDetails && flamegraphDetails.type !== undefined &&
89        flamegraphDetails.startNs !== undefined &&
90        flamegraphDetails.durNs !== undefined &&
91        flamegraphDetails.pids !== undefined &&
92        flamegraphDetails.upids !== undefined) {
93      this.profileType = toProfileType(flamegraphDetails.type);
94      this.ts = flamegraphDetails.durNs;
95      this.pids = flamegraphDetails.pids;
96      if (flamegraphDetails.flamegraph) {
97        this.flamegraph.updateDataIfChanged(
98            this.nodeRendering(), flamegraphDetails.flamegraph);
99      }
100      const height = flamegraphDetails.flamegraph ?
101          this.flamegraph.getHeight() + HEADER_HEIGHT :
102          0;
103      return m(
104          '.details-panel',
105          {
106            onclick: (e: PerfettoMouseEvent) => {
107              if (this.flamegraph !== undefined) {
108                this.onMouseClick({y: e.layerY, x: e.layerX});
109              }
110              return false;
111            },
112            onmousemove: (e: PerfettoMouseEvent) => {
113              if (this.flamegraph !== undefined) {
114                this.onMouseMove({y: e.layerY, x: e.layerX});
115                globals.rafScheduler.scheduleRedraw();
116              }
117            },
118            onmouseout: () => {
119              if (this.flamegraph !== undefined) {
120                this.onMouseOut();
121              }
122            }
123          },
124          this.maybeShowModal(flamegraphDetails.graphIncomplete),
125          m('.details-panel-heading.flamegraph-profile',
126            {onclick: (e: MouseEvent) => e.stopPropagation()},
127            [
128              m('div.options',
129                [
130                  m('div.title', this.getTitle()),
131                  this.getViewingOptionButtons(),
132                ]),
133              m('div.details',
134                [
135                  m('div.selected',
136                    `Selected function: ${
137                        toSelectedCallsite(
138                            flamegraphDetails.expandedCallsite)}`),
139                  m('div.time',
140                    `Snapshot time: ${timeToCode(flamegraphDetails.durNs)}`),
141                  m('input[type=text][placeholder=Focus]', {
142                    oninput: (e: Event) => {
143                      const target = (e.target as HTMLInputElement);
144                      this.focusRegex = target.value;
145                      this.updateFocusRegexDebounced();
146                    },
147                    // Required to stop hot-key handling:
148                    onkeydown: (e: Event) => e.stopPropagation(),
149                  }),
150                  this.profileType === ProfileType.NATIVE_HEAP_PROFILE ?
151                      m('button.download',
152                        {
153                          onclick: () => {
154                            this.downloadPprof();
155                          }
156                        },
157                        m('i.material-icons', 'file_download'),
158                        'Download profile') :
159                      null
160                ]),
161            ]),
162          m(`div[style=height:${height}px]`),
163      );
164    } else {
165      return m(
166          '.details-panel',
167          m('.details-panel-heading', m('h2', `Flamegraph Profile`)));
168    }
169  }
170
171
172  private maybeShowModal(graphIncomplete?: boolean): m.Vnode|undefined {
173    if (!graphIncomplete || globals.state.flamegraphModalDismissed) {
174      return undefined;
175    }
176    return showPartialModal({
177      title: 'The flamegraph is incomplete',
178      content:
179          m('div',
180            m('div',
181              'The current trace does not have a fully formed flamegraph.')),
182      buttons: [
183        {
184          text: 'Show the errors',
185          primary: true,
186          id: 'incomplete_graph_show',
187          action: () => {
188            Router.navigate('#!/info');
189          }
190        },
191        {
192          text: 'Skip',
193          primary: false,
194          id: 'incomplete_graph_skip',
195          action: () => {
196            globals.dispatch(Actions.dismissFlamegraphModal({}));
197            globals.rafScheduler.scheduleFullRedraw();
198          }
199        }
200      ],
201    });
202  }
203
204  private getTitle(): string {
205    switch (this.profileType!) {
206      case ProfileType.NATIVE_HEAP_PROFILE:
207        return 'Heap Profile:';
208      case ProfileType.JAVA_HEAP_GRAPH:
209        return 'Java Heap:';
210      case ProfileType.PERF_SAMPLE:
211        return 'Perf sample:';
212      default:
213        throw new Error('unknown type');
214    }
215  }
216
217  private nodeRendering(): NodeRendering {
218    if (this.profileType === undefined) {
219      return {};
220    }
221    const viewingOption = globals.state.currentFlamegraphState!.viewingOption;
222    switch (this.profileType) {
223      case ProfileType.JAVA_HEAP_GRAPH:
224        if (viewingOption === OBJECTS_ALLOCATED_NOT_FREED_KEY) {
225          return RENDER_OBJ_COUNT;
226        } else {
227          return RENDER_SELF_AND_TOTAL;
228        }
229      case ProfileType.NATIVE_HEAP_PROFILE:
230      case ProfileType.PERF_SAMPLE:
231        return RENDER_SELF_AND_TOTAL;
232      default:
233        throw new Error('unknown type');
234    }
235  }
236
237  private updateFocusRegex() {
238    globals.dispatch(Actions.changeFocusFlamegraphState({
239      focusRegex: this.focusRegex,
240    }));
241  }
242
243  getViewingOptionButtons(): m.Children {
244    return m(
245        'div',
246        ...FlamegraphDetailsPanel.selectViewingOptions(
247            assertExists(this.profileType)));
248  }
249
250  downloadPprof() {
251    const engine = Object.values(globals.state.engines)[0];
252    if (!engine) return;
253    getCurrentTrace()
254        .then(file => {
255          assertTrue(
256              this.pids.length === 1,
257              'Native profiles can only contain one pid.');
258          convertTraceToPprofAndDownload(file, this.pids[0], this.ts);
259        })
260        .catch(error => {
261          throw new Error(`Failed to get current trace ${error}`);
262        });
263  }
264
265  private changeFlamegraphData() {
266    const data = globals.flamegraphDetails;
267    const flamegraphData = data.flamegraph === undefined ? [] : data.flamegraph;
268    this.flamegraph.updateDataIfChanged(
269        this.nodeRendering(), flamegraphData, data.expandedCallsite);
270  }
271
272  renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize) {
273    this.changeFlamegraphData();
274    const current = globals.state.currentFlamegraphState;
275    if (current === null) return;
276    const unit =
277        current.viewingOption === SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY ||
278            current.viewingOption === ALLOC_SPACE_MEMORY_ALLOCATED_KEY ?
279        'B' :
280        '';
281    this.flamegraph.draw(ctx, size.width, size.height, 0, HEADER_HEIGHT, unit);
282  }
283
284  onMouseClick({x, y}: {x: number, y: number}): boolean {
285    const expandedCallsite = this.flamegraph.onMouseClick({x, y});
286    globals.dispatch(Actions.expandFlamegraphState({expandedCallsite}));
287    return true;
288  }
289
290  onMouseMove({x, y}: {x: number, y: number}): boolean {
291    this.flamegraph.onMouseMove({x, y});
292    return true;
293  }
294
295  onMouseOut() {
296    this.flamegraph.onMouseOut();
297  }
298
299  private static selectViewingOptions(profileType: ProfileType) {
300    switch (profileType) {
301      case ProfileType.PERF_SAMPLE:
302        return [this.buildButtonComponent(PERF_SAMPLES_KEY, 'samples')];
303      case ProfileType.JAVA_HEAP_GRAPH:
304        return [
305          this.buildButtonComponent(
306              SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY, 'space'),
307          this.buildButtonComponent(OBJECTS_ALLOCATED_NOT_FREED_KEY, 'objects')
308        ];
309      case ProfileType.NATIVE_HEAP_PROFILE:
310        return [
311          this.buildButtonComponent(
312              SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY, 'space'),
313          this.buildButtonComponent(OBJECTS_ALLOCATED_NOT_FREED_KEY, 'objects'),
314          this.buildButtonComponent(
315              ALLOC_SPACE_MEMORY_ALLOCATED_KEY, 'alloc space'),
316          this.buildButtonComponent(OBJECTS_ALLOCATED_KEY, 'alloc objects')
317        ];
318      default:
319        throw new Error(`Unexpected profile type ${profileType}`);
320    }
321  }
322
323  private static buildButtonComponent(
324      viewingOption: FlamegraphStateViewingOption, text: string) {
325    const buttonsClass =
326        (globals.state.currentFlamegraphState &&
327         globals.state.currentFlamegraphState.viewingOption === viewingOption) ?
328        '.chosen' :
329        '';
330    return m(
331        `button${buttonsClass}`,
332        {
333          onclick: () => {
334            globals.dispatch(
335                Actions.changeViewFlamegraphState({viewingOption}));
336          }
337        },
338        text);
339  }
340}
341