• 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 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 {Actions} from '../common/actions';
16import {Engine} from '../common/engine';
17import {
18  ALLOC_SPACE_MEMORY_ALLOCATED_KEY,
19  DEFAULT_VIEWING_OPTION,
20  expandCallsites,
21  findRootSize,
22  mergeCallsites,
23  OBJECTS_ALLOCATED_KEY,
24  OBJECTS_ALLOCATED_NOT_FREED_KEY,
25  PERF_SAMPLES_KEY,
26  SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY,
27} from '../common/flamegraph_util';
28import {NUM, STR} from '../common/query_result';
29import {CallsiteInfo, FlamegraphState, ProfileType} from '../common/state';
30import {tpDurationToSeconds, TPTime} from '../common/time';
31import {FlamegraphDetails, globals} from '../frontend/globals';
32import {publishFlamegraphDetails} from '../frontend/publish';
33import {
34  Config as PerfSampleConfig,
35  PERF_SAMPLES_PROFILE_TRACK_KIND,
36} from '../tracks/perf_samples_profile';
37
38import {AreaSelectionHandler} from './area_selection_handler';
39import {Controller} from './controller';
40
41export function profileType(s: string): ProfileType {
42  if (isProfileType(s)) {
43    return s;
44  }
45  if (s.startsWith('heap_profile')) {
46    return ProfileType.HEAP_PROFILE;
47  }
48  throw new Error('Unknown type ${s}');
49}
50
51function isProfileType(s: string): s is ProfileType {
52  return Object.values(ProfileType).includes(s as ProfileType);
53}
54
55function getFlamegraphType(type: ProfileType) {
56  switch (type) {
57    case ProfileType.HEAP_PROFILE:
58    case ProfileType.NATIVE_HEAP_PROFILE:
59    case ProfileType.JAVA_HEAP_SAMPLES:
60      return 'native';
61    case ProfileType.JAVA_HEAP_GRAPH:
62      return 'graph';
63    case ProfileType.PERF_SAMPLE:
64      return 'perf';
65    default:
66      throw new Error(`Unexpected profile type ${profileType}`);
67  }
68}
69
70export interface FlamegraphControllerArgs {
71  engine: Engine;
72}
73const MIN_PIXEL_DISPLAYED = 1;
74
75class TablesCache {
76  private engine: Engine;
77  private cache: Map<string, string>;
78  private prefix: string;
79  private tableId: number;
80  private cacheSizeLimit: number;
81
82  constructor(engine: Engine, prefix: string) {
83    this.engine = engine;
84    this.cache = new Map<string, string>();
85    this.prefix = prefix;
86    this.tableId = 0;
87    this.cacheSizeLimit = 10;
88  }
89
90  async getTableName(query: string): Promise<string> {
91    let tableName = this.cache.get(query);
92    if (tableName === undefined) {
93      // TODO(hjd): This should be LRU.
94      if (this.cache.size > this.cacheSizeLimit) {
95        for (const name of this.cache.values()) {
96          await this.engine.query(`drop table ${name}`);
97        }
98        this.cache.clear();
99      }
100      tableName = `${this.prefix}_${this.tableId++}`;
101      await this.engine.query(
102          `create temp table if not exists ${tableName} as ${query}`);
103      this.cache.set(query, tableName);
104    }
105    return tableName;
106  }
107}
108
109export class FlamegraphController extends Controller<'main'> {
110  private flamegraphDatasets: Map<string, CallsiteInfo[]> = new Map();
111  private lastSelectedFlamegraphState?: FlamegraphState;
112  private requestingData = false;
113  private queuedRequest = false;
114  private flamegraphDetails: FlamegraphDetails = {};
115  private areaSelectionHandler: AreaSelectionHandler;
116  private cache: TablesCache;
117
118  constructor(private args: FlamegraphControllerArgs) {
119    super('main');
120    this.cache = new TablesCache(args.engine, 'grouped_callsites');
121    this.areaSelectionHandler = new AreaSelectionHandler();
122  }
123
124  run() {
125    const [hasAreaChanged, area] = this.areaSelectionHandler.getAreaChange();
126    if (hasAreaChanged) {
127      const upids = [];
128      if (!area) {
129        this.checkCompletionAndPublishFlamegraph(
130            {...globals.flamegraphDetails, isInAreaSelection: false});
131        return;
132      }
133      for (const trackId of area.tracks) {
134        const trackState = globals.state.tracks[trackId];
135        if (!trackState ||
136            trackState.kind !== PERF_SAMPLES_PROFILE_TRACK_KIND) {
137          continue;
138        }
139        upids.push((trackState.config as PerfSampleConfig).upid);
140      }
141      if (upids.length === 0) {
142        this.checkCompletionAndPublishFlamegraph(
143            {...globals.flamegraphDetails, isInAreaSelection: false});
144        return;
145      }
146      globals.dispatch(Actions.openFlamegraph({
147        upids,
148        start: area.start,
149        end: area.end,
150        type: ProfileType.PERF_SAMPLE,
151        viewingOption: PERF_SAMPLES_KEY,
152      }));
153    }
154    const selection = globals.state.currentFlamegraphState;
155    if (!selection || !this.shouldRequestData(selection)) {
156      return;
157    }
158    if (this.requestingData) {
159      this.queuedRequest = true;
160      return;
161    }
162    this.requestingData = true;
163
164    this.assembleFlamegraphDetails(selection, area !== undefined);
165  }
166
167  private async assembleFlamegraphDetails(
168      selection: FlamegraphState, isInAreaSelection: boolean) {
169    const selectedFlamegraphState = {...selection};
170    const flamegraphMetadata = await this.getFlamegraphMetadata(
171        selection.type,
172        selectedFlamegraphState.start,
173        selectedFlamegraphState.end,
174        selectedFlamegraphState.upids);
175    if (flamegraphMetadata !== undefined) {
176      Object.assign(this.flamegraphDetails, flamegraphMetadata);
177    }
178
179    // TODO(hjd): Clean this up.
180    if (this.lastSelectedFlamegraphState &&
181        this.lastSelectedFlamegraphState.focusRegex !== selection.focusRegex) {
182      this.flamegraphDatasets.clear();
183    }
184
185    this.lastSelectedFlamegraphState = {...selection};
186
187    const expandedId = selectedFlamegraphState.expandedCallsite ?
188        selectedFlamegraphState.expandedCallsite.id :
189        -1;
190    const rootSize = selectedFlamegraphState.expandedCallsite === undefined ?
191        undefined :
192        selectedFlamegraphState.expandedCallsite.totalSize;
193
194    const key = `${selectedFlamegraphState.upids};${
195        selectedFlamegraphState.start};${selectedFlamegraphState.end}`;
196
197    try {
198      const flamegraphData = await this.getFlamegraphData(
199          key,
200          selectedFlamegraphState.viewingOption ?
201              selectedFlamegraphState.viewingOption :
202              DEFAULT_VIEWING_OPTION,
203          selection.start,
204          selection.end,
205          selectedFlamegraphState.upids,
206          selectedFlamegraphState.type,
207          selectedFlamegraphState.focusRegex);
208      if (flamegraphData !== undefined && selection &&
209          selection.kind === selectedFlamegraphState.kind &&
210          selection.start === selectedFlamegraphState.start &&
211          selection.end === selectedFlamegraphState.end) {
212        const expandedFlamegraphData =
213            expandCallsites(flamegraphData, expandedId);
214        this.prepareAndMergeCallsites(
215            expandedFlamegraphData,
216            this.lastSelectedFlamegraphState.viewingOption,
217            isInAreaSelection,
218            rootSize,
219            this.lastSelectedFlamegraphState.expandedCallsite);
220      }
221    } finally {
222      this.requestingData = false;
223      if (this.queuedRequest) {
224        this.queuedRequest = false;
225        this.run();
226      }
227    }
228  }
229
230  private shouldRequestData(selection: FlamegraphState) {
231    return selection.kind === 'FLAMEGRAPH_STATE' &&
232        (this.lastSelectedFlamegraphState === undefined ||
233         (this.lastSelectedFlamegraphState.start !== selection.start ||
234          this.lastSelectedFlamegraphState.end !== selection.end ||
235          this.lastSelectedFlamegraphState.type !== selection.type ||
236          !FlamegraphController.areArraysEqual(
237              this.lastSelectedFlamegraphState.upids, selection.upids) ||
238          this.lastSelectedFlamegraphState.viewingOption !==
239              selection.viewingOption ||
240          this.lastSelectedFlamegraphState.focusRegex !==
241              selection.focusRegex ||
242          this.lastSelectedFlamegraphState.expandedCallsite !==
243              selection.expandedCallsite));
244  }
245
246  private prepareAndMergeCallsites(
247      flamegraphData: CallsiteInfo[],
248      viewingOption: string|undefined = DEFAULT_VIEWING_OPTION,
249      isInAreaSelection: boolean, rootSize?: number,
250      expandedCallsite?: CallsiteInfo) {
251    this.flamegraphDetails.flamegraph = mergeCallsites(
252        flamegraphData, this.getMinSizeDisplayed(flamegraphData, rootSize));
253    this.flamegraphDetails.expandedCallsite = expandedCallsite;
254    this.flamegraphDetails.viewingOption = viewingOption;
255    this.flamegraphDetails.isInAreaSelection = isInAreaSelection;
256    this.checkCompletionAndPublishFlamegraph(this.flamegraphDetails);
257  }
258
259  private async checkCompletionAndPublishFlamegraph(flamegraphDetails:
260                                                        FlamegraphDetails) {
261    flamegraphDetails.graphIncomplete =
262        (await this.args.engine.query(`select value from stats
263       where severity = 'error' and name = 'heap_graph_non_finalized_graph'`))
264            .firstRow({value: NUM})
265            .value > 0;
266    publishFlamegraphDetails(flamegraphDetails);
267  }
268
269  async getFlamegraphData(
270      baseKey: string, viewingOption: string, start: TPTime, end: TPTime,
271      upids: number[], type: ProfileType,
272      focusRegex: string): Promise<CallsiteInfo[]> {
273    let currentData: CallsiteInfo[];
274    const key = `${baseKey}-${viewingOption}`;
275    if (this.flamegraphDatasets.has(key)) {
276      currentData = this.flamegraphDatasets.get(key)!;
277    } else {
278      // TODO(hjd): Show loading state.
279
280      // Collecting data for drawing flamegraph for selected profile.
281      // Data needs to be in following format:
282      // id, name, parent_id, depth, total_size
283      const tableName =
284          await this.prepareViewsAndTables(start, end, upids, type, focusRegex);
285      currentData = await this.getFlamegraphDataFromTables(
286          tableName, viewingOption, focusRegex);
287      this.flamegraphDatasets.set(key, currentData);
288    }
289    return currentData;
290  }
291
292  async getFlamegraphDataFromTables(
293      tableName: string, viewingOption = DEFAULT_VIEWING_OPTION,
294      focusRegex: string) {
295    let orderBy = '';
296    let totalColumnName: 'cumulativeSize'|'cumulativeAllocSize'|
297        'cumulativeCount'|'cumulativeAllocCount' = 'cumulativeSize';
298    let selfColumnName: 'size'|'count' = 'size';
299    // TODO(fmayer): Improve performance so this is no longer necessary.
300    // Alternatively consider collapsing frames of the same label.
301    const maxDepth = 100;
302    switch (viewingOption) {
303      case ALLOC_SPACE_MEMORY_ALLOCATED_KEY:
304        orderBy = `where cumulative_alloc_size > 0 and depth < ${
305            maxDepth} order by depth, parent_id,
306            cumulative_alloc_size desc, name`;
307        totalColumnName = 'cumulativeAllocSize';
308        selfColumnName = 'size';
309        break;
310      case OBJECTS_ALLOCATED_NOT_FREED_KEY:
311        orderBy = `where cumulative_count > 0 and depth < ${
312            maxDepth} order by depth, parent_id,
313            cumulative_count desc, name`;
314        totalColumnName = 'cumulativeCount';
315        selfColumnName = 'count';
316        break;
317      case OBJECTS_ALLOCATED_KEY:
318        orderBy = `where cumulative_alloc_count > 0 and depth < ${
319            maxDepth} order by depth, parent_id,
320            cumulative_alloc_count desc, name`;
321        totalColumnName = 'cumulativeAllocCount';
322        selfColumnName = 'count';
323        break;
324      case PERF_SAMPLES_KEY:
325      case SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY:
326        orderBy = `where cumulative_size > 0 and depth < ${
327            maxDepth} order by depth, parent_id,
328            cumulative_size desc, name`;
329        totalColumnName = 'cumulativeSize';
330        selfColumnName = 'size';
331        break;
332      default:
333        break;
334    }
335
336    const callsites = await this.args.engine.query(`
337        SELECT
338        id as hash,
339        IFNULL(IFNULL(DEMANGLE(name), name), '[NULL]') as name,
340        IFNULL(parent_id, -1) as parentHash,
341        depth,
342        cumulative_size as cumulativeSize,
343        cumulative_alloc_size as cumulativeAllocSize,
344        cumulative_count as cumulativeCount,
345        cumulative_alloc_count as cumulativeAllocCount,
346        map_name as mapping,
347        size,
348        count,
349        IFNULL(source_file, '') as sourceFile,
350        IFNULL(line_number, -1) as lineNumber
351        from ${tableName} ${orderBy}`);
352
353    const flamegraphData: CallsiteInfo[] = [];
354    const hashToindex: Map<number, number> = new Map();
355    const it = callsites.iter({
356      hash: NUM,
357      name: STR,
358      parentHash: NUM,
359      depth: NUM,
360      cumulativeSize: NUM,
361      cumulativeAllocSize: NUM,
362      cumulativeCount: NUM,
363      cumulativeAllocCount: NUM,
364      mapping: STR,
365      sourceFile: STR,
366      lineNumber: NUM,
367      size: NUM,
368      count: NUM,
369    });
370    for (let i = 0; it.valid(); ++i, it.next()) {
371      const hash = it.hash;
372      let name = it.name;
373      const parentHash = it.parentHash;
374      const depth = it.depth;
375      const totalSize = it[totalColumnName];
376      const selfSize = it[selfColumnName];
377      const mapping = it.mapping;
378      const highlighted = focusRegex !== '' &&
379          name.toLocaleLowerCase().includes(focusRegex.toLocaleLowerCase());
380      const parentId =
381          hashToindex.has(+parentHash) ? hashToindex.get(+parentHash)! : -1;
382
383      let location: string|undefined;
384      if (/[a-zA-Z]/i.test(it.sourceFile)) {
385        location = it.sourceFile;
386        if (it.lineNumber !== -1) {
387          location += `:${it.lineNumber}`;
388        }
389      }
390
391      if (depth === maxDepth - 1) {
392        name += ' [tree truncated]';
393      }
394      // Instead of hash, we will store index of callsite in this original array
395      // as an id of callsite. That way, we have quicker access to parent and it
396      // will stay unique:
397      hashToindex.set(hash, i);
398
399      flamegraphData.push({
400        id: i,
401        totalSize,
402        depth,
403        parentId,
404        name,
405        selfSize,
406        mapping,
407        merged: false,
408        highlighted,
409        location,
410      });
411    }
412    return flamegraphData;
413  }
414
415  private async prepareViewsAndTables(
416      start: TPTime, end: TPTime, upids: number[], type: ProfileType,
417      focusRegex: string): Promise<string> {
418    // Creating unique names for views so we can reuse and not delete them
419    // for each marker.
420    let focusRegexConditional = '';
421    if (focusRegex !== '') {
422      focusRegexConditional = `and focus_str = '${focusRegex}'`;
423    }
424    const flamegraphType = getFlamegraphType(type);
425
426    /*
427     * TODO(octaviant) this branching should be eliminated for simplicity.
428     */
429    if (type === ProfileType.PERF_SAMPLE) {
430      let upidConditional = `upid = ${upids[0]}`;
431      if (upids.length > 1) {
432        upidConditional =
433            `upid_group = '${FlamegraphController.serializeUpidGroup(upids)}'`;
434      }
435      return this.cache.getTableName(
436          `select id, name, map_name, parent_id, depth, cumulative_size,
437          cumulative_alloc_size, cumulative_count, cumulative_alloc_count,
438          size, alloc_size, count, alloc_count, source_file, line_number
439          from experimental_flamegraph
440          where profile_type = '${flamegraphType}' and ${start} <= ts and
441              ts <= ${end} and ${upidConditional}
442          ${focusRegexConditional}`);
443    }
444    return this.cache.getTableName(
445        `select id, name, map_name, parent_id, depth, cumulative_size,
446          cumulative_alloc_size, cumulative_count, cumulative_alloc_count,
447          size, alloc_size, count, alloc_count, source_file, line_number
448          from experimental_flamegraph
449          where profile_type = '${flamegraphType}'
450            and ts = ${end}
451            and upid = ${upids[0]}
452            ${focusRegexConditional}`);
453  }
454
455  getMinSizeDisplayed(flamegraphData: CallsiteInfo[], rootSize?: number):
456      number {
457    const timeState = globals.state.frontendLocalState.visibleState;
458    const dur = globals.stateVisibleTime().duration;
459    let width = tpDurationToSeconds(dur / timeState.resolution);
460    // TODO(168048193): Remove screen size hack:
461    width = Math.max(width, 800);
462    if (rootSize === undefined) {
463      rootSize = findRootSize(flamegraphData);
464    }
465    return MIN_PIXEL_DISPLAYED * rootSize / width;
466  }
467
468  async getFlamegraphMetadata(
469      type: ProfileType, start: TPTime, end: TPTime,
470      upids: number[]): Promise<FlamegraphDetails|undefined> {
471    // Don't do anything if selection of the marker stayed the same.
472    if ((this.lastSelectedFlamegraphState !== undefined &&
473         ((this.lastSelectedFlamegraphState.start === start &&
474           this.lastSelectedFlamegraphState.end === end &&
475           FlamegraphController.areArraysEqual(
476               this.lastSelectedFlamegraphState.upids, upids))))) {
477      return undefined;
478    }
479
480    // Collecting data for more information about profile, such as:
481    // total memory allocated, memory that is allocated and not freed.
482    const upidGroup = FlamegraphController.serializeUpidGroup(upids);
483
484    const result = await this.args.engine.query(
485        `select pid from process where upid in (${upidGroup})`);
486    const it = result.iter({pid: NUM});
487    const pids = [];
488    for (let i = 0; it.valid(); ++i, it.next()) {
489      pids.push(it.pid);
490    }
491    return {start, dur: end - start, pids, upids, type};
492  }
493
494  private static areArraysEqual(a: number[], b: number[]) {
495    if (a.length !== b.length) {
496      return false;
497    }
498    for (let i = 0; i < a.length; i++) {
499      if (a[i] !== b[i]) {
500        return false;
501      }
502    }
503    return true;
504  }
505
506  private static serializeUpidGroup(upids: number[]) {
507    return new Array(upids).join();
508  }
509}
510