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 {Engine} from '../common/engine';
16import {
17  ALLOC_SPACE_MEMORY_ALLOCATED_KEY,
18  DEFAULT_VIEWING_OPTION,
19  expandCallsites,
20  findRootSize,
21  mergeCallsites,
22  OBJECTS_ALLOCATED_KEY,
23  OBJECTS_ALLOCATED_NOT_FREED_KEY,
24  SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY
25} from '../common/flamegraph_util';
26import {slowlyCountRows} from '../common/query_iterator';
27import {CallsiteInfo, HeapProfileFlamegraph} from '../common/state';
28import {fromNs} from '../common/time';
29import {HeapProfileDetails} from '../frontend/globals';
30
31import {Controller} from './controller';
32import {globals} from './globals';
33
34export interface HeapProfileControllerArgs {
35  engine: Engine;
36}
37const MIN_PIXEL_DISPLAYED = 1;
38
39class TablesCache {
40  private engine: Engine;
41  private cache: Map<string, string>;
42  private prefix: string;
43  private tableId: number;
44  private cacheSizeLimit: number;
45
46  constructor(engine: Engine, prefix: string) {
47    this.engine = engine;
48    this.cache = new Map<string, string>();
49    this.prefix = prefix;
50    this.tableId = 0;
51    this.cacheSizeLimit = 10;
52  }
53
54  async getTableName(query: string): Promise<string> {
55    let tableName = this.cache.get(query);
56    if (tableName === undefined) {
57      // TODO(hjd): This should be LRU.
58      if (this.cache.size > this.cacheSizeLimit) {
59        for (const name of this.cache.values()) {
60          await this.engine.query(`drop table ${name}`);
61        }
62        this.cache.clear();
63      }
64      tableName = `${this.prefix}_${this.tableId++}`;
65      await this.engine.query(
66          `create temp table if not exists ${tableName} as ${query}`);
67      this.cache.set(query, tableName);
68    }
69    return tableName;
70  }
71}
72
73export class HeapProfileController extends Controller<'main'> {
74  private flamegraphDatasets: Map<string, CallsiteInfo[]> = new Map();
75  private lastSelectedHeapProfile?: HeapProfileFlamegraph;
76  private requestingData = false;
77  private queuedRequest = false;
78  private heapProfileDetails: HeapProfileDetails = {};
79  private cache: TablesCache;
80
81  constructor(private args: HeapProfileControllerArgs) {
82    super('main');
83    this.cache = new TablesCache(args.engine, 'grouped_callsites');
84  }
85
86  run() {
87    const selection = globals.state.currentHeapProfileFlamegraph;
88
89    if (!selection) return;
90
91    if (this.shouldRequestData(selection)) {
92      if (this.requestingData) {
93        this.queuedRequest = true;
94      } else {
95        this.requestingData = true;
96        const selectedHeapProfile: HeapProfileFlamegraph =
97            this.copyHeapProfile(selection);
98
99        this.getHeapProfileMetadata(
100                selection.type,
101                selectedHeapProfile.ts,
102                selectedHeapProfile.upid)
103            .then(result => {
104              if (result !== undefined) {
105                Object.assign(this.heapProfileDetails, result);
106              }
107
108              // TODO(hjd): Clean this up.
109              if (this.lastSelectedHeapProfile &&
110                  this.lastSelectedHeapProfile.focusRegex !==
111                      selection.focusRegex) {
112                this.flamegraphDatasets.clear();
113              }
114
115              this.lastSelectedHeapProfile = this.copyHeapProfile(selection);
116
117              const expandedId = selectedHeapProfile.expandedCallsite ?
118                  selectedHeapProfile.expandedCallsite.id :
119                  -1;
120              const rootSize =
121                  selectedHeapProfile.expandedCallsite === undefined ?
122                  undefined :
123                  selectedHeapProfile.expandedCallsite.totalSize;
124
125              const key =
126                  `${selectedHeapProfile.upid};${selectedHeapProfile.ts}`;
127
128              this.getFlamegraphData(
129                      key,
130                      selectedHeapProfile.viewingOption ?
131                          selectedHeapProfile.viewingOption :
132                          DEFAULT_VIEWING_OPTION,
133                      selection.ts,
134                      selectedHeapProfile.upid,
135                      selectedHeapProfile.type,
136                      selectedHeapProfile.focusRegex)
137                  .then(flamegraphData => {
138                    if (flamegraphData !== undefined && selection &&
139                        selection.kind === selectedHeapProfile.kind &&
140                        selection.id === selectedHeapProfile.id &&
141                        selection.ts === selectedHeapProfile.ts) {
142                      const expandedFlamegraphData =
143                          expandCallsites(flamegraphData, expandedId);
144                      this.prepareAndMergeCallsites(
145                          expandedFlamegraphData,
146                          this.lastSelectedHeapProfile!.viewingOption,
147                          rootSize,
148                          this.lastSelectedHeapProfile!.expandedCallsite);
149                    }
150                  })
151                  .finally(() => {
152                    this.requestingData = false;
153                    if (this.queuedRequest) {
154                      this.queuedRequest = false;
155                      this.run();
156                    }
157                  });
158            });
159      }
160    }
161  }
162
163  private copyHeapProfile(heapProfile: HeapProfileFlamegraph):
164      HeapProfileFlamegraph {
165    return {
166      kind: heapProfile.kind,
167      id: heapProfile.id,
168      upid: heapProfile.upid,
169      ts: heapProfile.ts,
170      type: heapProfile.type,
171      expandedCallsite: heapProfile.expandedCallsite,
172      viewingOption: heapProfile.viewingOption,
173      focusRegex: heapProfile.focusRegex,
174    };
175  }
176
177  private shouldRequestData(selection: HeapProfileFlamegraph) {
178    return selection.kind === 'HEAP_PROFILE_FLAMEGRAPH' &&
179        (this.lastSelectedHeapProfile === undefined ||
180         (this.lastSelectedHeapProfile !== undefined &&
181          (this.lastSelectedHeapProfile.id !== selection.id ||
182           this.lastSelectedHeapProfile.ts !== selection.ts ||
183           this.lastSelectedHeapProfile.type !== selection.type ||
184           this.lastSelectedHeapProfile.upid !== selection.upid ||
185           this.lastSelectedHeapProfile.viewingOption !==
186               selection.viewingOption ||
187           this.lastSelectedHeapProfile.focusRegex !== selection.focusRegex ||
188           this.lastSelectedHeapProfile.expandedCallsite !==
189               selection.expandedCallsite)));
190  }
191
192  private prepareAndMergeCallsites(
193      flamegraphData: CallsiteInfo[],
194      viewingOption: string|undefined = DEFAULT_VIEWING_OPTION,
195      rootSize?: number, expandedCallsite?: CallsiteInfo) {
196    const mergedFlamegraphData = mergeCallsites(
197        flamegraphData, this.getMinSizeDisplayed(flamegraphData, rootSize));
198    this.heapProfileDetails.flamegraph = mergedFlamegraphData;
199    this.heapProfileDetails.expandedCallsite = expandedCallsite;
200    this.heapProfileDetails.viewingOption = viewingOption;
201    globals.publish('HeapProfileDetails', this.heapProfileDetails);
202  }
203
204
205  async getFlamegraphData(
206      baseKey: string, viewingOption: string, ts: number, upid: number,
207      type: string, focusRegex: string): Promise<CallsiteInfo[]> {
208    let currentData: CallsiteInfo[];
209    const key = `${baseKey}-${viewingOption}`;
210    if (this.flamegraphDatasets.has(key)) {
211      currentData = this.flamegraphDatasets.get(key)!;
212    } else {
213      // TODO(hjd): Show loading state.
214
215      // Collecting data for drawing flamegraph for selected heap profile.
216      // Data needs to be in following format:
217      // id, name, parent_id, depth, total_size
218      const tableName =
219          await this.prepareViewsAndTables(ts, upid, type, focusRegex);
220      currentData = await this.getFlamegraphDataFromTables(
221          tableName, viewingOption, focusRegex);
222      this.flamegraphDatasets.set(key, currentData);
223    }
224    return currentData;
225  }
226
227  async getFlamegraphDataFromTables(
228      tableName: string, viewingOption = DEFAULT_VIEWING_OPTION,
229      focusRegex: string) {
230    let orderBy = '';
231    let sizeIndex = 4;
232    let selfIndex = 9;
233    // TODO(fmayer): Improve performance so this is no longer necessary.
234    // Alternatively consider collapsing frames of the same label.
235    const maxDepth = 100;
236    switch (viewingOption) {
237      case SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY:
238        orderBy = `where cumulative_size > 0 and depth < ${
239            maxDepth} order by depth, parent_id,
240            cumulative_size desc, name`;
241        sizeIndex = 4;
242        selfIndex = 9;
243        break;
244      case ALLOC_SPACE_MEMORY_ALLOCATED_KEY:
245        orderBy = `where cumulative_alloc_size > 0 and depth < ${
246            maxDepth} order by depth, parent_id,
247            cumulative_alloc_size desc, name`;
248        sizeIndex = 5;
249        selfIndex = 9;
250        break;
251      case OBJECTS_ALLOCATED_NOT_FREED_KEY:
252        orderBy = `where cumulative_count > 0 and depth < ${
253            maxDepth} order by depth, parent_id,
254            cumulative_count desc, name`;
255        sizeIndex = 6;
256        selfIndex = 10;
257        break;
258      case OBJECTS_ALLOCATED_KEY:
259        orderBy = `where cumulative_alloc_count > 0 and depth < ${
260            maxDepth} order by depth, parent_id,
261            cumulative_alloc_count desc, name`;
262        sizeIndex = 7;
263        selfIndex = 10;
264        break;
265      default:
266        break;
267    }
268
269    const callsites = await this.args.engine.query(
270        `SELECT id, IFNULL(DEMANGLE(name), name), IFNULL(parent_id, -1), depth,
271        cumulative_size, cumulative_alloc_size, cumulative_count,
272        cumulative_alloc_count, map_name, size, count from ${tableName} ${
273            orderBy}`);
274
275    const flamegraphData: CallsiteInfo[] = new Array();
276    const hashToindex: Map<number, number> = new Map();
277    for (let i = 0; i < slowlyCountRows(callsites); i++) {
278      const hash = callsites.columns[0].longValues![i];
279      let name = callsites.columns[1].stringValues![i];
280      const parentHash = callsites.columns[2].longValues![i];
281      const depth = +callsites.columns[3].longValues![i];
282      const totalSize = +callsites.columns[sizeIndex].longValues![i];
283      const mapping = callsites.columns[8].stringValues![i];
284      const selfSize = +callsites.columns[selfIndex].longValues![i];
285      const highlighted = focusRegex !== '' &&
286          name.toLocaleLowerCase().includes(focusRegex.toLocaleLowerCase());
287      const parentId =
288          hashToindex.has(+parentHash) ? hashToindex.get(+parentHash)! : -1;
289      if (depth === maxDepth - 1) {
290        name += ' [tree truncated]';
291      }
292      hashToindex.set(+hash, i);
293      // Instead of hash, we will store index of callsite in this original array
294      // as an id of callsite. That way, we have quicker access to parent and it
295      // will stay unique.
296      flamegraphData.push({
297        id: i,
298        totalSize,
299        depth,
300        parentId,
301        name,
302        selfSize,
303        mapping,
304        merged: false,
305        highlighted
306      });
307    }
308    return flamegraphData;
309  }
310
311  private async prepareViewsAndTables(
312      ts: number, upid: number, type: string,
313      focusRegex: string): Promise<string> {
314    // Creating unique names for views so we can reuse and not delete them
315    // for each marker.
316    let whereClause = '';
317    if (focusRegex !== '') {
318      whereClause = `where focus_str = '${focusRegex}'`;
319    }
320
321    return this.cache.getTableName(
322        `select id, name, map_name, parent_id, depth, cumulative_size,
323          cumulative_alloc_size, cumulative_count, cumulative_alloc_count,
324          size, alloc_size, count, alloc_count
325          from experimental_flamegraph(${ts}, ${upid}, '${type}') ${
326            whereClause}`);
327  }
328
329  getMinSizeDisplayed(flamegraphData: CallsiteInfo[], rootSize?: number):
330      number {
331    const timeState = globals.state.frontendLocalState.visibleState;
332    let width = (timeState.endSec - timeState.startSec) / timeState.resolution;
333    // TODO(168048193): Remove screen size hack:
334    width = Math.max(width, 800);
335    if (rootSize === undefined) {
336      rootSize = findRootSize(flamegraphData);
337    }
338    return MIN_PIXEL_DISPLAYED * rootSize / width;
339  }
340
341  async getHeapProfileMetadata(type: string, ts: number, upid: number) {
342    // Don't do anything if selection of the marker stayed the same.
343    if ((this.lastSelectedHeapProfile !== undefined &&
344         ((this.lastSelectedHeapProfile.ts === ts &&
345           this.lastSelectedHeapProfile.upid === upid)))) {
346      return undefined;
347    }
348
349    // Collecting data for more information about heap profile, such as:
350    // total memory allocated, memory that is allocated and not freed.
351    const pidValue = await this.args.engine.query(
352        `select pid from process where upid = ${upid}`);
353    const pid = pidValue.columns[0].longValues![0];
354    const startTime = fromNs(ts) - globals.state.traceTime.startSec;
355    return {ts: startTime, tsNs: ts, pid, upid, type};
356  }
357}
358