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));
   }