Add Perfetto protolog parser

Test: npm run test:presubmit
Bug: 276432490
Change-Id: I20e753e926f1bb6741c8bb7b6c3696ebd46b44db
diff --git a/tools/winscope/src/parsers/perfetto/parser_factory.ts b/tools/winscope/src/parsers/perfetto/parser_factory.ts
index b208996..c10b2eb 100644
--- a/tools/winscope/src/parsers/perfetto/parser_factory.ts
+++ b/tools/winscope/src/parsers/perfetto/parser_factory.ts
@@ -20,12 +20,18 @@
 import {Parser} from 'trace/parser';
 import {TraceFile} from 'trace/trace_file';
 import {initWasm, resetEngineWorker, WasmEngineProxy} from 'trace_processor/wasm_engine_proxy';
+import {ParserProtolog} from './parser_protolog';
 import {ParserTransactions} from './parser_transactions';
 import {ParserTransitions} from './parser_transitions';
 import {ParserSurfaceFlinger} from './surface_flinger/parser_surface_flinger';
 
 export class ParserFactory {
-  private static readonly PARSERS = [ParserSurfaceFlinger, ParserTransactions, ParserTransitions];
+  private static readonly PARSERS = [
+    ParserSurfaceFlinger,
+    ParserTransactions,
+    ParserTransitions,
+    ParserProtolog,
+  ];
   private static readonly CHUNK_SIZE_BYTES = 50 * 1024 * 1024;
   private static traceProcessor?: WasmEngineProxy;
 
diff --git a/tools/winscope/src/parsers/perfetto/parser_protolog.ts b/tools/winscope/src/parsers/perfetto/parser_protolog.ts
new file mode 100644
index 0000000..6362166
--- /dev/null
+++ b/tools/winscope/src/parsers/perfetto/parser_protolog.ts
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2024 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 {assertDefined} from 'common/assert_utils';
+import {ElapsedTimestamp, RealTimestamp, TimestampType} from 'common/time';
+import {TimeUtils} from 'common/time_utils';
+import {LogMessage} from 'trace/protolog';
+import {TraceFile} from 'trace/trace_file';
+import {TraceType} from 'trace/trace_type';
+import {WasmEngineProxy} from 'trace_processor/wasm_engine_proxy';
+import {AbstractParser} from './abstract_parser';
+
+class PerfettoLogMessageTableRow {
+  message: string = '<NO_MESSAGE>';
+  tag: string = '<NO_TAG>';
+  level: string = '<NO_LEVEL>';
+  location: string = '<NO_LOC>';
+  timestamp: bigint = 0n;
+
+  constructor(timestamp: bigint, tag: string, level: string, message: string) {
+    this.timestamp = timestamp ?? this.timestamp;
+    this.tag = tag ?? this.tag;
+    this.level = level ?? this.level;
+    this.message = message ?? this.message;
+  }
+}
+
+export class ParserProtolog extends AbstractParser<LogMessage> {
+  constructor(traceFile: TraceFile, traceProcessor: WasmEngineProxy) {
+    super(traceFile, traceProcessor);
+  }
+
+  override getTraceType(): TraceType {
+    return TraceType.PROTO_LOG;
+  }
+
+  override async getEntry(index: number, timestampType: TimestampType): Promise<LogMessage> {
+    const protologEntry = await this.queryProtoLogEntry(index);
+
+    let time: string;
+    let timestamp: bigint;
+    const realToElapsedTimeOffsetNs = assertDefined(this.realToElapsedTimeOffsetNs);
+    if (timestampType === TimestampType.REAL) {
+      timestamp = protologEntry.timestamp + realToElapsedTimeOffsetNs;
+      time = TimeUtils.format(new RealTimestamp(timestamp));
+    } else {
+      timestamp = protologEntry.timestamp;
+      time = TimeUtils.format(new ElapsedTimestamp(timestamp));
+    }
+
+    return new LogMessage(
+      protologEntry.message,
+      time,
+      protologEntry.tag,
+      protologEntry.level,
+      protologEntry.location,
+      timestamp
+    );
+  }
+
+  protected override getTableName(): string {
+    return 'protolog';
+  }
+
+  private async queryProtoLogEntry(index: number): Promise<PerfettoLogMessageTableRow> {
+    const sql = `
+      SELECT
+        ts, tag, level, message
+      FROM
+        protolog
+      WHERE protolog.id = ${index};
+    `;
+    const result = await this.traceProcessor.query(sql).waitAllRows();
+
+    if (result.numRows() !== 1) {
+      throw new Error(
+        `Expected exactly 1 protolog message with id ${index} but got ${result.numRows()}`
+      );
+    }
+
+    const entry = result.iter({});
+
+    return new PerfettoLogMessageTableRow(
+      entry.get('ts') as bigint,
+      entry.get('tag') as string,
+      entry.get('level') as string,
+      entry.get('message') as string
+    );
+  }
+}
diff --git a/tools/winscope/src/parsers/perfetto/parser_protolog_test.ts b/tools/winscope/src/parsers/perfetto/parser_protolog_test.ts
new file mode 100644
index 0000000..d734f1f
--- /dev/null
+++ b/tools/winscope/src/parsers/perfetto/parser_protolog_test.ts
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2024 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 {assertDefined} from 'common/assert_utils';
+import {ElapsedTimestamp, RealTimestamp, TimestampType} from 'common/time';
+import {UnitTestUtils} from 'test/unit/utils';
+import {Parser} from 'trace/parser';
+import {LogMessage} from 'trace/protolog';
+import {TraceType} from 'trace/trace_type';
+
+describe('Perfetto ParserProtolog', () => {
+  let parser: Parser<object>;
+
+  const expectedFirstLogMessageElapsed = {
+    text: 'Sent Transition (#11) createdAt=01-29 17:54:23.793',
+    time: '1h38m59s2ms349294ns',
+    tag: 'WindowManager',
+    level: 'VERBOSE',
+    at: '<NO_LOC>',
+    timestamp: 5939002349294n,
+  };
+
+  const expectedFirstLogMessageReal = {
+    text: 'Sent Transition (#11) createdAt=01-29 17:54:23.793',
+    time: '2024-01-29T16:54:24.827624563',
+    tag: 'WindowManager',
+    level: 'VERBOSE',
+    at: '<NO_LOC>',
+    timestamp: 1706547264827624563n,
+  };
+
+  beforeAll(async () => {
+    parser = await UnitTestUtils.getPerfettoParser(
+      TraceType.PROTO_LOG,
+      'traces/perfetto/protolog.perfetto-trace'
+    );
+  });
+
+  it('has expected trace type', () => {
+    expect(parser.getTraceType()).toEqual(TraceType.PROTO_LOG);
+  });
+
+  it('provides elapsed timestamps', () => {
+    const timestamps = assertDefined(parser.getTimestamps(TimestampType.ELAPSED));
+
+    expect(timestamps.length).toEqual(75);
+
+    // TODO: They shouldn't all have the same timestamp...
+    const expected = [
+      new ElapsedTimestamp(5939002349294n),
+      new ElapsedTimestamp(5939002349294n),
+      new ElapsedTimestamp(5939002349294n),
+    ];
+    expect(timestamps.slice(0, 3)).toEqual(expected);
+  });
+
+  it('provides real timestamps', () => {
+    const timestamps = assertDefined(parser.getTimestamps(TimestampType.REAL));
+
+    expect(timestamps.length).toEqual(75);
+
+    // TODO: They shouldn't all have the same timestamp...
+    const expected = [
+      new RealTimestamp(1706547264827624563n),
+      new RealTimestamp(1706547264827624563n),
+      new RealTimestamp(1706547264827624563n),
+    ];
+    expect(timestamps.slice(0, 3)).toEqual(expected);
+  });
+
+  it('reconstructs human-readable log message (ELAPSED time)', async () => {
+    const message = await parser.getEntry(0, TimestampType.ELAPSED);
+
+    expect(Object.assign({}, message)).toEqual(expectedFirstLogMessageElapsed);
+    expect(message).toBeInstanceOf(LogMessage);
+  });
+
+  it('reconstructs human-readable log message (REAL time)', async () => {
+    const message = await parser.getEntry(0, TimestampType.REAL);
+
+    expect(Object.assign({}, message)).toEqual(expectedFirstLogMessageReal);
+    expect(message).toBeInstanceOf(LogMessage);
+  });
+});
diff --git a/tools/winscope/src/test/fixtures/traces/perfetto/protolog.perfetto-trace b/tools/winscope/src/test/fixtures/traces/perfetto/protolog.perfetto-trace
new file mode 100644
index 0000000..48e0296
--- /dev/null
+++ b/tools/winscope/src/test/fixtures/traces/perfetto/protolog.perfetto-trace
Binary files differ