• 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 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 {
27  CallsiteInfo,
28  FlamegraphStateViewingOption,
29  ProfileType,
30} from '../common/state';
31import {tpTimeToCode} from '../common/time';
32import {profileType} from '../controller/flamegraph_controller';
33
34import {Flamegraph, NodeRendering} from './flamegraph';
35import {globals} from './globals';
36import {Modal, ModalDefinition} from './modal';
37import {Panel, PanelSize} from './panel';
38import {debounce} from './rate_limiters';
39import {Router} from './router';
40import {getCurrentTrace} from './sidebar';
41import {convertTraceToPprofAndDownload} from './trace_converter';
42import {Button} from './widgets/button';
43import {findRef} from './widgets/utils';
44
45interface FlamegraphDetailsPanelAttrs {}
46
47const HEADER_HEIGHT = 30;
48
49function toSelectedCallsite(c: CallsiteInfo|undefined): string {
50  if (c !== undefined && c.name !== undefined) {
51    return c.name;
52  }
53  return '(none)';
54}
55
56const RENDER_SELF_AND_TOTAL: NodeRendering = {
57  selfSize: 'Self',
58  totalSize: 'Total',
59};
60const RENDER_OBJ_COUNT: NodeRendering = {
61  selfSize: 'Self objects',
62  totalSize: 'Subtree objects',
63};
64
65export class FlamegraphDetailsPanel extends Panel<FlamegraphDetailsPanelAttrs> {
66  private profileType?: ProfileType = undefined;
67  private ts = 0n;
68  private pids: number[] = [];
69  private flamegraph: Flamegraph = new Flamegraph([]);
70  private focusRegex = '';
71  private updateFocusRegexDebounced = debounce(() => {
72    this.updateFocusRegex();
73  }, 20);
74  private canvas?: HTMLCanvasElement;
75
76  view() {
77    const flamegraphDetails = globals.flamegraphDetails;
78    if (flamegraphDetails && flamegraphDetails.type !== undefined &&
79        flamegraphDetails.start !== undefined &&
80        flamegraphDetails.dur !== undefined &&
81        flamegraphDetails.pids !== undefined &&
82        flamegraphDetails.upids !== undefined) {
83      this.profileType = profileType(flamegraphDetails.type);
84      this.ts = flamegraphDetails.start + flamegraphDetails.dur;
85      this.pids = flamegraphDetails.pids;
86      if (flamegraphDetails.flamegraph) {
87        this.flamegraph.updateDataIfChanged(
88            this.nodeRendering(), flamegraphDetails.flamegraph);
89      }
90      const height = flamegraphDetails.flamegraph ?
91          this.flamegraph.getHeight() + HEADER_HEIGHT :
92          0;
93      return m(
94          '.details-panel',
95          this.maybeShowModal(flamegraphDetails.graphIncomplete),
96          m('.details-panel-heading.flamegraph-profile',
97            {onclick: (e: MouseEvent) => e.stopPropagation()},
98            [
99              m('div.options',
100                [
101                  m('div.title', this.getTitle()),
102                  this.getViewingOptionButtons(),
103                ]),
104              m('div.details',
105                [
106                  m('div.selected',
107                    `Selected function: ${
108                        toSelectedCallsite(
109                            flamegraphDetails.expandedCallsite)}`),
110                  m('div.time',
111                    `Snapshot time: ${tpTimeToCode(flamegraphDetails.dur)}`),
112                  m('input[type=text][placeholder=Focus]', {
113                    oninput: (e: Event) => {
114                      const target = (e.target as HTMLInputElement);
115                      this.focusRegex = target.value;
116                      this.updateFocusRegexDebounced();
117                    },
118                    // Required to stop hot-key handling:
119                    onkeydown: (e: Event) => e.stopPropagation(),
120                  }),
121                  (this.profileType === ProfileType.NATIVE_HEAP_PROFILE ||
122                   this.profileType === ProfileType.JAVA_HEAP_SAMPLES) &&
123                      m(Button, {
124                        icon: 'file_download',
125                        onclick: () => {
126                          this.downloadPprof();
127                        },
128                      }),
129                ]),
130            ]),
131          m(`canvas[ref=canvas]`, {
132            style: `height:${height}px; width:100%`,
133            onmousemove: (e: MouseEvent) => {
134              const {offsetX, offsetY} = e;
135              this.onMouseMove({x: offsetX, y: offsetY});
136            },
137            onmouseout: () => {
138              this.onMouseOut();
139            },
140            onclick: (e: MouseEvent) => {
141              const {offsetX, offsetY} = e;
142              this.onMouseClick({x: offsetX, y: offsetY});
143            },
144          }),
145      );
146    } else {
147      return m(
148          '.details-panel',
149          m('.details-panel-heading', m('h2', `Flamegraph Profile`)));
150    }
151  }
152
153
154  private maybeShowModal(graphIncomplete?: boolean) {
155    if (!graphIncomplete || globals.state.flamegraphModalDismissed) {
156      return undefined;
157    }
158    return m(Modal, {
159      title: 'The flamegraph is incomplete',
160      vAlign: 'TOP',
161      content: m('div',
162          'The current trace does not have a fully formed flamegraph'),
163      buttons: [
164        {
165          text: 'Show the errors',
166          primary: true,
167          action: () => Router.navigate('#!/info'),
168        },
169        {
170          text: 'Skip',
171          action: () => {
172            globals.dispatch(Actions.dismissFlamegraphModal({}));
173            globals.rafScheduler.scheduleFullRedraw();
174          },
175        },
176      ],
177    } as ModalDefinition);
178  }
179
180  private getTitle(): string {
181    switch (this.profileType!) {
182      case ProfileType.HEAP_PROFILE:
183        return 'Heap profile:';
184      case ProfileType.NATIVE_HEAP_PROFILE:
185        return 'Native heap profile:';
186      case ProfileType.JAVA_HEAP_SAMPLES:
187        return 'Java heap samples:';
188      case ProfileType.JAVA_HEAP_GRAPH:
189        return 'Java heap graph:';
190      case ProfileType.PERF_SAMPLE:
191        return 'Profile:';
192      default:
193        throw new Error('unknown type');
194    }
195  }
196
197  private nodeRendering(): NodeRendering {
198    if (this.profileType === undefined) {
199      return {};
200    }
201    const viewingOption = globals.state.currentFlamegraphState!.viewingOption;
202    switch (this.profileType) {
203      case ProfileType.JAVA_HEAP_GRAPH:
204        if (viewingOption === OBJECTS_ALLOCATED_NOT_FREED_KEY) {
205          return RENDER_OBJ_COUNT;
206        } else {
207          return RENDER_SELF_AND_TOTAL;
208        }
209      case ProfileType.HEAP_PROFILE:
210      case ProfileType.NATIVE_HEAP_PROFILE:
211      case ProfileType.JAVA_HEAP_SAMPLES:
212      case ProfileType.PERF_SAMPLE:
213        return RENDER_SELF_AND_TOTAL;
214      default:
215        throw new Error('unknown type');
216    }
217  }
218
219  private updateFocusRegex() {
220    globals.dispatch(Actions.changeFocusFlamegraphState({
221      focusRegex: this.focusRegex,
222    }));
223  }
224
225  getViewingOptionButtons(): m.Children {
226    return m(
227        'div',
228        ...FlamegraphDetailsPanel.selectViewingOptions(
229            assertExists(this.profileType)));
230  }
231
232  downloadPprof() {
233    const engine = globals.getCurrentEngine();
234    if (!engine) return;
235    getCurrentTrace()
236        .then((file) => {
237          assertTrue(
238              this.pids.length === 1,
239              'Native profiles can only contain one pid.');
240          convertTraceToPprofAndDownload(file, this.pids[0], this.ts);
241        })
242        .catch((error) => {
243          throw new Error(`Failed to get current trace ${error}`);
244        });
245  }
246
247  private changeFlamegraphData() {
248    const data = globals.flamegraphDetails;
249    const flamegraphData = data.flamegraph === undefined ? [] : data.flamegraph;
250    this.flamegraph.updateDataIfChanged(
251        this.nodeRendering(), flamegraphData, data.expandedCallsite);
252  }
253
254  oncreate({dom}: m.CVnodeDOM<FlamegraphDetailsPanelAttrs>) {
255    this.canvas = FlamegraphDetailsPanel.findCanvasElement(dom);
256    // TODO(stevegolton): If we truely want to be standalone, then we shouldn't
257    // rely on someone else calling the rafScheduler when the window is resized,
258    // but it's good enough for now as we know the ViewerPage will do it.
259    globals.rafScheduler.addRedrawCallback(this.rafRedrawCallback);
260  }
261
262  onupdate({dom}: m.CVnodeDOM<FlamegraphDetailsPanelAttrs>) {
263    this.canvas = FlamegraphDetailsPanel.findCanvasElement(dom);
264  }
265
266  onremove(_vnode: m.CVnodeDOM<FlamegraphDetailsPanelAttrs>) {
267    globals.rafScheduler.removeRedrawCallback(this.rafRedrawCallback);
268  }
269
270  private static findCanvasElement(dom: Element): HTMLCanvasElement|undefined {
271    const canvas = findRef(dom, 'canvas');
272    if (canvas && canvas instanceof HTMLCanvasElement) {
273      return canvas;
274    } else {
275      return undefined;
276    }
277  }
278
279  private rafRedrawCallback = () => {
280    if (this.canvas) {
281      const canvas = this.canvas;
282      canvas.width = canvas.offsetWidth * devicePixelRatio;
283      canvas.height = canvas.offsetHeight * devicePixelRatio;
284      const ctx = canvas.getContext('2d');
285      if (ctx) {
286        ctx.clearRect(0, 0, canvas.width, canvas.height);
287        ctx.save();
288        ctx.scale(devicePixelRatio, devicePixelRatio);
289        const {offsetWidth: width, offsetHeight: height} = canvas;
290        this.renderLocalCanvas(ctx, {width, height});
291        ctx.restore();
292      }
293    }
294  };
295
296  renderCanvas() {
297    // No-op
298  }
299
300  private renderLocalCanvas(ctx: CanvasRenderingContext2D, size: PanelSize) {
301    this.changeFlamegraphData();
302    const current = globals.state.currentFlamegraphState;
303    if (current === null) return;
304    const unit =
305        current.viewingOption === SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY ||
306            current.viewingOption === ALLOC_SPACE_MEMORY_ALLOCATED_KEY ?
307        'B' :
308        '';
309    this.flamegraph.draw(ctx, size.width, size.height, 0, 0, unit);
310  }
311
312  private onMouseClick({x, y}: {x: number, y: number}): boolean {
313    const expandedCallsite = this.flamegraph.onMouseClick({x, y});
314    globals.dispatch(Actions.expandFlamegraphState({expandedCallsite}));
315    return true;
316  }
317
318  private onMouseMove({x, y}: {x: number, y: number}): boolean {
319    this.flamegraph.onMouseMove({x, y});
320    globals.rafScheduler.scheduleFullRedraw();
321    return true;
322  }
323
324  private onMouseOut() {
325    this.flamegraph.onMouseOut();
326    globals.rafScheduler.scheduleFullRedraw();
327  }
328
329  private static selectViewingOptions(profileType: ProfileType) {
330    switch (profileType) {
331      case ProfileType.PERF_SAMPLE:
332        return [this.buildButtonComponent(PERF_SAMPLES_KEY, 'Samples')];
333      case ProfileType.JAVA_HEAP_GRAPH:
334        return [
335          this.buildButtonComponent(
336              SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY, 'Size'),
337          this.buildButtonComponent(OBJECTS_ALLOCATED_NOT_FREED_KEY, 'Objects'),
338        ];
339      case ProfileType.HEAP_PROFILE:
340        return [
341          this.buildButtonComponent(
342              SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY, 'Unreleased size'),
343          this.buildButtonComponent(
344              OBJECTS_ALLOCATED_NOT_FREED_KEY, 'Unreleased count'),
345          this.buildButtonComponent(
346              ALLOC_SPACE_MEMORY_ALLOCATED_KEY, 'Total size'),
347          this.buildButtonComponent(OBJECTS_ALLOCATED_KEY, 'Total count'),
348        ];
349      case ProfileType.NATIVE_HEAP_PROFILE:
350        return [
351          this.buildButtonComponent(
352              SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY, 'Unreleased malloc size'),
353          this.buildButtonComponent(
354              OBJECTS_ALLOCATED_NOT_FREED_KEY, 'Unreleased malloc count'),
355          this.buildButtonComponent(
356              ALLOC_SPACE_MEMORY_ALLOCATED_KEY, 'Total malloc size'),
357          this.buildButtonComponent(
358              OBJECTS_ALLOCATED_KEY, 'Total malloc count'),
359        ];
360      case ProfileType.JAVA_HEAP_SAMPLES:
361        return [
362          this.buildButtonComponent(
363              ALLOC_SPACE_MEMORY_ALLOCATED_KEY, 'Total allocation size'),
364          this.buildButtonComponent(
365              OBJECTS_ALLOCATED_KEY, 'Total allocation count'),
366        ];
367      default:
368        throw new Error(`Unexpected profile type ${profileType}`);
369    }
370  }
371
372  private static buildButtonComponent(
373      viewingOption: FlamegraphStateViewingOption, text: string) {
374    const active =
375        (globals.state.currentFlamegraphState !== null &&
376         globals.state.currentFlamegraphState.viewingOption === viewingOption);
377    return m(Button, {
378      label: text,
379      active,
380      minimal: true,
381      onclick: () => {
382        globals.dispatch(Actions.changeViewFlamegraphState({viewingOption}));
383      },
384    });
385  }
386}
387