• 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 {Actions} from '../common/actions';
18import {
19  ALLOC_SPACE_MEMORY_ALLOCATED_KEY,
20  OBJECTS_ALLOCATED_KEY,
21  OBJECTS_ALLOCATED_NOT_FREED_KEY,
22  SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY,
23} from '../common/flamegraph_util';
24import {HeapProfileFlamegraphViewingOption} from '../common/state';
25import {timeToCode} from '../common/time';
26
27import {PerfettoMouseEvent} from './events';
28import {Flamegraph, NodeRendering} from './flamegraph';
29import {globals} from './globals';
30import {Panel, PanelSize} from './panel';
31import {debounce} from './rate_limiters';
32
33interface HeapProfileDetailsPanelAttrs {}
34
35const HEADER_HEIGHT = 30;
36
37enum ProfileType {
38  NATIVE_HEAP_PROFILE = 'native',
39  JAVA_HEAP_GRAPH = 'graph',
40}
41
42function isProfileType(s: string): s is ProfileType {
43  return Object.values(ProfileType).includes(s as ProfileType);
44}
45
46function toProfileType(s: string): ProfileType {
47  if (!isProfileType(s)) {
48    throw new Error('Unknown type ${s}');
49  }
50  return s;
51}
52
53const RENDER_SELF_AND_TOTAL: NodeRendering = {
54  selfSize: 'Self',
55  totalSize: 'Total',
56};
57const RENDER_OBJ_COUNT: NodeRendering = {
58  selfSize: 'Self objects',
59  totalSize: 'Subtree objects',
60};
61
62export class HeapProfileDetailsPanel extends
63    Panel<HeapProfileDetailsPanelAttrs> {
64  private profileType?: ProfileType = undefined;
65  private ts = 0;
66  private pid = 0;
67  private flamegraph: Flamegraph = new Flamegraph([]);
68  private focusRegex = '';
69  private updateFocusRegexDebounced = debounce(() => {
70    this.updateFocusRegex();
71  }, 20);
72
73  view() {
74    const heapDumpInfo = globals.heapProfileDetails;
75    if (heapDumpInfo && heapDumpInfo.type !== undefined &&
76        heapDumpInfo.ts !== undefined && heapDumpInfo.tsNs !== undefined &&
77        heapDumpInfo.pid !== undefined && heapDumpInfo.upid !== undefined) {
78      this.profileType = toProfileType(heapDumpInfo.type);
79      this.ts = heapDumpInfo.tsNs;
80      this.pid = heapDumpInfo.pid;
81      if (heapDumpInfo.flamegraph) {
82        this.flamegraph.updateDataIfChanged(
83            this.nodeRendering(), heapDumpInfo.flamegraph);
84      }
85      const height = heapDumpInfo.flamegraph ?
86          this.flamegraph.getHeight() + HEADER_HEIGHT :
87          0;
88      return m(
89          '.details-panel',
90          {
91            onclick: (e: PerfettoMouseEvent) => {
92              if (this.flamegraph !== undefined) {
93                this.onMouseClick({y: e.layerY, x: e.layerX});
94              }
95              return false;
96            },
97            onmousemove: (e: PerfettoMouseEvent) => {
98              if (this.flamegraph !== undefined) {
99                this.onMouseMove({y: e.layerY, x: e.layerX});
100                globals.rafScheduler.scheduleRedraw();
101              }
102              return false;
103            },
104            onmouseout: () => {
105              if (this.flamegraph !== undefined) {
106                this.onMouseOut();
107              }
108            }
109          },
110          m('.details-panel-heading.heap-profile',
111            {onclick: (e: MouseEvent) => e.stopPropagation()},
112            [
113              m('div.options',
114                [
115                  m('div.title', this.getTitle()),
116                  this.getViewingOptionButtons(),
117                ]),
118              m('div.details',
119                [
120                  m('div.time',
121                    `Snapshot time: ${timeToCode(heapDumpInfo.ts)}`),
122                  m('input[type=text][placeholder=Focus]', {
123                    oninput: (e: Event) => {
124                      const target = (e.target as HTMLInputElement);
125                      this.focusRegex = target.value;
126                      this.updateFocusRegexDebounced();
127                    },
128                    // Required to stop hot-key handling:
129                    onkeydown: (e: Event) => e.stopPropagation(),
130                  }),
131                  this.profileType === ProfileType.NATIVE_HEAP_PROFILE ?
132                      m('button.download',
133                        {
134                          onclick: () => {
135                            this.downloadPprof();
136                          }
137                        },
138                        m('i.material-icons', 'file_download'),
139                        'Download profile') :
140                      null
141                ]),
142            ]),
143          m(`div[style=height:${height}px]`),
144      );
145    } else {
146      return m(
147          '.details-panel',
148          m('.details-panel-heading', m('h2', `Heap Profile`)));
149    }
150  }
151
152  private getTitle(): string {
153    switch (this.profileType!) {
154      case ProfileType.NATIVE_HEAP_PROFILE:
155        return 'Heap Profile:';
156      case ProfileType.JAVA_HEAP_GRAPH:
157        return 'Java Heap:';
158      default:
159        throw new Error('unknown type');
160    }
161  }
162
163  private nodeRendering(): NodeRendering {
164    if (this.profileType === undefined) {
165      return {};
166    }
167    const viewingOption =
168        globals.state.currentHeapProfileFlamegraph!.viewingOption;
169    switch (this.profileType) {
170      case ProfileType.NATIVE_HEAP_PROFILE:
171        return RENDER_SELF_AND_TOTAL;
172      case ProfileType.JAVA_HEAP_GRAPH:
173        if (viewingOption === OBJECTS_ALLOCATED_NOT_FREED_KEY) {
174          return RENDER_OBJ_COUNT;
175        } else {
176          return RENDER_SELF_AND_TOTAL;
177        }
178      default:
179        throw new Error('unknown type');
180    }
181  }
182
183  private updateFocusRegex() {
184    globals.dispatch(Actions.changeFocusHeapProfileFlamegraph({
185      focusRegex: this.focusRegex,
186    }));
187  }
188
189  getButtonsClass(button: HeapProfileFlamegraphViewingOption): string {
190    if (globals.state.currentHeapProfileFlamegraph === null) return '';
191    return globals.state.currentHeapProfileFlamegraph.viewingOption === button ?
192        '.chosen' :
193        '';
194  }
195
196  getViewingOptionButtons(): m.Children {
197    const viewingOptions = [
198      m(`button${this.getButtonsClass(SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY)}`,
199        {
200          onclick: () => {
201            this.changeViewingOption(SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY);
202          }
203        },
204        'space'),
205      m(`button${this.getButtonsClass(OBJECTS_ALLOCATED_NOT_FREED_KEY)}`,
206        {
207          onclick: () => {
208            this.changeViewingOption(OBJECTS_ALLOCATED_NOT_FREED_KEY);
209          }
210        },
211        'objects'),
212    ];
213
214    if (this.profileType === ProfileType.NATIVE_HEAP_PROFILE) {
215      viewingOptions.push(
216          m(`button${this.getButtonsClass(ALLOC_SPACE_MEMORY_ALLOCATED_KEY)}`,
217            {
218              onclick: () => {
219                this.changeViewingOption(ALLOC_SPACE_MEMORY_ALLOCATED_KEY);
220              }
221            },
222            'alloc space'),
223          m(`button${this.getButtonsClass(OBJECTS_ALLOCATED_KEY)}`,
224            {
225              onclick: () => {
226                this.changeViewingOption(OBJECTS_ALLOCATED_KEY);
227              }
228            },
229            'alloc objects'));
230    }
231    return m('div', ...viewingOptions);
232  }
233
234  changeViewingOption(viewingOption: HeapProfileFlamegraphViewingOption) {
235    globals.dispatch(Actions.changeViewHeapProfileFlamegraph({viewingOption}));
236  }
237
238  downloadPprof() {
239    const engine = Object.values(globals.state.engines)[0];
240    if (!engine) return;
241    const src = engine.source;
242    globals.dispatch(
243        Actions.convertTraceToPprof({pid: this.pid, ts1: this.ts, src}));
244  }
245
246  private changeFlamegraphData() {
247    const data = globals.heapProfileDetails;
248    const flamegraphData = data.flamegraph === undefined ? [] : data.flamegraph;
249    this.flamegraph.updateDataIfChanged(
250        this.nodeRendering(), flamegraphData, data.expandedCallsite);
251  }
252
253  renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize) {
254    this.changeFlamegraphData();
255    const current = globals.state.currentHeapProfileFlamegraph;
256    if (current === null) return;
257    const unit =
258        current.viewingOption === SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY ||
259            current.viewingOption === ALLOC_SPACE_MEMORY_ALLOCATED_KEY ?
260        'B' :
261        '';
262    this.flamegraph.draw(ctx, size.width, size.height, 0, HEADER_HEIGHT, unit);
263  }
264
265  onMouseClick({x, y}: {x: number, y: number}): boolean {
266    const expandedCallsite = this.flamegraph.onMouseClick({x, y});
267    globals.dispatch(Actions.expandHeapProfileFlamegraph({expandedCallsite}));
268    return true;
269  }
270
271  onMouseMove({x, y}: {x: number, y: number}): boolean {
272    this.flamegraph.onMouseMove({x, y});
273    return true;
274  }
275
276  onMouseOut() {
277    this.flamegraph.onMouseOut();
278  }
279}
280