| // Copyright (C) 2020 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| import { |
| CallsiteInfo, |
| CpuProfileSampleSelection, |
| getLegacySelection, |
| } from '../common/state'; |
| import {CpuProfileDetails, globals} from '../frontend/globals'; |
| import {publishCpuProfileDetails} from '../frontend/publish'; |
| import {Engine} from '../trace_processor/engine'; |
| import {NUM, STR} from '../trace_processor/query_result'; |
| |
| import {Controller} from './controller'; |
| |
| export interface CpuProfileControllerArgs { |
| engine: Engine; |
| } |
| |
| export class CpuProfileController extends Controller<'main'> { |
| private lastSelectedSample?: CpuProfileSampleSelection; |
| private requestingData = false; |
| private queuedRunRequest = false; |
| |
| constructor(private args: CpuProfileControllerArgs) { |
| super('main'); |
| } |
| |
| run() { |
| const selection = getLegacySelection(globals.state); |
| if (!selection || selection.kind !== 'CPU_PROFILE_SAMPLE') { |
| return; |
| } |
| |
| const selectedSample = selection as CpuProfileSampleSelection; |
| if (!this.shouldRequestData(selectedSample)) { |
| return; |
| } |
| |
| if (this.requestingData) { |
| this.queuedRunRequest = true; |
| return; |
| } |
| |
| this.requestingData = true; |
| publishCpuProfileDetails({}); |
| this.lastSelectedSample = this.copyCpuProfileSample(selection); |
| |
| this.getSampleData(selectedSample.id) |
| .then((sampleData) => { |
| /* eslint-disable @typescript-eslint/strict-boolean-expressions */ |
| if ( |
| sampleData !== undefined && |
| selectedSample && |
| /* eslint-enable */ |
| this.lastSelectedSample && |
| this.lastSelectedSample.id === selectedSample.id |
| ) { |
| const cpuProfileDetails: CpuProfileDetails = { |
| id: selectedSample.id, |
| ts: selectedSample.ts, |
| utid: selectedSample.utid, |
| stack: sampleData, |
| }; |
| |
| publishCpuProfileDetails(cpuProfileDetails); |
| } |
| }) |
| .finally(() => { |
| this.requestingData = false; |
| if (this.queuedRunRequest) { |
| this.queuedRunRequest = false; |
| this.run(); |
| } |
| }); |
| } |
| |
| private copyCpuProfileSample( |
| cpuProfileSample: CpuProfileSampleSelection, |
| ): CpuProfileSampleSelection { |
| return { |
| kind: cpuProfileSample.kind, |
| id: cpuProfileSample.id, |
| utid: cpuProfileSample.utid, |
| ts: cpuProfileSample.ts, |
| }; |
| } |
| |
| private shouldRequestData(selection: CpuProfileSampleSelection) { |
| return ( |
| this.lastSelectedSample === undefined || |
| (this.lastSelectedSample !== undefined && |
| this.lastSelectedSample.id !== selection.id) |
| ); |
| } |
| |
| async getSampleData(id: number) { |
| // The goal of the query is to get all the frames of |
| // the callstack at the callsite given by |id|. To do this, it does |
| // the following: |
| // 1. Gets the leaf callsite id for the sample given by |id|. |
| // 2. For this callsite, get all the frame ids and depths |
| // for the frame and all ancestors in the callstack. |
| // 3. For each frame, get the mapping name (i.e. library which |
| // contains the frame). |
| // 4. Symbolize each frame using the symbol table if possible. |
| // 5. Sort the query by the depth of the callstack frames. |
| const sampleQuery = ` |
| SELECT |
| samples.id as id, |
| IFNULL( |
| ( |
| SELECT name |
| FROM stack_profile_symbol symbol |
| WHERE symbol.symbol_set_id = spf.symbol_set_id |
| LIMIT 1 |
| ), |
| COALESCE(spf.deobfuscated_name, spf.name, "") |
| ) AS name, |
| spm.name AS mapping |
| FROM cpu_profile_stack_sample AS samples |
| LEFT JOIN ( |
| SELECT |
| id, |
| frame_id, |
| depth |
| FROM stack_profile_callsite |
| UNION ALL |
| SELECT |
| leaf.id AS id, |
| callsite.frame_id AS frame_id, |
| callsite.depth AS depth |
| FROM stack_profile_callsite leaf |
| JOIN experimental_ancestor_stack_profile_callsite(leaf.id) AS callsite |
| ) AS callsites |
| ON samples.callsite_id = callsites.id |
| LEFT JOIN stack_profile_frame AS spf |
| ON callsites.frame_id = spf.id |
| LEFT JOIN stack_profile_mapping AS spm |
| ON spf.mapping = spm.id |
| WHERE samples.id = ${id} |
| ORDER BY callsites.depth; |
| `; |
| |
| const callsites = await this.args.engine.query(sampleQuery); |
| |
| if (callsites.numRows() === 0) { |
| return undefined; |
| } |
| |
| const it = callsites.iter({ |
| id: NUM, |
| name: STR, |
| mapping: STR, |
| }); |
| |
| const sampleData: CallsiteInfo[] = []; |
| for (; it.valid(); it.next()) { |
| sampleData.push({ |
| id: it.id, |
| totalSize: 0, |
| depth: 0, |
| parentId: 0, |
| name: it.name, |
| selfSize: 0, |
| mapping: it.mapping, |
| merged: false, |
| highlighted: false, |
| }); |
| } |
| |
| return sampleData; |
| } |
| } |