Convert protolog legacy to perfetto.

Bug: 411363817
Test: npm run test:unit:ci

Change-Id: I8fe15affa5dd162c4e22f63700f113bc71cad600
diff --git a/tools/winscope/src/parsers/legacy/abstract_parser.ts b/tools/winscope/src/parsers/legacy/abstract_parser.ts
index c2ecc81..45a6658 100644
--- a/tools/winscope/src/parsers/legacy/abstract_parser.ts
+++ b/tools/winscope/src/parsers/legacy/abstract_parser.ts
@@ -97,7 +97,11 @@
     throw new Error('Not implemented');
   }
 
-  convertToPerfettoPackets(sequenceId: number): perfetto.protos.TracePacket[] {
+  convertToPerfettoPackets(
+    sequenceId: number,
+    trustedPid: number,
+    trustedUid: number,
+  ): perfetto.protos.TracePacket[] {
     throw new Error('not implemented');
   }
 
diff --git a/tools/winscope/src/parsers/legacy_to_perfetto_converter.ts b/tools/winscope/src/parsers/legacy_to_perfetto_converter.ts
index c112fe8..91f9b4f 100644
--- a/tools/winscope/src/parsers/legacy_to_perfetto_converter.ts
+++ b/tools/winscope/src/parsers/legacy_to_perfetto_converter.ts
@@ -209,6 +209,17 @@
     legacyParsers: Array<Parser<object>>,
     trace: perfetto.protos.Trace,
   ): perfetto.protos.TracePacket[] {
+    const [largestUid, largestPid] = trace.packet.reduce(
+      ([uid, pid], packet) => {
+        return [
+          Math.max(packet.trustedUid ?? 0, uid),
+          Math.max(packet.trustedPid ?? 0, pid),
+        ];
+      },
+      [0, 0],
+    );
+    const [trustedUid, trustedPid] = [largestUid + 1, largestPid + 1];
+
     const packets: perfetto.protos.TracePacket[] = [];
     let sequenceId =
       Math.max(
@@ -217,7 +228,11 @@
     for (const parser of legacyParsers) {
       if (parser.convertToPerfettoPackets) {
         try {
-          const legacyPackets = parser.convertToPerfettoPackets(sequenceId);
+          const legacyPackets = parser.convertToPerfettoPackets(
+            sequenceId,
+            trustedUid,
+            trustedPid,
+          );
           if (legacyPackets.length > 0) {
             legacyPackets[0].firstPacketOnSequence = true;
             packets.push(...legacyPackets);
diff --git a/tools/winscope/src/parsers/protolog/legacy/legacy_to_perfetto_configs.ts b/tools/winscope/src/parsers/protolog/legacy/legacy_to_perfetto_configs.ts
new file mode 100644
index 0000000..99f2510
--- /dev/null
+++ b/tools/winscope/src/parsers/protolog/legacy/legacy_to_perfetto_configs.ts
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2025 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 Long from 'long';
+import {perfetto} from 'protos/perfetto/trace/static';
+import configJson32 from '../../../../configs/services.core.protolog32.json'; // eslint-disable-line no-restricted-imports
+import configJson64 from '../../../../configs/services.core.protolog64.json'; // eslint-disable-line no-restricted-imports
+
+interface LegacyConfig {
+  groups: {[key: string]: {tag: string}};
+  messages: {
+    [key: string]: {
+      message: string;
+      level: string;
+      group: string;
+      at: string;
+    };
+  };
+}
+
+function makeProtologViewerConfig(
+  configJson: LegacyConfig,
+): perfetto.protos.ProtoLogViewerConfig {
+  const groupNameToId = new Map<string, number>();
+
+  const groups: perfetto.protos.ProtoLogViewerConfig.Group[] = Object.entries(
+    configJson.groups,
+  ).map(([name, {tag}], index) => {
+    const group = perfetto.protos.ProtoLogViewerConfig.Group.fromObject({
+      id: index + 1,
+      name,
+      tag,
+    });
+    groupNameToId.set(group.name, group.id);
+    return group;
+  });
+
+  const messages: perfetto.protos.ProtoLogViewerConfig.MessageData[] =
+    Object.entries(configJson.messages).map(
+      ([id, {message, level, group, at}]) => {
+        let protologLevel: perfetto.protos.ProtoLogLevel;
+        switch (level) {
+          case 'DEBUG':
+            protologLevel = perfetto.protos.ProtoLogLevel.PROTOLOG_LEVEL_DEBUG;
+            break;
+          case 'VERBOSE':
+            protologLevel =
+              perfetto.protos.ProtoLogLevel.PROTOLOG_LEVEL_VERBOSE;
+            break;
+          case 'INFO':
+            protologLevel = perfetto.protos.ProtoLogLevel.PROTOLOG_LEVEL_INFO;
+            break;
+          case 'WARN':
+            protologLevel = perfetto.protos.ProtoLogLevel.PROTOLOG_LEVEL_WARN;
+            break;
+          case 'ERROR':
+            protologLevel = perfetto.protos.ProtoLogLevel.PROTOLOG_LEVEL_ERROR;
+            break;
+          case 'WTF':
+            protologLevel = perfetto.protos.ProtoLogLevel.PROTOLOG_LEVEL_WTF;
+            break;
+          default:
+            protologLevel =
+              perfetto.protos.ProtoLogLevel.PROTOLOG_LEVEL_UNDEFINED;
+        }
+        return perfetto.protos.ProtoLogViewerConfig.MessageData.fromObject({
+          messageId: Long.fromString(id),
+          message,
+          level: protologLevel,
+          groupId: groupNameToId.get(group),
+          location: at,
+        });
+      },
+    );
+  return perfetto.protos.ProtoLogViewerConfig.fromObject({
+    messages,
+    groups,
+  });
+}
+
+export const CONFIG_32 = makeProtologViewerConfig(configJson32);
+export const CONFIG_64 = makeProtologViewerConfig(configJson64);
diff --git a/tools/winscope/src/parsers/protolog/legacy/parser_protolog.ts b/tools/winscope/src/parsers/protolog/legacy/parser_protolog.ts
index fe35b62..9790dd6 100644
--- a/tools/winscope/src/parsers/protolog/legacy/parser_protolog.ts
+++ b/tools/winscope/src/parsers/protolog/legacy/parser_protolog.ts
@@ -15,20 +15,25 @@
  */
 
 import {assertDefined} from 'common/assert_utils';
+import {utf8Encode} from 'common/string_utils';
 import {Timestamp} from 'common/time/time';
+import Long from 'long';
 import {AbstractParser} from 'parsers/legacy/abstract_parser';
-import {LogMessage} from 'parsers/protolog/log_message';
-import {ParserProtologUtils} from 'parsers/protolog/parser_protolog_utils';
+import {perfetto} from 'protos/perfetto/trace/static';
 import root from 'protos/protolog/udc/json';
 import {com} from 'protos/protolog/udc/static';
 import {TraceType} from 'trace/trace_type';
 import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
 import configJson32 from '../../../../configs/services.core.protolog32.json'; // eslint-disable-line no-restricted-imports
 import configJson64 from '../../../../configs/services.core.protolog64.json'; // eslint-disable-line no-restricted-imports
+import {CONFIG_32, CONFIG_64} from './legacy_to_perfetto_configs';
 
 type ProtoLogMessage = com.android.internal.protolog.IProtoLogMessage;
 
-class ParserProtoLog extends AbstractParser<PropertyTreeNode, ProtoLogMessage> {
+export class ParserProtoLog extends AbstractParser<
+  PropertyTreeNode,
+  ProtoLogMessage
+> {
   private static readonly ProtoLogFileProto = root.lookupType(
     'com.android.internal.protolog.ProtoLogFileProto',
   );
@@ -61,15 +66,15 @@
       buffer,
     ) as com.android.internal.protolog.IProtoLogFileProto;
 
-    if (fileProto.version === ParserProtoLog.PROTOLOG_32_BIT_VERSION) {
+    if (this.is32BitVersion(fileProto.log?.at(0))) {
       if (configJson32.version !== ParserProtoLog.PROTOLOG_32_BIT_VERSION) {
-        const message = `Unsupported ProtoLog JSON config version ${configJson32.version} expected ${ParserProtoLog.PROTOLOG_32_BIT_VERSION}`;
+        const message = `Unsupported ProtoLog JSON config version ${configJson32.version}. Expected ${ParserProtoLog.PROTOLOG_32_BIT_VERSION}`;
         console.log(message);
         throw new TypeError(message);
       }
-    } else if (fileProto.version === ParserProtoLog.PROTOLOG_64_BIT_VERSION) {
+    } else if (this.is64BitVersion(fileProto.log?.at(0))) {
       if (configJson64.version !== ParserProtoLog.PROTOLOG_64_BIT_VERSION) {
-        const message = `Unsupported ProtoLog JSON config version ${configJson64.version} expected ${ParserProtoLog.PROTOLOG_64_BIT_VERSION}`;
+        const message = `Unsupported ProtoLog JSON config version ${configJson64.version}. Expected ${ParserProtoLog.PROTOLOG_64_BIT_VERSION}`;
         console.log(message);
         throw new TypeError(message);
       }
@@ -95,185 +100,124 @@
     return fileProto.log;
   }
 
+  private is32BitVersion(entry: ProtoLogMessage | undefined): boolean {
+    return (entry?.messageHashLegacy ?? 0) > 0;
+  }
+
+  private is64BitVersion(entry: ProtoLogMessage | undefined): boolean {
+    return (
+      entry?.messageHash instanceof Long &&
+      (entry.messageHash.toString() ?? '0') !== '0'
+    );
+  }
+
+  override convertToPerfettoPackets(
+    sequenceId: number,
+    trustedUid = 1,
+    trustedPid = 1,
+  ): perfetto.protos.TracePacket[] {
+    const packets = [];
+    const firstPacket = this.createPacket(sequenceId, trustedUid, trustedPid);
+    firstPacket.sequenceFlags =
+      perfetto.protos.TracePacket.SequenceFlags.SEQ_INCREMENTAL_STATE_CLEARED;
+    packets.push(firstPacket);
+    packets.push(this.makeViewerConfigPacket(sequenceId, trustedUid));
+
+    const stringToIid = new Map<string, number>();
+    let stringIid = 1;
+
+    for (const entry of this.decodedEntries) {
+      const packet = this.createPacket(sequenceId, trustedUid, trustedPid);
+      packet.timestamp = assertDefined(entry.elapsedRealtimeNanos);
+
+      let messageId: Long;
+      if (this.is64BitVersion(entry)) {
+        messageId = assertDefined(entry.messageHash);
+      } else {
+        messageId = Long.fromNumber(assertDefined(entry.messageHashLegacy));
+      }
+
+      const strParamIids: number[] = [];
+
+      entry.strParams?.forEach((param) => {
+        const iid = stringToIid.get(param);
+        if (iid !== undefined) {
+          strParamIids.push(iid);
+        } else {
+          stringToIid.set(param, stringIid);
+          const packet = this.createPacket(sequenceId, trustedUid, trustedPid);
+          this.updateInternedDataPacket(packet, param, stringIid);
+          packets.push(packet);
+          strParamIids.push(stringIid);
+          stringIid++;
+        }
+      });
+
+      if (strParamIids.length > 0) {
+        packet.sequenceFlags =
+          perfetto.protos.TracePacket.SequenceFlags.SEQ_NEEDS_INCREMENTAL_STATE;
+      }
+
+      packet.protologMessage = perfetto.protos.ProtoLogMessage.create({
+        messageId,
+        strParamIids,
+        sint64Params: entry.sint64Params,
+        doubleParams: entry.doubleParams,
+        booleanParams: entry.booleanParams?.map((param) => {
+          return param ? 1 : 0;
+        }),
+      });
+      packets.push(packet);
+    }
+
+    return packets;
+  }
+
   protected override getTimestamp(entry: ProtoLogMessage): Timestamp {
     return this.timestampConverter.makeTimestampFromBootTimeNs(
       BigInt(assertDefined(entry.elapsedRealtimeNanos).toString()),
     );
   }
 
-  override processDecodedEntry(
-    index: number,
-    entry: ProtoLogMessage,
-  ): PropertyTreeNode {
-    let messageHash = assertDefined(entry.messageHash).toString();
-    let config: ProtologConfig | undefined = undefined;
-    if (messageHash !== null && messageHash !== '0') {
-      config = assertDefined(configJson64) as ProtologConfig;
+  private makeViewerConfigPacket(
+    sequenceId: number,
+    trustedUid: number,
+  ): perfetto.protos.TracePacket {
+    const packet = this.createPacket(sequenceId, trustedUid, undefined);
+    if (this.is64BitVersion(this.decodedEntries[0])) {
+      packet.protologViewerConfig = CONFIG_64;
     } else {
-      messageHash = assertDefined(entry.messageHashLegacy).toString();
-      config = assertDefined(configJson32) as ProtologConfig;
+      packet.protologViewerConfig = CONFIG_32;
     }
-
-    const message: ConfigMessage | undefined = config.messages[messageHash];
-    const tag: string | undefined = message
-      ? config.groups[message.group].tag
-      : undefined;
-
-    const logMessage = this.makeLogMessage(entry, message, tag);
-    return ParserProtologUtils.makeMessagePropertiesTree(
-      logMessage,
-      this.timestampConverter,
-      this.getRealToMonotonicTimeOffsetNs() !== undefined,
-    );
+    return packet;
   }
 
-  private makeLogMessage(
-    entry: ProtoLogMessage,
-    message: ConfigMessage | undefined,
-    tag: string | undefined,
-  ): LogMessage {
-    if (!message || !tag) {
-      return this.makeLogMessageWithoutFormat(entry);
-    }
-    try {
-      return this.makeLogMessageWithFormat(entry, message, tag);
-    } catch (error) {
-      if (error instanceof FormatStringMismatchError) {
-        return this.makeLogMessageWithoutFormat(entry);
-      }
-      throw this.createParsingError((error as Error).message);
-    }
+  private updateInternedDataPacket(
+    packet: perfetto.protos.TracePacket,
+    str: string,
+    iid: number,
+  ): perfetto.protos.TracePacket {
+    const internedString = perfetto.protos.InternedString.fromObject({
+      iid: Long.fromNumber(iid),
+      str: utf8Encode(str),
+    });
+    packet.internedData = perfetto.protos.InternedData.fromObject({
+      protologStringArgs: [internedString],
+    });
+    return packet;
   }
 
-  private makeLogMessageWithFormat(
-    entry: ProtoLogMessage,
-    message: ConfigMessage,
-    tag: string,
-  ): LogMessage {
-    let text = '';
-
-    const strParams: string[] = assertDefined(entry.strParams);
-    let strParamsIdx = 0;
-    const sint64Params: Array<bigint> = assertDefined(entry.sint64Params).map(
-      (param) => BigInt(param.toString()),
-    );
-    let sint64ParamsIdx = 0;
-    const doubleParams: number[] = assertDefined(entry.doubleParams);
-    let doubleParamsIdx = 0;
-    const booleanParams: boolean[] = assertDefined(entry.booleanParams);
-    let booleanParamsIdx = 0;
-
-    const messageFormat = message.message;
-    for (let i = 0; i < messageFormat.length; ) {
-      if (messageFormat[i] === '%') {
-        if (i + 1 >= messageFormat.length) {
-          // Should never happen - protologtool checks for that
-          throw this.createParsingError('invalid format string');
-        }
-        switch (messageFormat[i + 1]) {
-          case '%':
-            text += '%';
-            break;
-          case 'd':
-            text += this.getParam(sint64Params, sint64ParamsIdx++).toString(10);
-            break;
-          case 'o':
-            text += this.getParam(sint64Params, sint64ParamsIdx++).toString(8);
-            break;
-          case 'x':
-            text += this.getParam(sint64Params, sint64ParamsIdx++).toString(16);
-            break;
-          case 'f':
-            text += this.getParam(doubleParams, doubleParamsIdx++).toFixed(6);
-            break;
-          case 'e':
-            text += this.getParam(
-              doubleParams,
-              doubleParamsIdx++,
-            ).toExponential();
-            break;
-          case 'g':
-            text += this.getParam(doubleParams, doubleParamsIdx++).toString();
-            break;
-          case 's':
-            text += this.getParam(strParams, strParamsIdx++);
-            break;
-          case 'b':
-            text += this.getParam(booleanParams, booleanParamsIdx++).toString();
-            break;
-          default:
-            // Should never happen - protologtool checks for that
-            throw this.createParsingError(
-              'invalid format string conversion: ' + messageFormat[i + 1],
-            );
-        }
-        i += 2;
-      } else {
-        text += messageFormat[i];
-        i += 1;
-      }
+  private createPacket(
+    sequenceId: number,
+    trustedUid: number,
+    trustedPid: number | undefined,
+  ): perfetto.protos.TracePacket {
+    const packet = perfetto.protos.TracePacket.create();
+    packet.trustedPacketSequenceId = sequenceId;
+    packet.trustedUid = trustedUid;
+    if (trustedPid) {
+      packet.trustedPid = trustedPid;
     }
-
-    return {
-      text,
-      tag,
-      level: message.level,
-      at: message.at,
-      timestamp: BigInt(assertDefined(entry.elapsedRealtimeNanos).toString()),
-    };
-  }
-
-  private getParam<T>(arr: T[], idx: number): T {
-    if (arr.length <= idx) {
-      throw this.createParsingError('no param for format string conversion');
-    }
-    return arr[idx];
-  }
-
-  private makeLogMessageWithoutFormat(entry: ProtoLogMessage): LogMessage {
-    const text =
-      assertDefined(entry.messageHash).toString() +
-      ' - [' +
-      assertDefined(entry.strParams).toString() +
-      '] [' +
-      assertDefined(entry.sint64Params).toString() +
-      '] [' +
-      assertDefined(entry.doubleParams).toString() +
-      '] [' +
-      assertDefined(entry.booleanParams).toString() +
-      ']';
-
-    return {
-      text,
-      tag: 'INVALID',
-      level: 'invalid',
-      at: '',
-      timestamp: BigInt(assertDefined(entry.elapsedRealtimeNanos).toString()),
-    };
-  }
-
-  private createParsingError(msg: string) {
-    return new Error(`Protolog parsing error: ${msg}`);
+    return packet;
   }
 }
-
-class FormatStringMismatchError extends Error {
-  constructor(message: string) {
-    super(message);
-  }
-}
-
-interface ProtologConfig {
-  version: string;
-  messages: {[key: string]: ConfigMessage};
-  groups: {[key: string]: {tag: string}};
-}
-
-interface ConfigMessage {
-  message: string;
-  level: string;
-  group: string;
-  at: string;
-}
-
-export {ParserProtoLog};
diff --git a/tools/winscope/src/parsers/protolog/legacy/parser_protolog_test.ts b/tools/winscope/src/parsers/protolog/legacy/parser_protolog_test.ts
index a91f9f5..b3f4bea 100644
--- a/tools/winscope/src/parsers/protolog/legacy/parser_protolog_test.ts
+++ b/tools/winscope/src/parsers/protolog/legacy/parser_protolog_test.ts
@@ -15,139 +15,382 @@
  */
 
 import {assertDefined} from 'common/assert_utils';
+import {utf8Encode} from 'common/string_utils';
 import {
   TimestampConverterUtils,
   timestampEqualityTester,
 } from 'common/time/test_utils';
 import {Timestamp} from 'common/time/time';
+import Long from 'long';
+import {perfetto} from 'protos/perfetto/trace/static';
 import {LegacyParserProvider} from 'test/unit/fixture_utils';
 import {CoarseVersion} from 'trace/coarse_version';
 import {Parser} from 'trace/parser';
 import {TraceType} from 'trace/trace_type';
 import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
+import {CONFIG_32, CONFIG_64} from './legacy_to_perfetto_configs';
 
-interface ExpectedMessage {
-  'message': string;
-  'ts': string;
-  'at': string;
-  'level': string;
-  'tag': string;
+interface ExpectedInternedData {
+  packetIndex: number;
+  str: string;
+  iid: number;
 }
 
-const genProtoLogTest =
-  (
-    traceFile: string,
-    timestampCount: number,
-    first3ExpectedRealTimestamps: Timestamp[],
-    expectedFirstMessage: ExpectedMessage,
-  ) =>
-  () => {
-    let parser: Parser<PropertyTreeNode>;
+interface ExpectedMessagePacket {
+  packetIndex: number;
+  sequenceFlags: perfetto.protos.TracePacket.SequenceFlags;
+  timestamp: Long;
+  messageId: Long;
+  strParamIids: number[];
+  sint64Params: Long[];
+  doubleParams: number[];
+  booleanParams: number[];
+}
 
-    beforeAll(async () => {
-      jasmine.addCustomEqualityTester(timestampEqualityTester);
-      parser = await new LegacyParserProvider()
-        .addFilename(traceFile)
-        .getParser<PropertyTreeNode>();
+interface ExpectedMessage {
+  message: string;
+  ts: string;
+  at: string;
+  level: string;
+  tag: string;
+}
+
+abstract class ParserProtologTest {
+  abstract readonly traceFile: string;
+  abstract readonly timestampCount: number;
+  abstract readonly first3ExpectedRealTimestamps: Timestamp[];
+  abstract readonly expectedConfig: perfetto.protos.IProtoLogViewerConfig;
+  abstract readonly internedData1: ExpectedInternedData;
+  abstract readonly internedData2: ExpectedInternedData;
+  abstract readonly messagePacketWithInternedStrings: ExpectedMessagePacket;
+  abstract readonly messagePacketNoInternedStrings: ExpectedMessagePacket;
+  abstract readonly expectedFirstMessage: ExpectedMessage;
+
+  execute() {
+    describe('ParserProtologTest', () => {
+      const [sequenceId, trustedUid, trustedPid] = [10, 3, 5];
+      let parser: Parser<PropertyTreeNode>;
+
+      beforeAll(async () => {
+        jasmine.addCustomEqualityTester(timestampEqualityTester);
+        parser = await new LegacyParserProvider()
+          .addFilename(this.traceFile)
+          .getParser<PropertyTreeNode>();
+      });
+
+      it('has expected trace type', () => {
+        expect(parser.getTraceType()).toEqual(TraceType.PROTO_LOG);
+      });
+
+      it('has expected coarse version', () => {
+        expect(parser.getCoarseVersion()).toEqual(CoarseVersion.LEGACY);
+      });
+
+      it('has expected length', () => {
+        expect(parser.getLengthEntries()).toEqual(this.timestampCount);
+      });
+
+      it('provides timestamps', () => {
+        const timestamps = assertDefined(parser.getTimestamps());
+        expect(timestamps.length).toEqual(this.timestampCount);
+
+        expect(timestamps.slice(0, 3)).toEqual(
+          this.first3ExpectedRealTimestamps,
+        );
+      });
+
+      it('does not provide entry', () => {
+        expect(parser.getEntry).toThrow();
+      });
+
+      it('converts to valid perfetto packets', async () => {
+        const packets = parser.convertToPerfettoPackets!(
+          sequenceId,
+          trustedUid,
+          trustedPid,
+        );
+        expect(
+          packets.filter((packet) => packet.protologMessage).length,
+        ).toEqual(this.timestampCount);
+
+        const firstPacket = packets[0];
+        expect(firstPacket.trustedPacketSequenceId).toEqual(sequenceId);
+        expect(firstPacket.sequenceFlags).toEqual(
+          perfetto.protos.TracePacket.SequenceFlags
+            .SEQ_INCREMENTAL_STATE_CLEARED,
+        );
+        expect(firstPacket.trustedUid).toEqual(trustedUid);
+        expect(firstPacket.trustedPid).toEqual(trustedPid);
+        expect(firstPacket.internedData).toBeNull();
+        expect(firstPacket.protologViewerConfig).toBeNull();
+        expect(firstPacket.protologMessage).toBeNull();
+
+        const viewerConfigPacket = packets[1];
+        expect(viewerConfigPacket.trustedPacketSequenceId).toEqual(sequenceId);
+        expect(viewerConfigPacket.sequenceFlags).toEqual(
+          perfetto.protos.TracePacket.SequenceFlags.SEQ_UNSPECIFIED,
+        );
+        expect(viewerConfigPacket.protologViewerConfig).toEqual(
+          this.expectedConfig,
+        );
+        expect(viewerConfigPacket.trustedUid).toEqual(trustedUid);
+        expect(viewerConfigPacket.trustedPid).toEqual(0);
+        expect(viewerConfigPacket.internedData).toBeNull();
+        expect(viewerConfigPacket.protologMessage).toBeNull();
+
+        checkInternedDataPacket(packets, this.internedData1);
+        checkInternedDataPacket(packets, this.internedData2);
+
+        checkMessagePacket(packets, this.messagePacketNoInternedStrings);
+        checkMessagePacket(packets, this.messagePacketWithInternedStrings);
+      });
+
+      function checkMessagePacket(
+        packets: perfetto.protos.TracePacket[],
+        expectedMsg: ExpectedMessagePacket,
+      ) {
+        const packet = packets[expectedMsg.packetIndex];
+        expect(packet.trustedPacketSequenceId).toEqual(sequenceId);
+        expect(packet.sequenceFlags).toEqual(expectedMsg.sequenceFlags);
+        expect(packet.trustedUid).toEqual(trustedUid);
+        expect(packet.trustedPid).toEqual(trustedPid);
+        const ts1 = expectedMsg.timestamp;
+        ts1.unsigned = true;
+        expect(packet.timestamp).toEqual(ts1);
+        expect(packet.protologMessage?.messageId).toEqual(
+          expectedMsg.messageId,
+        );
+        expect(packet.protologMessage?.strParamIids).toEqual(
+          expectedMsg.strParamIids,
+        );
+        expect(packet.protologMessage?.booleanParams).toEqual(
+          expectedMsg.booleanParams,
+        );
+        expect(packet.protologMessage?.doubleParams).toEqual(
+          expectedMsg.doubleParams,
+        );
+        expect(packet.protologMessage?.sint64Params).toEqual(
+          expectedMsg.sint64Params,
+        );
+        expect(packet.protologViewerConfig).toBeNull();
+        expect(packet.internedData).toBeNull();
+      }
+
+      function checkInternedDataPacket(
+        packets: perfetto.protos.TracePacket[],
+        expectedData: ExpectedInternedData,
+      ) {
+        const packet = packets[expectedData.packetIndex];
+        expect(packet.trustedPacketSequenceId).toEqual(sequenceId);
+        expect(packet.sequenceFlags).toEqual(
+          perfetto.protos.TracePacket.SequenceFlags.SEQ_UNSPECIFIED,
+        );
+        expect(packet.trustedUid).toEqual(trustedUid);
+        expect(packet.trustedPid).toEqual(trustedPid);
+        expect(packet.internedData?.protologStringArgs).toEqual([
+          perfetto.protos.InternedString.fromObject({
+            iid: Long.fromNumber(expectedData.iid),
+            str: utf8Encode(expectedData.str),
+          }),
+        ]);
+        expect(packet.protologViewerConfig).toBeNull();
+        expect(packet.protologMessage).toBeNull();
+      }
+
+      it('converts to valid perfetto trace', async () => {
+        const perfettoParser = await new LegacyParserProvider()
+          .addFilename(this.traceFile)
+          .setConvertToPerfetto(true)
+          .setLatestRealToElapsedTimeOffsetNs(
+            assertDefined(parser.getRealToBootTimeOffsetNs()),
+          )
+          .getParser<PropertyTreeNode>();
+
+        expect(perfettoParser.getTimestamps()?.slice(0, 3)).toEqual(
+          this.first3ExpectedRealTimestamps,
+        );
+
+        const message = await perfettoParser.getEntry(0);
+
+        expect(
+          assertDefined(message.getChildByName('text')).formattedValue(),
+        ).toEqual(this.expectedFirstMessage.message);
+        expect(
+          assertDefined(message.getChildByName('timestamp')).formattedValue(),
+        ).toEqual(this.expectedFirstMessage.ts);
+        expect(
+          assertDefined(message.getChildByName('tag')).formattedValue(),
+        ).toEqual(this.expectedFirstMessage.tag);
+        expect(
+          assertDefined(message.getChildByName('level')).formattedValue(),
+        ).toEqual(this.expectedFirstMessage.level);
+        expect(
+          assertDefined(message.getChildByName('at')).formattedValue(),
+        ).toEqual(this.expectedFirstMessage.at);
+      });
     });
+  }
+}
 
-    it('has expected trace type', () => {
-      expect(parser.getTraceType()).toEqual(TraceType.PROTO_LOG);
-    });
-
-    it('has expected coarse version', () => {
-      expect(parser.getCoarseVersion()).toEqual(CoarseVersion.LEGACY);
-    });
-
-    it('has expected length', () => {
-      expect(parser.getLengthEntries()).toEqual(timestampCount);
-    });
-
-    it('provides timestamps', () => {
-      const timestamps = assertDefined(parser.getTimestamps());
-      expect(timestamps.length).toEqual(timestampCount);
-
-      expect(timestamps.slice(0, 3)).toEqual(first3ExpectedRealTimestamps);
-    });
-
-    it('reconstructs human-readable log message', async () => {
-      const message = await parser.getEntry(0);
-
-      expect(
-        assertDefined(message.getChildByName('text')).formattedValue(),
-      ).toEqual(expectedFirstMessage['message']);
-      expect(
-        assertDefined(message.getChildByName('timestamp')).formattedValue(),
-      ).toEqual(expectedFirstMessage['ts']);
-      expect(
-        assertDefined(message.getChildByName('tag')).formattedValue(),
-      ).toEqual(expectedFirstMessage['tag']);
-      expect(
-        assertDefined(message.getChildByName('level')).formattedValue(),
-      ).toEqual(expectedFirstMessage['level']);
-      expect(
-        assertDefined(message.getChildByName('at')).formattedValue(),
-      ).toEqual(expectedFirstMessage['at']);
-    });
+class ParserProtolog32Test extends ParserProtologTest {
+  override readonly traceFile =
+    'traces/elapsed_and_real_timestamp/ProtoLog32.pb';
+  override readonly timestampCount = 50;
+  override readonly first3ExpectedRealTimestamps = [
+    TimestampConverterUtils.makeRealTimestamp(1655727125377266486n),
+    TimestampConverterUtils.makeRealTimestamp(1655727125377336718n),
+    TimestampConverterUtils.makeRealTimestamp(1655727125377350430n),
+  ];
+  override readonly expectedConfig = CONFIG_32;
+  override readonly internedData1: ExpectedInternedData = {
+    packetIndex: 2,
+    iid: 1,
+    str: 'ITYPE_IME',
   };
+  override readonly internedData2: ExpectedInternedData = {
+    packetIndex: 3,
+    iid: 2,
+    str: 'false',
+  };
+  override readonly messagePacketNoInternedStrings: ExpectedMessagePacket = {
+    packetIndex: 50,
+    sequenceFlags: perfetto.protos.TracePacket.SequenceFlags.SEQ_UNSPECIFIED,
+    timestamp: Long.fromNumber(850755642097),
+    messageId: Long.fromNumber(1984782949),
+    strParamIids: [],
+    sint64Params: [],
+    booleanParams: [],
+    doubleParams: [],
+  };
+  override readonly messagePacketWithInternedStrings: ExpectedMessagePacket = {
+    packetIndex: 4,
+    sequenceFlags:
+      perfetto.protos.TracePacket.SequenceFlags.SEQ_NEEDS_INCREMENTAL_STATE,
+    timestamp: Long.fromNumber(850746266486),
+    messageId: Long.fromNumber(2070726247),
+    strParamIids: [1, 2, 2],
+    sint64Params: [],
+    booleanParams: [],
+    doubleParams: [],
+  };
+  override readonly expectedFirstMessage: ExpectedMessage = {
+    message:
+      'InsetsSource updateVisibility for ITYPE_IME, serverVisible: false clientVisible: false',
+    ts: '2022-06-20, 12:12:05.377',
+    tag: 'WindowManager',
+    level: 'DEBUG',
+    at: 'com/android/server/wm/InsetsSourceProvider.java',
+  };
+}
 
-describe('ParserProtoLog', () => {
-  describe(
-    '32',
-    genProtoLogTest(
-      'traces/elapsed_and_real_timestamp/ProtoLog32.pb',
-      50,
-      [
-        TimestampConverterUtils.makeRealTimestamp(1655727125377266486n),
-        TimestampConverterUtils.makeRealTimestamp(1655727125377336718n),
-        TimestampConverterUtils.makeRealTimestamp(1655727125377350430n),
-      ],
-      {
-        'message':
-          'InsetsSource updateVisibility for ITYPE_IME, serverVisible: false clientVisible: false',
-        'ts': '2022-06-20, 12:12:05.377',
-        'tag': 'WindowManager',
-        'level': 'DEBUG',
-        'at': 'com/android/server/wm/InsetsSourceProvider.java',
-      },
-    ),
-  );
-  describe(
-    '64',
-    genProtoLogTest(
-      'traces/elapsed_and_real_timestamp/ProtoLog64.pb',
-      4615,
-      [
-        TimestampConverterUtils.makeRealTimestamp(1709196806399529939n),
-        TimestampConverterUtils.makeRealTimestamp(1709196806399763866n),
-        TimestampConverterUtils.makeRealTimestamp(1709196806400297151n),
-      ],
-      {
-        'message': 'Starting activity when config will change = false',
-        'ts': '2024-02-29, 08:53:26.400',
-        'tag': 'WindowManager',
-        'level': 'VERBOSE',
-        'at': 'com/android/server/wm/ActivityStarter.java',
-      },
-    ),
-  );
-  describe(
-    'Missing config message',
-    genProtoLogTest(
-      'traces/elapsed_and_real_timestamp/ProtoLogMissingConfigMessage.pb',
-      7295,
-      [
-        TimestampConverterUtils.makeRealTimestamp(1669053909777144978n),
-        TimestampConverterUtils.makeRealTimestamp(1669053909778011697n),
-        TimestampConverterUtils.makeRealTimestamp(1669053909778800707n),
-      ],
-      {
-        'message': 'SURFACE isColorSpaceAgnostic=true: NotificationShade',
-        'ts': '2022-11-21, 18:05:09.777',
-        'tag': 'WindowManager',
-        'level': 'INFO',
-        'at': 'com/android/server/wm/WindowSurfaceController.java',
-      },
-    ),
-  );
+class ParserProtolog64Test extends ParserProtologTest {
+  override readonly traceFile =
+    'traces/elapsed_and_real_timestamp/ProtoLog64.pb';
+  override readonly timestampCount = 4615;
+  override readonly first3ExpectedRealTimestamps = [
+    TimestampConverterUtils.makeRealTimestamp(1709196806399529939n),
+    TimestampConverterUtils.makeRealTimestamp(1709196806399763866n),
+    TimestampConverterUtils.makeRealTimestamp(1709196806400297151n),
+  ];
+  override readonly expectedConfig = CONFIG_64;
+  override readonly internedData1: ExpectedInternedData = {
+    packetIndex: 5,
+    iid: 1,
+    str: 'ActivityRecord{e361a5d u0 com.google.android.gm/.ConversationListActivityGmail',
+  };
+  override readonly internedData2: ExpectedInternedData = {
+    packetIndex: 6,
+    iid: 2,
+    str: 'null',
+  };
+  override readonly messagePacketNoInternedStrings: ExpectedMessagePacket = {
+    packetIndex: 2,
+    sequenceFlags: perfetto.protos.TracePacket.SequenceFlags.SEQ_UNSPECIFIED,
+    timestamp: Long.fromNumber(1315553529939),
+    messageId: Long.fromString('1665699123574159131'),
+    strParamIids: [],
+    sint64Params: [],
+    booleanParams: [0],
+    doubleParams: [],
+  };
+  override readonly messagePacketWithInternedStrings: ExpectedMessagePacket = {
+    packetIndex: 9,
+    sequenceFlags:
+      perfetto.protos.TracePacket.SequenceFlags.SEQ_NEEDS_INCREMENTAL_STATE,
+    timestamp: Long.fromNumber(1315574594310),
+    messageId: Long.fromString('-6873410057142191118'),
+    strParamIids: [1, 2, 3, 4],
+    sint64Params: [],
+    booleanParams: [],
+    doubleParams: [],
+  };
+  override readonly expectedFirstMessage: ExpectedMessage = {
+    message: 'Starting activity when config will change = false',
+    ts: '2024-02-29, 08:53:26.400',
+    tag: 'WindowManager',
+    level: 'VERBOSE',
+    at: 'com/android/server/wm/ActivityStarter.java',
+  };
+}
+
+class ParserProtologMissingConfigTest extends ParserProtologTest {
+  override readonly traceFile =
+    'traces/elapsed_and_real_timestamp/ProtoLogMissingConfigMessage.pb';
+  override readonly timestampCount = 7295;
+  override readonly first3ExpectedRealTimestamps = [
+    TimestampConverterUtils.makeRealTimestamp(1669053909777144978n),
+    TimestampConverterUtils.makeRealTimestamp(1669053909778011697n),
+    TimestampConverterUtils.makeRealTimestamp(1669053909778800707n),
+  ];
+  override readonly expectedConfig = CONFIG_32;
+  override readonly internedData1: ExpectedInternedData = {
+    packetIndex: 2,
+    iid: 1,
+    str: 'NotificationShade',
+  };
+  override readonly internedData2: ExpectedInternedData = {
+    packetIndex: 4,
+    iid: 2,
+    str: 'Window{f199162 u0 NotificationShade}',
+  };
+  override readonly messagePacketNoInternedStrings: ExpectedMessagePacket = {
+    packetIndex: 92,
+    sequenceFlags: perfetto.protos.TracePacket.SequenceFlags.SEQ_UNSPECIFIED,
+    timestamp: Long.fromNumber(24398203599667),
+    messageId: Long.fromString('1381227466'),
+    strParamIids: [],
+    sint64Params: [Long.fromNumber(2), Long.fromNumber(0)],
+    booleanParams: [],
+    doubleParams: [],
+  };
+  override readonly messagePacketWithInternedStrings: ExpectedMessagePacket = {
+    packetIndex: 3,
+    sequenceFlags:
+      perfetto.protos.TracePacket.SequenceFlags.SEQ_NEEDS_INCREMENTAL_STATE,
+    timestamp: Long.fromNumber(24398190144978),
+    messageId: Long.fromNumber(585096182),
+    strParamIids: [1],
+    sint64Params: [],
+    booleanParams: [1],
+    doubleParams: [],
+  };
+  override readonly expectedFirstMessage: ExpectedMessage = {
+    message: 'SURFACE isColorSpaceAgnostic=true: NotificationShade',
+    ts: '2022-11-21, 18:05:09.777',
+    tag: 'WindowManager',
+    level: 'INFO',
+    at: 'com/android/server/wm/WindowSurfaceController.java',
+  };
+}
+
+describe('32', () => {
+  new ParserProtolog32Test().execute();
+});
+
+describe('64', () => {
+  new ParserProtolog64Test().execute();
+});
+
+describe('Missing config', () => {
+  new ParserProtologMissingConfigTest().execute();
 });
diff --git a/tools/winscope/src/parsers/protolog/parser_protolog_utils.ts b/tools/winscope/src/parsers/protolog/parser_protolog_utils.ts
index d16b35f..8b55a10 100644
--- a/tools/winscope/src/parsers/protolog/parser_protolog_utils.ts
+++ b/tools/winscope/src/parsers/protolog/parser_protolog_utils.ts
@@ -27,7 +27,6 @@
   static makeMessagePropertiesTree(
     logMessage: LogMessage,
     timestampConverter: ParserTimestampConverter,
-    isMonotonic: boolean,
   ): PropertyTreeNode {
     const tree = new PropertyTreeBuilderFromProto()
       .setData(logMessage)
@@ -39,9 +38,6 @@
     const customFormatters = new Map([['timestamp', TIMESTAMP_NODE_FORMATTER]]);
 
     const strategy: MakeTimestampStrategyType = (valueNs: bigint) => {
-      if (isMonotonic) {
-        return timestampConverter.makeTimestampFromMonotonicNs(valueNs);
-      }
       return timestampConverter.makeTimestampFromBootTimeNs(valueNs);
     };
 
diff --git a/tools/winscope/src/parsers/protolog/perfetto/parser_protolog.ts b/tools/winscope/src/parsers/protolog/perfetto/parser_protolog.ts
index 2d07355..a0e5235 100644
--- a/tools/winscope/src/parsers/protolog/perfetto/parser_protolog.ts
+++ b/tools/winscope/src/parsers/protolog/perfetto/parser_protolog.ts
@@ -60,7 +60,6 @@
     return ParserProtologUtils.makeMessagePropertiesTree(
       logMessage,
       this.timestampConverter,
-      false,
     );
   }
 
diff --git a/tools/winscope/src/trace/parser.ts b/tools/winscope/src/trace/parser.ts
index 03f90f5..7e87eae 100644
--- a/tools/winscope/src/trace/parser.ts
+++ b/tools/winscope/src/trace/parser.ts
@@ -40,5 +40,9 @@
   getRealToMonotonicTimeOffsetNs(): bigint | undefined;
   getRealToBootTimeOffsetNs(): bigint | undefined;
   createTimestamps(): void;
-  convertToPerfettoPackets?(sequenceId: number): perfetto.protos.TracePacket[];
+  convertToPerfettoPackets?(
+    sequenceId: number,
+    trustedUid?: number,
+    trustedPid?: number,
+  ): perfetto.protos.TracePacket[];
 }