ui: Include one or more perf tracks in area selection.
- factor out some of the code in aggregation_controller, so that
it can be reused in flamegraph_controller;
- if area selection changes, call the flamegraph_controller;
- modify FlamegraphState, so it can contain start timestamp, end
timestamp and array of upids;
- in FlamegraphDetails, store a boolean indicating if the area has
changed.
Examples:
a)select part of a perf sample:
https://screenshot.googleplex.com/8TGQYTadUscnTUt
b)select perf samples across multiple processes:
https://screenshot.googleplex.com/3HgpSSQPGRMzjVw
Bug: b/195934783
Change-Id: I2d17b10eb997b37f6d0268f04530314b1aacb11a
diff --git a/src/trace_processor/importers/proto/flamegraph_construction_algorithms.cc b/src/trace_processor/importers/proto/flamegraph_construction_algorithms.cc
index 1579f4a..83dcc48 100644
--- a/src/trace_processor/importers/proto/flamegraph_construction_algorithms.cc
+++ b/src/trace_processor/importers/proto/flamegraph_construction_algorithms.cc
@@ -93,7 +93,7 @@
TraceStorage* storage,
base::Optional<UniquePid> upid,
base::Optional<std::string> upid_group,
- base::Optional<int64_t> timestamp,
+ int64_t default_timestamp,
StringId profile_type) {
const tables::StackProfileCallsiteTable& callsites_tbl =
storage->stack_profile_callsite_table();
@@ -136,18 +136,16 @@
} else {
row.depth = 0;
}
- // For heap profiling, the 'ts' column is always the arbitrary value
- // inputed in a query of the form below, not the actual time
- // when the allocation happened:
+
+ // The 'ts' column is given a default value, taken from the query.
+ // So if the query is:
// `select * form experimental_flamegraph(605908369259172, 1, 'native')`
- // However, removing this value would break the query with constraints
- // such as the one above because SQLite will do an equality check on the
- // `ts` column: `ts == 605908369259172`.
- // TODO(octaviant) find a way of removing this or giving it a meaningful
- // value
- if (timestamp) {
- row.ts = *timestamp;
- }
+ // then ts == 605908369259172
+ // This is not accurate. However, at present there is no other
+ // straightforward way of assigning timestamps to non-leaf nodes in the
+ // flamegraph tree. Non-leaf nodes would have to be assigned >= 1
+ // timestamps, which would increase data size without an advantage.
+ row.ts = default_timestamp;
if (upid) {
row.upid = *upid;
}
@@ -399,14 +397,33 @@
SqlValue::Long(tc.value)};
filtered = filtered.Filter({cs});
}
-
if (filtered.row_count() == 0) {
- return nullptr;
+ std::unique_ptr<tables::ExperimentalFlamegraphNodesTable> empty_tbl(
+ new tables::ExperimentalFlamegraphNodesTable(
+ storage->mutable_string_pool(), nullptr));
+ return empty_tbl;
+ }
+
+ // The logic underneath is selecting a default timestamp to be used by all
+ // frames which do not have a timestamp. The timestamp is taken from the query
+ // value and it's not meaningful for the row. It prevents however the rows
+ // with no timestamp from being filtered out by Sqlite, after we create the
+ // table ExperimentalFlamegraphNodesTable in this class.
+ int64_t default_timestamp = 0;
+ if (!time_constraints.empty()) {
+ auto& tc = time_constraints[0];
+ if (tc.op == FilterOp::kGt) {
+ default_timestamp = tc.value + 1;
+ } else if (tc.op == FilterOp::kLt) {
+ default_timestamp = tc.value - 1;
+ } else {
+ default_timestamp = tc.value;
+ }
}
StringId profile_type = storage->InternString("perf");
FlamegraphTableAndMergedCallsites table_and_callsites =
BuildFlamegraphTableTreeStructure(storage, upid, upid_group,
- base::nullopt, profile_type);
+ default_timestamp, profile_type);
return BuildFlamegraphTableCallstackSizeAndCount(
std::move(table_and_callsites.tbl),
table_and_callsites.callsite_to_merged_callsite, filtered);
diff --git a/ui/src/common/actions.ts b/ui/src/common/actions.ts
index 0bdc3df..4df3cb7 100644
--- a/ui/src/common/actions.ts
+++ b/ui/src/common/actions.ts
@@ -60,6 +60,7 @@
TrackState,
VisibleState,
} from './state';
+import {toNs} from './time';
type StateDraft = Draft<State>;
@@ -615,8 +616,13 @@
ts: args.ts,
type: args.type,
};
- this.openFlamegraph(
- state, {...args, viewingOption: DEFAULT_VIEWING_OPTION});
+ this.openFlamegraph(state, {
+ type: args.type,
+ startNs: toNs(state.traceTime.startSec),
+ endNs: args.ts,
+ upids: [args.upid],
+ viewingOption: DEFAULT_VIEWING_OPTION
+ });
},
selectPerfSamples(
@@ -629,21 +635,27 @@
ts: args.ts,
type: args.type,
};
- this.openFlamegraph(state, {...args, viewingOption: PERF_SAMPLES_KEY});
+ this.openFlamegraph(state, {
+ type: args.type,
+ startNs: toNs(state.traceTime.startSec),
+ endNs: args.ts,
+ upids: [args.upid],
+ viewingOption: PERF_SAMPLES_KEY
+ });
},
openFlamegraph(state: StateDraft, args: {
- id: number,
- upid: number,
- ts: number,
+ upids: number[],
+ startNs: number,
+ endNs: number,
type: string,
viewingOption: FlamegraphStateViewingOption
}): void {
state.currentFlamegraphState = {
kind: 'FLAMEGRAPH_STATE',
- id: args.id,
- upid: args.upid,
- ts: args.ts,
+ upids: args.upids,
+ startNs: args.startNs,
+ endNs: args.endNs,
type: args.type,
viewingOption: args.viewingOption,
focusRegex: ''
diff --git a/ui/src/common/state.ts b/ui/src/common/state.ts
index 5d2b388..4d0c3f7 100644
--- a/ui/src/common/state.ts
+++ b/ui/src/common/state.ts
@@ -77,7 +77,8 @@
// 11: Rename updateChromeCategories to fetchChromeCategories.
// 12: Add a field to cache mapping from UI track ID to trace track ID in order
// to speed up flow arrows rendering.
-export const STATE_VERSION = 12;
+// 13: FlamegraphState changed to support area selection.
+export const STATE_VERSION = 13;
export const SCROLLING_TRACK_GROUP = 'ScrollingTracks';
@@ -248,9 +249,9 @@
export interface FlamegraphState {
kind: 'FLAMEGRAPH_STATE';
- id: number;
- upid: number;
- ts: number;
+ upids: number[];
+ startNs: number;
+ endNs: number;
type: string;
viewingOption: FlamegraphStateViewingOption;
focusRegex: string;
diff --git a/ui/src/controller/aggregation/aggregation_controller.ts b/ui/src/controller/aggregation/aggregation_controller.ts
index 06c3cee..af41ec7 100644
--- a/ui/src/controller/aggregation/aggregation_controller.ts
+++ b/ui/src/controller/aggregation/aggregation_controller.ts
@@ -26,6 +26,7 @@
import {NUM} from '../../common/query_result';
import {Area, Sorting} from '../../common/state';
import {publishAggregateData} from '../../frontend/publish';
+import {AreaSelectionHandler} from '../area_selection_handler';
import {Controller} from '../controller';
import {globals} from '../globals';
@@ -38,17 +39,9 @@
return column.kind === 'STRING' || column.kind === 'STATE';
}
-function isAreaEqual(area: Area, previousArea?: Area) {
- if (previousArea === undefined || area.startSec !== previousArea.startSec ||
- area.endSec !== previousArea.endSec) {
- return false;
- }
- return area.tracks.every((element, i) => element === previousArea.tracks[i]);
-}
-
export abstract class AggregationController extends Controller<'main'> {
readonly kind: string;
- private previousArea?: Area;
+ private areaSelectionHandler: AreaSelectionHandler;
private previousSorting?: Sorting;
private requestingData = false;
private queuedRequest = false;
@@ -64,6 +57,7 @@
constructor(private args: AggregationControllerArgs) {
super('main');
this.kind = this.args.kind;
+ this.areaSelectionHandler = new AreaSelectionHandler();
}
run() {
@@ -82,22 +76,20 @@
});
return;
}
- const selectedArea = globals.state.areas[selection.areaId];
const aggregatePreferences =
globals.state.aggregatePreferences[this.args.kind];
- const areaChanged = !isAreaEqual(selectedArea, this.previousArea);
const sortingChanged = aggregatePreferences &&
this.previousSorting !== aggregatePreferences.sorting;
- if (!areaChanged && !sortingChanged) return;
+ const [hasAreaChanged, area] = this.areaSelectionHandler.getAreaChange();
+ if ((!hasAreaChanged && !sortingChanged) || !area) return;
if (this.requestingData) {
this.queuedRequest = true;
} else {
this.requestingData = true;
if (sortingChanged) this.previousSorting = aggregatePreferences.sorting;
- if (areaChanged) this.previousArea = Object.assign({}, selectedArea);
- this.getAggregateData(selectedArea, areaChanged)
+ this.getAggregateData(area, hasAreaChanged)
.then(data => publishAggregateData({data, kind: this.args.kind}))
.finally(() => {
this.requestingData = false;
diff --git a/ui/src/controller/area_selection_handler.ts b/ui/src/controller/area_selection_handler.ts
new file mode 100644
index 0000000..3f5368d
--- /dev/null
+++ b/ui/src/controller/area_selection_handler.ts
@@ -0,0 +1,57 @@
+// Copyright (C) 2021 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 {Area, AreaById} from '../common/state';
+import {globals as frontendGlobals} from '../frontend/globals';
+
+export class AreaSelectionHandler {
+ private previousArea?: Area;
+
+ getAreaChange(): [boolean, AreaById|undefined] {
+ const currentSelection = frontendGlobals.state.currentSelection;
+ if (currentSelection === null || currentSelection.kind !== 'AREA') {
+ return [false, undefined];
+ }
+
+ const selectedArea = frontendGlobals.state.areas[currentSelection.areaId];
+ // Area is considered changed if:
+ // 1. The new area is defined and the old area undefined.
+ // 2. The new area is undefined and the old area defined (viceversa from 1).
+ // 3. Both areas are defined but their start or end times differ.
+ // 4. Both areas are defined but their tracks differ.
+ let hasAreaChanged = (!!this.previousArea !== !!selectedArea);
+ if (selectedArea && this.previousArea) {
+ // There seems to be an issue with clang-format http://shortn/_Pt98d5MCjG
+ // where `a ||= b` is formatted to `a || = b`, by inserting a space which
+ // breaks the operator.
+ // Therefore, we are using the pattern `a = a || b` instead.
+ hasAreaChanged = hasAreaChanged ||
+ selectedArea.startSec !== this.previousArea.startSec;
+ hasAreaChanged =
+ hasAreaChanged || selectedArea.endSec !== this.previousArea.endSec;
+ hasAreaChanged = hasAreaChanged ||
+ selectedArea.tracks.length !== this.previousArea.tracks.length;
+ for (let i = 0; i < selectedArea.tracks.length; ++i) {
+ hasAreaChanged = hasAreaChanged ||
+ selectedArea.tracks[i] !== this.previousArea.tracks[i];
+ }
+ }
+
+ if (hasAreaChanged) {
+ this.previousArea = selectedArea;
+ }
+
+ return [hasAreaChanged, selectedArea];
+ }
+}
diff --git a/ui/src/controller/area_selection_handler_unittest.ts b/ui/src/controller/area_selection_handler_unittest.ts
new file mode 100644
index 0000000..563e816
--- /dev/null
+++ b/ui/src/controller/area_selection_handler_unittest.ts
@@ -0,0 +1,135 @@
+// Copyright (C) 2021 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 {AreaById, createEmptyState} from '../common/state';
+import {globals as frontendGlobals} from '../frontend/globals';
+
+import {AreaSelectionHandler} from './area_selection_handler';
+
+test('validAreaAfterUndefinedArea', () => {
+ const areaId = '0';
+ const latestArea: AreaById = {startSec: 0, endSec: 1, tracks: [], id: areaId};
+ frontendGlobals.state = createEmptyState();
+ frontendGlobals.state.currentSelection = {kind: 'AREA', areaId};
+ frontendGlobals.state.areas[areaId] = latestArea;
+
+ const areaSelectionHandler = new AreaSelectionHandler();
+ const [hasAreaChanged, selectedArea] = areaSelectionHandler.getAreaChange();
+
+ expect(hasAreaChanged).toEqual(true);
+ expect(selectedArea).toEqual(latestArea);
+});
+
+test('UndefinedAreaAfterValidArea', () => {
+ const previousAreaId = '0';
+ const previous:
+ AreaById = {startSec: 0, endSec: 1, tracks: [], id: previousAreaId};
+ frontendGlobals.state = createEmptyState();
+ frontendGlobals.state.currentSelection = {
+ kind: 'AREA',
+ areaId: previousAreaId
+ };
+ frontendGlobals.state.areas[previousAreaId] = previous;
+ const areaSelectionHandler = new AreaSelectionHandler();
+ areaSelectionHandler.getAreaChange();
+
+ const currentAreaId = '1';
+ frontendGlobals.state.currentSelection = {
+ kind: 'AREA',
+ areaId: currentAreaId
+ };
+ const [hasAreaChanged, selectedArea] = areaSelectionHandler.getAreaChange();
+
+ expect(hasAreaChanged).toEqual(true);
+ expect(selectedArea).toEqual(undefined);
+});
+
+test('UndefinedAreaAfterUndefinedArea', () => {
+ frontendGlobals.state.currentSelection = {kind: 'AREA', areaId: '0'};
+ const areaSelectionHandler = new AreaSelectionHandler();
+ areaSelectionHandler.getAreaChange();
+
+ frontendGlobals.state.currentSelection = {kind: 'AREA', areaId: '1'};
+ const [hasAreaChanged, selectedArea] = areaSelectionHandler.getAreaChange();
+
+ expect(hasAreaChanged).toEqual(true);
+ expect(selectedArea).toEqual(undefined);
+});
+
+test('validAreaAfterValidArea', () => {
+ const previousAreaId = '0';
+ const previous:
+ AreaById = {startSec: 0, endSec: 1, tracks: [], id: previousAreaId};
+ frontendGlobals.state = createEmptyState();
+ frontendGlobals.state.currentSelection = {
+ kind: 'AREA',
+ areaId: previousAreaId
+ };
+ frontendGlobals.state.areas[previousAreaId] = previous;
+ const areaSelectionHandler = new AreaSelectionHandler();
+ areaSelectionHandler.getAreaChange();
+
+ const currentAreaId = '1';
+ const current:
+ AreaById = {startSec: 1, endSec: 2, tracks: [], id: currentAreaId};
+ frontendGlobals.state.currentSelection = {
+ kind: 'AREA',
+ areaId: currentAreaId
+ };
+ frontendGlobals.state.areas[currentAreaId] = current;
+ const [hasAreaChanged, selectedArea] = areaSelectionHandler.getAreaChange();
+
+ expect(hasAreaChanged).toEqual(true);
+ expect(selectedArea).toEqual(current);
+});
+
+test('sameAreaSelected', () => {
+ const previousAreaId = '0';
+ const previous:
+ AreaById = {startSec: 0, endSec: 1, tracks: [], id: previousAreaId};
+ frontendGlobals.state = createEmptyState();
+ frontendGlobals.state.currentSelection = {
+ kind: 'AREA',
+ areaId: previousAreaId
+ };
+ frontendGlobals.state.areas[previousAreaId] = previous;
+ const areaSelectionHandler = new AreaSelectionHandler();
+ areaSelectionHandler.getAreaChange();
+
+ const currentAreaId = '0';
+ const current:
+ AreaById = {startSec: 0, endSec: 1, tracks: [], id: currentAreaId};
+ frontendGlobals.state.currentSelection = {
+ kind: 'AREA',
+ areaId: currentAreaId
+ };
+ frontendGlobals.state.areas[currentAreaId] = current;
+ const [hasAreaChanged, selectedArea] = areaSelectionHandler.getAreaChange();
+
+ expect(hasAreaChanged).toEqual(false);
+ expect(selectedArea).toEqual(current);
+});
+
+test('NonAreaSelectionAfterUndefinedArea', () => {
+ frontendGlobals.state.currentSelection = {kind: 'AREA', areaId: '0'};
+ const areaSelectionHandler = new AreaSelectionHandler();
+ areaSelectionHandler.getAreaChange();
+
+ frontendGlobals.state
+ .currentSelection = {kind: 'COUNTER', leftTs: 0, rightTs: 0, id: 1};
+ const [hasAreaChanged, selectedArea] = areaSelectionHandler.getAreaChange();
+
+ expect(hasAreaChanged).toEqual(false);
+ expect(selectedArea).toEqual(undefined);
+});
diff --git a/ui/src/controller/flamegraph_controller.ts b/ui/src/controller/flamegraph_controller.ts
index 7459c3f..9fec1f5 100644
--- a/ui/src/controller/flamegraph_controller.ts
+++ b/ui/src/controller/flamegraph_controller.ts
@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+import {Actions} from '../common/actions';
import {Engine} from '../common/engine';
import {
ALLOC_SPACE_MEMORY_ALLOCATED_KEY,
@@ -26,10 +27,18 @@
} from '../common/flamegraph_util';
import {NUM, STR} from '../common/query_result';
import {CallsiteInfo, FlamegraphState} from '../common/state';
-import {fromNs} from '../common/time';
-import {FlamegraphDetails} from '../frontend/globals';
+import {toNs} from '../common/time';
+import {
+ FlamegraphDetails,
+ globals as frontendGlobals
+} from '../frontend/globals';
import {publishFlamegraphDetails} from '../frontend/publish';
+import {
+ Config as PerfSampleConfig,
+ PERF_SAMPLES_PROFILE_TRACK_KIND
+} from '../tracks/perf_samples_profile/common';
+import {AreaSelectionHandler} from './area_selection_handler';
import {Controller} from './controller';
import {globals} from './globals';
@@ -78,15 +87,46 @@
private requestingData = false;
private queuedRequest = false;
private flamegraphDetails: FlamegraphDetails = {};
+ private areaSelectionHandler: AreaSelectionHandler;
private cache: TablesCache;
constructor(private args: FlamegraphControllerArgs) {
super('main');
this.cache = new TablesCache(args.engine, 'grouped_callsites');
+ this.areaSelectionHandler = new AreaSelectionHandler();
}
run() {
- const selection = globals.state.currentFlamegraphState;
+ const [hasAreaChanged, area] = this.areaSelectionHandler.getAreaChange();
+ if (hasAreaChanged) {
+ const upids = [];
+ if (!area) {
+ publishFlamegraphDetails(
+ {...frontendGlobals.flamegraphDetails, isInAreaSelection: false});
+ return;
+ }
+ for (const trackId of area.tracks) {
+ const trackState = frontendGlobals.state.tracks[trackId];
+ if (!trackState ||
+ trackState.kind !== PERF_SAMPLES_PROFILE_TRACK_KIND) {
+ continue;
+ }
+ upids.push((trackState.config as PerfSampleConfig).upid);
+ }
+ if (upids.length === 0) {
+ publishFlamegraphDetails(
+ {...frontendGlobals.flamegraphDetails, isInAreaSelection: false});
+ return;
+ }
+ frontendGlobals.dispatch(Actions.openFlamegraph({
+ upids,
+ startNs: toNs(area.startSec),
+ endNs: toNs(area.endSec),
+ type: 'perf',
+ viewingOption: PERF_SAMPLES_KEY
+ }));
+ }
+ const selection = frontendGlobals.state.currentFlamegraphState;
if (!selection || !this.shouldRequestData(selection)) {
return;
}
@@ -96,15 +136,17 @@
}
this.requestingData = true;
- this.assembleFlamegraphDetails(selection);
+ this.assembleFlamegraphDetails(selection, hasAreaChanged);
}
- private async assembleFlamegraphDetails(selection: FlamegraphState) {
+ private async assembleFlamegraphDetails(
+ selection: FlamegraphState, hasAreaChanged: boolean) {
const selectedFlamegraphState = {...selection};
const flamegraphMetadata = await this.getFlamegraphMetadata(
selection.type,
- selectedFlamegraphState.ts,
- selectedFlamegraphState.upid);
+ selectedFlamegraphState.startNs,
+ selectedFlamegraphState.endNs,
+ selectedFlamegraphState.upids);
if (flamegraphMetadata !== undefined) {
Object.assign(this.flamegraphDetails, flamegraphMetadata);
}
@@ -124,7 +166,8 @@
undefined :
selectedFlamegraphState.expandedCallsite.totalSize;
- const key = `${selectedFlamegraphState.upid};${selectedFlamegraphState.ts}`;
+ const key = `${selectedFlamegraphState.upids};${
+ selectedFlamegraphState.startNs};${selectedFlamegraphState.endNs}`;
try {
const flamegraphData = await this.getFlamegraphData(
@@ -132,19 +175,21 @@
selectedFlamegraphState.viewingOption ?
selectedFlamegraphState.viewingOption :
DEFAULT_VIEWING_OPTION,
- selection.ts,
- selectedFlamegraphState.upid,
+ selection.startNs,
+ selection.endNs,
+ selectedFlamegraphState.upids,
selectedFlamegraphState.type,
selectedFlamegraphState.focusRegex);
if (flamegraphData !== undefined && selection &&
selection.kind === selectedFlamegraphState.kind &&
- selection.id === selectedFlamegraphState.id &&
- selection.ts === selectedFlamegraphState.ts) {
+ selection.startNs === selectedFlamegraphState.startNs &&
+ selection.endNs === selectedFlamegraphState.endNs) {
const expandedFlamegraphData =
expandCallsites(flamegraphData, expandedId);
this.prepareAndMergeCallsites(
expandedFlamegraphData,
this.lastSelectedFlamegraphState.viewingOption,
+ hasAreaChanged,
rootSize,
this.lastSelectedFlamegraphState.expandedCallsite);
}
@@ -160,10 +205,11 @@
private shouldRequestData(selection: FlamegraphState) {
return selection.kind === 'FLAMEGRAPH_STATE' &&
(this.lastSelectedFlamegraphState === undefined ||
- (this.lastSelectedFlamegraphState.id !== selection.id ||
- this.lastSelectedFlamegraphState.ts !== selection.ts ||
+ (this.lastSelectedFlamegraphState.startNs !== selection.startNs ||
+ this.lastSelectedFlamegraphState.endNs !== selection.endNs ||
this.lastSelectedFlamegraphState.type !== selection.type ||
- this.lastSelectedFlamegraphState.upid !== selection.upid ||
+ !FlamegraphController.areArraysEqual(
+ this.lastSelectedFlamegraphState.upids, selection.upids) ||
this.lastSelectedFlamegraphState.viewingOption !==
selection.viewingOption ||
this.lastSelectedFlamegraphState.focusRegex !==
@@ -175,17 +221,20 @@
private prepareAndMergeCallsites(
flamegraphData: CallsiteInfo[],
viewingOption: string|undefined = DEFAULT_VIEWING_OPTION,
- rootSize?: number, expandedCallsite?: CallsiteInfo) {
+ hasAreaChanged: boolean, rootSize?: number,
+ expandedCallsite?: CallsiteInfo) {
this.flamegraphDetails.flamegraph = mergeCallsites(
flamegraphData, this.getMinSizeDisplayed(flamegraphData, rootSize));
this.flamegraphDetails.expandedCallsite = expandedCallsite;
this.flamegraphDetails.viewingOption = viewingOption;
+ this.flamegraphDetails.isInAreaSelection = hasAreaChanged;
publishFlamegraphDetails(this.flamegraphDetails);
}
async getFlamegraphData(
- baseKey: string, viewingOption: string, ts: number, upid: number,
- type: string, focusRegex: string): Promise<CallsiteInfo[]> {
+ baseKey: string, viewingOption: string, startNs: number, endNs: number,
+ upids: number[], type: string,
+ focusRegex: string): Promise<CallsiteInfo[]> {
let currentData: CallsiteInfo[];
const key = `${baseKey}-${viewingOption}`;
if (this.flamegraphDatasets.has(key)) {
@@ -196,8 +245,8 @@
// Collecting data for drawing flamegraph for selected profile.
// Data needs to be in following format:
// id, name, parent_id, depth, total_size
- const tableName =
- await this.prepareViewsAndTables(ts, upid, type, focusRegex);
+ const tableName = await this.prepareViewsAndTables(
+ startNs, endNs, upids, type, focusRegex);
currentData = await this.getFlamegraphDataFromTables(
tableName, viewingOption, focusRegex);
this.flamegraphDatasets.set(key, currentData);
@@ -329,7 +378,7 @@
}
private async prepareViewsAndTables(
- ts: number, upid: number, type: string,
+ startNs: number, endNs: number, upids: number[], type: string,
focusRegex: string): Promise<string> {
// Creating unique names for views so we can reuse and not delete them
// for each marker.
@@ -343,19 +392,25 @@
* TODO(octaviant) this branching should be eliminated for simplicity.
*/
if (type === 'perf') {
+ let upidConditional = `upid = ${upids[0]}`;
+ if (upids.length > 1) {
+ upidConditional =
+ `upid_group = '${FlamegraphController.serializeUpidGroup(upids)}'`;
+ }
return this.cache.getTableName(
`select id, name, map_name, parent_id, depth, cumulative_size,
cumulative_alloc_size, cumulative_count, cumulative_alloc_count,
size, alloc_size, count, alloc_count, source_file, line_number
from experimental_flamegraph
- where profile_type = "${type}" and ts <= ${ts} and upid = ${upid}
+ where profile_type = '${type}' and ${startNs} <= ts and ts <= ${
+ endNs} and ${upidConditional}
${focusRegexConditional}`);
}
return this.cache.getTableName(
`select id, name, map_name, parent_id, depth, cumulative_size,
cumulative_alloc_size, cumulative_count, cumulative_alloc_count,
size, alloc_size, count, alloc_count, source_file, line_number
- from experimental_flamegraph(${ts}, ${upid}, '${type}') ${
+ from experimental_flamegraph(${endNs}, ${upids[0]}, '${type}') ${
focusRegexConditional}`);
}
@@ -371,20 +426,44 @@
return MIN_PIXEL_DISPLAYED * rootSize / width;
}
- async getFlamegraphMetadata(type: string, ts: number, upid: number) {
+ async getFlamegraphMetadata(
+ type: string, startNs: number, endNs: number, upids: number[]) {
// Don't do anything if selection of the marker stayed the same.
if ((this.lastSelectedFlamegraphState !== undefined &&
- ((this.lastSelectedFlamegraphState.ts === ts &&
- this.lastSelectedFlamegraphState.upid === upid)))) {
+ ((this.lastSelectedFlamegraphState.startNs === startNs &&
+ this.lastSelectedFlamegraphState.endNs === endNs &&
+ FlamegraphController.areArraysEqual(
+ this.lastSelectedFlamegraphState.upids, upids))))) {
return undefined;
}
// Collecting data for more information about profile, such as:
// total memory allocated, memory that is allocated and not freed.
+ const upidGroup = FlamegraphController.serializeUpidGroup(upids);
+
const result = await this.args.engine.query(
- `select pid from process where upid = ${upid}`);
- const pid = result.firstRow({pid: NUM}).pid;
- const startTime = fromNs(ts) - globals.state.traceTime.startSec;
- return {ts: startTime, tsNs: ts, pid, upid, type};
+ `select pid from process where upid in (${upidGroup})`);
+ const it = result.iter({pid: NUM});
+ const pids = [];
+ for (let i = 0; it.valid(); ++i, it.next()) {
+ pids.push(it.pid);
+ }
+ return {startNs, durNs: endNs - startNs, pids, upids, type};
+ }
+
+ private static areArraysEqual(a: number[], b: number[]) {
+ if (a.length !== b.length) {
+ return false;
+ }
+ for (let i = 0; i < a.length; i++) {
+ if (a[i] !== b[i]) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static serializeUpidGroup(upids: number[]) {
+ return new Array(upids).join();
}
}
diff --git a/ui/src/frontend/details_panel.ts b/ui/src/frontend/details_panel.ts
index 9f95ab3..5a98bd1 100644
--- a/ui/src/frontend/details_panel.ts
+++ b/ui/src/frontend/details_panel.ts
@@ -247,6 +247,13 @@
})
});
}
+ if (globals.flamegraphDetails.isInAreaSelection) {
+ detailsPanels.push({
+ key: 'flamegraph_selection',
+ name: 'Flamegraph Selection',
+ vnode: m(FlamegraphDetailsPanel, {key: 'flamegraph'})
+ });
+ }
break;
case 'SLICE':
detailsPanels.push({
diff --git a/ui/src/frontend/flamegraph_panel.ts b/ui/src/frontend/flamegraph_panel.ts
index 62a315d..ea3d5a5 100644
--- a/ui/src/frontend/flamegraph_panel.ts
+++ b/ui/src/frontend/flamegraph_panel.ts
@@ -14,7 +14,7 @@
import * as m from 'mithril';
-import {assertExists} from '../base/logging';
+import {assertExists, assertTrue} from '../base/logging';
import {Actions} from '../common/actions';
import {
ALLOC_SPACE_MEMORY_ALLOCATED_KEY,
@@ -74,7 +74,7 @@
export class FlamegraphDetailsPanel extends Panel<FlamegraphDetailsPanelAttrs> {
private profileType?: ProfileType = undefined;
private ts = 0;
- private pid = 0;
+ private pids: number[] = [];
private flamegraph: Flamegraph = new Flamegraph([]);
private focusRegex = '';
private updateFocusRegexDebounced = debounce(() => {
@@ -84,13 +84,13 @@
view() {
const flamegraphDetails = globals.flamegraphDetails;
if (flamegraphDetails && flamegraphDetails.type !== undefined &&
- flamegraphDetails.ts !== undefined &&
- flamegraphDetails.tsNs !== undefined &&
- flamegraphDetails.pid !== undefined &&
- flamegraphDetails.upid !== undefined) {
+ flamegraphDetails.startNs !== undefined &&
+ flamegraphDetails.durNs !== undefined &&
+ flamegraphDetails.pids !== undefined &&
+ flamegraphDetails.upids !== undefined) {
this.profileType = toProfileType(flamegraphDetails.type);
- this.ts = flamegraphDetails.tsNs;
- this.pid = flamegraphDetails.pid;
+ this.ts = flamegraphDetails.durNs;
+ this.pids = flamegraphDetails.pids;
if (flamegraphDetails.flamegraph) {
this.flamegraph.updateDataIfChanged(
this.nodeRendering(), flamegraphDetails.flamegraph);
@@ -134,7 +134,7 @@
toSelectedCallsite(
flamegraphDetails.expandedCallsite)}`),
m('div.time',
- `Snapshot time: ${timeToCode(flamegraphDetails.ts)}`),
+ `Snapshot time: ${timeToCode(flamegraphDetails.durNs)}`),
m('input[type=text][placeholder=Focus]', {
oninput: (e: Event) => {
const target = (e.target as HTMLInputElement);
@@ -216,7 +216,10 @@
if (!engine) return;
getCurrentTrace()
.then(file => {
- convertTraceToPprofAndDownload(file, this.pid, this.ts);
+ assertTrue(
+ this.pids.length === 1,
+ 'Native profiles can only contain one pid.');
+ convertTraceToPprofAndDownload(file, this.pids[0], this.ts);
})
.catch(error => {
throw new Error(`Failed to get current trace ${error}`);
diff --git a/ui/src/frontend/globals.ts b/ui/src/frontend/globals.ts
index 38a3e59..3c2b847 100644
--- a/ui/src/frontend/globals.ts
+++ b/ui/src/frontend/globals.ts
@@ -39,6 +39,7 @@
type PivotTableHelperStore = Map<string, PivotTableHelper>;
type AggregateDataStore = Map<string, AggregateData>;
type Description = Map<string, string>;
+
export interface SliceDetails {
ts?: number;
dur?: number;
@@ -103,14 +104,17 @@
export interface FlamegraphDetails {
type?: string;
id?: number;
- ts?: number;
- tsNs?: number;
- pid?: number;
- upid?: number;
+ startNs?: number;
+ durNs?: number;
+ pids?: number[];
+ upids?: number[];
flamegraph?: CallsiteInfo[];
expandedCallsite?: CallsiteInfo;
viewingOption?: string;
expandedId?: number;
+ // isInAreaSelection is true if a flamegraph is part of the current area
+ // selection.
+ isInAreaSelection?: boolean;
}
export interface CpuProfileDetails {