Merge "perfetto: migrate PRESUBMIT.py to Python 3"
diff --git a/ui/src/assets/details.scss b/ui/src/assets/details.scss
index a4f50d9..531822c 100644
--- a/ui/src/assets/details.scss
+++ b/ui/src/assets/details.scss
@@ -411,6 +411,18 @@
background-color: white;
color: #3c4b5d;
padding: 5px;
+ display: grid;
+ grid-template-columns: auto auto;
+ justify-content: space-between;
+ }
+
+ .log-filters {
+ display: flex;
+ margin-right: 5px;
+
+ .log-label {
+ padding-right: 0.35rem;
+ }
}
header.stale {
diff --git a/ui/src/common/actions.ts b/ui/src/common/actions.ts
index 2d8ab06..827517a 100644
--- a/ui/src/common/actions.ts
+++ b/ui/src/common/actions.ts
@@ -1137,6 +1137,10 @@
args.to,
args.direction);
},
+
+ setMinimumLogLevel(state: StateDraft, args: {minimumLevel: number}) {
+ state.logFilteringCriteria.minimumLevel = args.minimumLevel;
+ },
};
// Move element at `from` index to `direction` of `to` element.
diff --git a/ui/src/common/empty_state.ts b/ui/src/common/empty_state.ts
index 7a0d0f5..4698114 100644
--- a/ui/src/common/empty_state.ts
+++ b/ui/src/common/empty_state.ts
@@ -157,5 +157,8 @@
fetchChromeCategories: false,
chromeCategories: undefined,
nonSerializableState: createEmptyNonSerializableState(),
+
+ // The first two log priorities are ignored.
+ logFilteringCriteria: {minimumLevel: 2},
};
}
diff --git a/ui/src/common/state.ts b/ui/src/common/state.ts
index 935ba75..c7e988b 100644
--- a/ui/src/common/state.ts
+++ b/ui/src/common/state.ts
@@ -92,7 +92,8 @@
// 20: Refactored thread sorting order.
// 21: Updated perf sample selection to include a ts range instead of single ts
// 22: Add log selection kind.
-export const STATE_VERSION = 22;
+// 23: Add log filtering criteria for Android log entries.
+export const STATE_VERSION = 23;
export const SCROLLING_TRACK_GROUP = 'ScrollingTracks';
@@ -480,6 +481,10 @@
pivotTableRedux: PivotTableReduxState;
}
+export interface LogFilteringCriteria {
+ minimumLevel: number;
+}
+
export interface State {
version: number;
currentEngineId?: string;
@@ -574,6 +579,9 @@
// using permalink. Can be used to store those parts of the state that can't
// be serialized at the moment, such as ES6 Set and Map.
nonSerializableState: NonSerializableState;
+
+ // Android logs filtering state.
+ logFilteringCriteria: LogFilteringCriteria;
}
export const defaultTraceTime = {
diff --git a/ui/src/controller/logs_controller.ts b/ui/src/controller/logs_controller.ts
index 38cdf44..f772a0a 100644
--- a/ui/src/controller/logs_controller.ts
+++ b/ui/src/controller/logs_controller.ts
@@ -21,23 +21,25 @@
LogExistsKey,
} from '../common/logs';
import {NUM, STR} from '../common/query_result';
+import {LogFilteringCriteria} from '../common/state';
import {fromNs, TimeSpan, toNsCeil, toNsFloor} from '../common/time';
import {publishTrackData} from '../frontend/publish';
import {Controller} from './controller';
-import {App} from './globals';
+import {App, globals} from './globals';
async function updateLogBounds(
engine: Engine, span: TimeSpan): Promise<LogBounds> {
const vizStartNs = toNsFloor(span.start);
const vizEndNs = toNsCeil(span.end);
- const countResult = await engine.query(`
- select
+ const countResult = await engine.query(`select
ifnull(min(ts), 0) as minTs,
ifnull(max(ts), 0) as maxTs,
count(ts) as countTs
- from android_logs where ts >= ${vizStartNs} and ts <= ${vizEndNs}`);
+ from filtered_logs
+ where ts >= ${vizStartNs}
+ and ts <= ${vizEndNs}`);
const countRow = countResult.firstRow({minTs: NUM, maxTs: NUM, countTs: NUM});
@@ -46,12 +48,12 @@
const total = countRow.countTs;
const minResult = await engine.query(`
- select ifnull(max(ts), 0) as maxTs from android_logs where ts < ${
+ select ifnull(max(ts), 0) as maxTs from filtered_logs where ts < ${
vizStartNs}`);
const startNs = minResult.firstRow({maxTs: NUM}).maxTs;
const maxResult = await engine.query(`
- select ifnull(min(ts), 0) as minTs from android_logs where ts > ${
+ select ifnull(min(ts), 0) as minTs from filtered_logs where ts > ${
vizEndNs}`);
const endNs = maxResult.firstRow({minTs: NUM}).minTs;
@@ -81,7 +83,7 @@
prio,
ifnull(tag, '[NULL]') as tag,
ifnull(msg, '[NULL]') as msg
- from android_logs
+ from filtered_logs
where ${vizSqlBounds}
order by ts
limit ${pagination.start}, ${pagination.count}
@@ -147,12 +149,15 @@
}
/**
- * LogsController looks at two parts of the state:
+ * LogsController looks at three parts of the state:
* 1. The visible trace window
* 2. The requested offset and count the log lines to display
+ * 3. The log filtering criteria.
* And keeps two bits of published information up to date:
* 1. The total number of log messages in visible range
* 2. The logs lines that should be displayed
+ * Based on the log filtering criteria, it also builds the filtered_logs view
+ * and keeps it up to date.
*/
export class LogsController extends Controller<'main'> {
private app: App;
@@ -160,6 +165,9 @@
private span: TimeSpan;
private pagination: Pagination;
private hasLogs = false;
+ private logFilteringCriteria?: LogFilteringCriteria;
+ private requestingData = false;
+ private queuedRunRequest = false;
constructor(args: LogsControllerArgs) {
super('main');
@@ -187,7 +195,21 @@
run() {
if (!this.hasLogs) return;
+ if (this.requestingData) {
+ this.queuedRunRequest = true;
+ return;
+ }
+ this.requestingData = true;
+ this.updateLogTracks().finally(() => {
+ this.requestingData = false;
+ if (this.queuedRunRequest) {
+ this.queuedRunRequest = false;
+ this.run();
+ }
+ });
+ }
+ private async updateLogTracks() {
const traceTime = this.app.state.frontendLocalState.visibleState;
const newSpan = new TimeSpan(traceTime.startSec, traceTime.endSec);
const oldSpan = this.span;
@@ -204,36 +226,37 @@
const requestedPagination = new Pagination(offset, count);
const oldPagination = this.pagination;
- const needSpanUpdate = !oldSpan.equals(newSpan);
- const needPaginationUpdate = !oldPagination.contains(requestedPagination);
+ const newFilteringCriteria =
+ this.logFilteringCriteria !== globals.state.logFilteringCriteria;
+ const needBoundsUpdate = !oldSpan.equals(newSpan) || newFilteringCriteria;
+ const needEntriesUpdate =
+ !oldPagination.contains(requestedPagination) || needBoundsUpdate;
- // TODO(hjd): We could waste a lot of time queueing useless updates here.
- // We should avoid enqueuing a request when one is in progress.
- if (needSpanUpdate) {
+ if (newFilteringCriteria) {
+ this.logFilteringCriteria = globals.state.logFilteringCriteria;
+ await this.engine.query('drop view if exists filtered_logs');
+ await this.engine.query(`create view filtered_logs as
+ select * from android_logs
+ where prio >= ${this.logFilteringCriteria.minimumLevel}`);
+ }
+
+ if (needBoundsUpdate) {
this.span = newSpan;
- updateLogBounds(this.engine, newSpan).then((data) => {
- if (!newSpan.equals(this.span)) return;
- publishTrackData({
- id: LogBoundsKey,
- data,
- });
+ const logBounds = await updateLogBounds(this.engine, newSpan);
+ publishTrackData({
+ id: LogBoundsKey,
+ data: logBounds,
});
}
- // TODO(hjd): We could waste a lot of time queueing useless updates here.
- // We should avoid enqueuing a request when one is in progress.
- if (needSpanUpdate || needPaginationUpdate) {
+ if (needEntriesUpdate) {
this.pagination = requestedPagination.grow(100);
-
- updateLogEntries(this.engine, newSpan, this.pagination).then((data) => {
- if (!this.pagination.contains(requestedPagination)) return;
- publishTrackData({
- id: LogEntriesKey,
- data,
- });
+ const logEntries =
+ await updateLogEntries(this.engine, newSpan, this.pagination);
+ publishTrackData({
+ id: LogEntriesKey,
+ data: logEntries,
});
}
-
- return [];
}
}
diff --git a/ui/src/frontend/logs_filters.ts b/ui/src/frontend/logs_filters.ts
new file mode 100644
index 0000000..e0abef8
--- /dev/null
+++ b/ui/src/frontend/logs_filters.ts
@@ -0,0 +1,63 @@
+// Copyright (C) 2022 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 * as m from 'mithril';
+
+import {Actions} from '../common/actions';
+import {globals} from './globals';
+
+export const LOG_PRIORITIES =
+ ['-', '-', 'Verbose', 'Debug', 'Info', 'Warn', 'Error', 'Fatal'];
+const IGNORED_STATES = 2;
+
+interface LogPriorityWidgetAttrs {
+ options: string[];
+ selectedIndex: number;
+ onSelect: (id: number) => void;
+}
+
+class LogPriorityWidget implements m.ClassComponent<LogPriorityWidgetAttrs> {
+ view(vnode: m.Vnode<LogPriorityWidgetAttrs>) {
+ const attrs = vnode.attrs;
+ const optionComponents = [];
+ for (let i = IGNORED_STATES; i < attrs.options.length; i++) {
+ const selected = i === attrs.selectedIndex;
+ optionComponents.push(
+ m('option', {value: i, selected}, attrs.options[i]));
+ }
+ return m(
+ 'select',
+ {
+ onchange: (e: InputEvent) => {
+ const selectionValue = (e.target as HTMLSelectElement).value;
+ attrs.onSelect(Number(selectionValue));
+ },
+ },
+ optionComponents,
+ );
+ }
+}
+
+export class LogsFilters implements m.ClassComponent {
+ view(_: m.CVnode<{}>) {
+ return m(
+ '.log-filters', m('.log-label', 'Log Level'), m(LogPriorityWidget, {
+ options: LOG_PRIORITIES,
+ selectedIndex: globals.state.logFilteringCriteria.minimumLevel,
+ onSelect: (minimumLevel) => {
+ globals.dispatch(Actions.setMinimumLogLevel({minimumLevel}));
+ },
+ }));
+ }
+}
diff --git a/ui/src/frontend/logs_panel.ts b/ui/src/frontend/logs_panel.ts
index 1b6d165..3abfcd3 100644
--- a/ui/src/frontend/logs_panel.ts
+++ b/ui/src/frontend/logs_panel.ts
@@ -26,12 +26,11 @@
import {TimeSpan} from '../common/time';
import {globals} from './globals';
+import {LOG_PRIORITIES, LogsFilters} from './logs_filters';
import {Panel} from './panel';
const ROW_H = 20;
-const PRIO_TO_LETTER = ['-', '-', 'V', 'D', 'I', 'W', 'E', 'F'];
-
export class LogPanel extends Panel<{}> {
private scrollContainer?: HTMLElement;
private bounds?: LogBounds;
@@ -116,7 +115,7 @@
const tags = this.entries.tags;
const messages = this.entries.messages;
for (let i = 0; i < this.entries.timestamps.length; i++) {
- const priorityLetter = PRIO_TO_LETTER[priorities[i]];
+ const priorityLetter = LOG_PRIORITIES[priorities[i]][0];
const ts = timestamps[i];
const prioClass = priorityLetter || '';
rows.push(
@@ -142,7 +141,10 @@
{
'class': isStale ? 'stale' : '',
},
- `Logs rows [${offset}, ${offset + count}] / ${total}`),
+ [
+ `Logs rows [${offset}, ${offset + count}] / ${total}`,
+ m(LogsFilters),
+ ]),
m('.rows', {style: {height: `${total * ROW_H}px`}}, rows));
}