IME refactor - parsing side.

- Moved 3 parsers and associated tests in parsers/input_method
- HierarchyTreeBuilderInputMethod used in all three ime parsers
- Added option in PropertyTreeBuilderFromProto/PropertyTreeNodeFactory to not visit object prototype, as ime protos have defaults in prototype
- Made 'children' property optional in HierarchyTreeBuilder, so we don't have to create a children property for every WM node in ParserWindowManagerUtils

Bug: b/311643292
Test: npm run test:unit:ci
Change-Id: Ie50eb45b3760854d59b9f5c6a22c925284c8984e
diff --git a/tools/winscope/src/parsers/hierarchy_tree_builder.ts b/tools/winscope/src/parsers/hierarchy_tree_builder.ts
index d0fc093..ac24320 100644
--- a/tools/winscope/src/parsers/hierarchy_tree_builder.ts
+++ b/tools/winscope/src/parsers/hierarchy_tree_builder.ts
@@ -71,16 +71,16 @@
     identifierToChild: Map<string | number, PropertiesProvider[]>
   ): HierarchyTreeNode {
     const childProperties = child.getEagerProperties();
-    const subChildren: HierarchyTreeNode[] = assertDefined(
-      childProperties.getChildByName('children')
-    )
-      .getAllChildren()
-      .flatMap((identifier: PropertyTreeNode) => {
-        const key = this.getIdentifierValue(identifier);
-        return assertDefined(identifierToChild.get(key)).map((child) => {
-          return this.buildSubtree(child, identifierToChild);
-        });
-      });
+    const subChildren: HierarchyTreeNode[] =
+      childProperties
+        .getChildByName('children')
+        ?.getAllChildren()
+        .flatMap((identifier: PropertyTreeNode) => {
+          const key = this.getIdentifierValue(identifier);
+          return assertDefined(identifierToChild.get(key)).map((child) => {
+            return this.buildSubtree(child, identifierToChild);
+          });
+        }) ?? [];
 
     return this.buildHierarchyTreeNode(
       childProperties.id,
diff --git a/tools/winscope/src/parsers/input_method/hierarchy_tree_builder_input_method.ts b/tools/winscope/src/parsers/input_method/hierarchy_tree_builder_input_method.ts
new file mode 100644
index 0000000..a4a0c03
--- /dev/null
+++ b/tools/winscope/src/parsers/input_method/hierarchy_tree_builder_input_method.ts
@@ -0,0 +1,45 @@
+/*
+ * 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 {HierarchyTreeBuilder} from 'parsers/hierarchy_tree_builder';
+import {HierarchyTreeNode} from 'trace/tree_node/hierarchy_tree_node';
+import {PropertiesProvider} from 'trace/tree_node/properties_provider';
+import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
+
+export class HierarchyTreeBuilderInputMethod extends HierarchyTreeBuilder {
+  protected override buildIdentifierToChildMap(
+    children: PropertiesProvider[]
+  ): Map<string | number, PropertiesProvider[]> {
+    // only ever one child (client, service, manager service) so map unnecessary
+    return new Map<string | number, PropertiesProvider[]>();
+  }
+
+  protected override makeRootChildren(
+    children: PropertiesProvider[],
+    identifierToChild: Map<string | number, PropertiesProvider[]>
+  ): ReadonlyArray<HierarchyTreeNode> {
+    if (children.length === 0) return [];
+    return [this.buildSubtree(children[0], identifierToChild)];
+  }
+
+  protected override getIdentifierValue(identifier: PropertyTreeNode): number {
+    return identifier.getValue();
+  }
+
+  protected override getSubtreeName(propertyTreeName: string): string {
+    return propertyTreeName;
+  }
+}
diff --git a/tools/winscope/src/parsers/input_method/hierarchy_tree_builder_input_method_test.ts b/tools/winscope/src/parsers/input_method/hierarchy_tree_builder_input_method_test.ts
new file mode 100644
index 0000000..d7c0228
--- /dev/null
+++ b/tools/winscope/src/parsers/input_method/hierarchy_tree_builder_input_method_test.ts
@@ -0,0 +1,107 @@
+/*
+ * 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 {PropertyTreeBuilder} from 'test/unit/property_tree_builder';
+import {TreeNodeUtils} from 'test/unit/tree_node_utils';
+import {HierarchyTreeNode} from 'trace/tree_node/hierarchy_tree_node';
+import {OperationChain} from 'trace/tree_node/operations/operation_chain';
+import {PropertiesProvider} from 'trace/tree_node/properties_provider';
+import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
+import {HierarchyTreeBuilderInputMethod} from './hierarchy_tree_builder_input_method';
+
+describe('HierarchyTreeBuilderInputMethod', () => {
+  let builder: HierarchyTreeBuilderInputMethod;
+  let entry: PropertiesProvider;
+  let entryPropertiesTree: PropertyTreeNode;
+
+  beforeEach(() => {
+    jasmine.addCustomEqualityTester(TreeNodeUtils.treeNodeEqualityTester);
+    builder = new HierarchyTreeBuilderInputMethod();
+    entryPropertiesTree = new PropertyTreeBuilder()
+      .setIsRoot(true)
+      .setRootId('InputMethod')
+      .setName('entry')
+      .build();
+    entry = new PropertiesProvider(
+      entryPropertiesTree,
+      async () => entryPropertiesTree,
+      OperationChain.emptyChain<PropertyTreeNode>(),
+      OperationChain.emptyChain<PropertyTreeNode>(),
+      OperationChain.emptyChain<PropertyTreeNode>()
+    );
+  });
+
+  it('throws error if entry not set', () => {
+    const noEntryError = new Error('root not set');
+    expect(() => builder.setChildren([]).build()).toThrow(noEntryError);
+  });
+
+  it('throws error if children not set', () => {
+    const noChildrenError = new Error('children not set');
+    expect(() => builder.setRoot(entry).build()).toThrow(noChildrenError);
+  });
+
+  it('builds root with no children correctly', () => {
+    const root = builder.setRoot(entry).setChildren([]).build();
+
+    const expectedRoot = new HierarchyTreeNode(
+      'InputMethod entry',
+      'entry',
+      new PropertiesProvider(
+        entryPropertiesTree,
+        async () => entryPropertiesTree,
+        OperationChain.emptyChain<PropertyTreeNode>(),
+        OperationChain.emptyChain<PropertyTreeNode>(),
+        OperationChain.emptyChain<PropertyTreeNode>()
+      )
+    );
+
+    expect(root).toEqual(expectedRoot);
+  });
+
+  it('builds root with children correctly', () => {
+    const childProps = new PropertyTreeBuilder()
+      .setRootId('Service')
+      .setName('child')
+      .setIsRoot(true)
+      .build();
+
+    const childProvider = new PropertiesProvider(
+      childProps,
+      async () => childProps,
+      OperationChain.emptyChain<PropertyTreeNode>(),
+      OperationChain.emptyChain<PropertyTreeNode>(),
+      OperationChain.emptyChain<PropertyTreeNode>()
+    );
+
+    const root = builder.setRoot(entry).setChildren([childProvider]).build();
+
+    const expectedRoot = new HierarchyTreeNode(
+      'InputMethod entry',
+      'entry',
+      new PropertiesProvider(
+        entryPropertiesTree,
+        async () => entryPropertiesTree,
+        OperationChain.emptyChain<PropertyTreeNode>(),
+        OperationChain.emptyChain<PropertyTreeNode>(),
+        OperationChain.emptyChain<PropertyTreeNode>()
+      )
+    );
+    expectedRoot.addOrReplaceChild(new HierarchyTreeNode('Service child', 'child', childProvider));
+
+    expect(root).toEqual(expectedRoot);
+  });
+});
diff --git a/tools/winscope/src/parsers/input_method/parser_input_method_clients.ts b/tools/winscope/src/parsers/input_method/parser_input_method_clients.ts
new file mode 100644
index 0000000..46697c9
--- /dev/null
+++ b/tools/winscope/src/parsers/input_method/parser_input_method_clients.ts
@@ -0,0 +1,271 @@
+/*
+ * 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 {assertDefined} from 'common/assert_utils';
+import {Timestamp, TimestampType} from 'common/time';
+import {TimeUtils} from 'common/time_utils';
+import {AbstractParser} from 'parsers/abstract_parser';
+import {AddDefaults} from 'parsers/operations/add_defaults';
+import {SetFormatters} from 'parsers/operations/set_formatters';
+import {TranslateIntDef} from 'parsers/operations/translate_intdef';
+import {TamperedMessageType} from 'parsers/tampered_message_type';
+import root from 'protos/ime/latest/json';
+import {android} from 'protos/ime/latest/static';
+import {TraceFile} from 'trace/trace_file';
+import {TraceTreeNode} from 'trace/trace_tree_node';
+import {TraceType} from 'trace/trace_type';
+import {HierarchyTreeNode} from 'trace/tree_node/hierarchy_tree_node';
+import {LazyPropertiesStrategyType} from 'trace/tree_node/properties_provider';
+import {PropertiesProviderBuilder} from 'trace/tree_node/properties_provider_builder';
+import {PropertyTreeBuilderFromProto} from 'trace/tree_node/property_tree_builder_from_proto';
+import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
+import {ImeUtils} from 'viewers/common/ime_utils';
+import {HierarchyTreeBuilderInputMethod} from './hierarchy_tree_builder_input_method';
+
+class ParserInputMethodClients extends AbstractParser {
+  private static readonly MAGIC_NUMBER = [0x09, 0x49, 0x4d, 0x43, 0x54, 0x52, 0x41, 0x43, 0x45]; // .IMCTRACE
+
+  private static readonly ENTRY_DENYLIST_PROPERTIES = ['client'];
+  private static readonly ENTRY_EAGER_PROPERTIES = ['where'];
+  private static readonly CLIENT_EAGER_PROPERTIES = [
+    'viewRootImpl',
+    'inputMethodManager',
+    'editorInfo',
+  ];
+
+  private static readonly InputMethodClientsTraceFileProto = TamperedMessageType.tamper(
+    root.lookupType('android.view.inputmethod.InputMethodClientsTraceFileProto')
+  );
+  private static readonly entryField =
+    ParserInputMethodClients.InputMethodClientsTraceFileProto.fields['entry'];
+  private static readonly clientField = assertDefined(
+    ParserInputMethodClients.entryField.tamperedMessageType
+  ).fields['client'];
+
+  private static readonly Operations = {
+    SetFormattersClient: new SetFormatters(ParserInputMethodClients.clientField),
+    TranslateIntDefClient: new TranslateIntDef(ParserInputMethodClients.clientField),
+    AddDefaultsClientEager: new AddDefaults(
+      ParserInputMethodClients.clientField,
+      ParserInputMethodClients.CLIENT_EAGER_PROPERTIES
+    ),
+    AddDefaultsClientLazy: new AddDefaults(
+      ParserInputMethodClients.clientField,
+      undefined,
+      ParserInputMethodClients.CLIENT_EAGER_PROPERTIES
+    ),
+    SetFormattersEntry: new SetFormatters(ParserInputMethodClients.entryField),
+    AddDefaultsEntryEager: new AddDefaults(
+      ParserInputMethodClients.entryField,
+      ParserInputMethodClients.ENTRY_EAGER_PROPERTIES
+    ),
+    AddDefaultsEntryLazy: new AddDefaults(
+      ParserInputMethodClients.entryField,
+      undefined,
+      ParserInputMethodClients.ENTRY_EAGER_PROPERTIES.concat(
+        ParserInputMethodClients.ENTRY_DENYLIST_PROPERTIES
+      )
+    ),
+  };
+
+  private realToElapsedTimeOffsetNs: undefined | bigint;
+
+  constructor(trace: TraceFile) {
+    super(trace);
+    this.realToElapsedTimeOffsetNs = undefined;
+  }
+
+  getTraceType(): TraceType {
+    return TraceType.INPUT_METHOD_CLIENTS;
+  }
+
+  override getMagicNumber(): number[] {
+    return ParserInputMethodClients.MAGIC_NUMBER;
+  }
+
+  override decodeTrace(
+    buffer: Uint8Array
+  ): android.view.inputmethod.IInputMethodClientsTraceProto[] {
+    const decoded = ParserInputMethodClients.InputMethodClientsTraceFileProto.decode(
+      buffer
+    ) as android.view.inputmethod.IInputMethodClientsTraceFileProto;
+    const timeOffset = BigInt(decoded.realToElapsedTimeOffsetNanos?.toString() ?? '0');
+    this.realToElapsedTimeOffsetNs = timeOffset !== 0n ? timeOffset : undefined;
+    return decoded.entry ?? [];
+  }
+
+  override getTimestamp(
+    type: TimestampType,
+    entry: android.view.inputmethod.IInputMethodClientsTraceProto
+  ): undefined | Timestamp {
+    const elapsedRealtimeNanos = BigInt(assertDefined(entry.elapsedRealtimeNanos).toString());
+    if (type === TimestampType.ELAPSED) {
+      return new Timestamp(type, elapsedRealtimeNanos);
+    } else if (type === TimestampType.REAL && this.realToElapsedTimeOffsetNs !== undefined) {
+      return new Timestamp(type, elapsedRealtimeNanos + this.realToElapsedTimeOffsetNs);
+    }
+    return undefined;
+  }
+
+  override processDecodedEntry(
+    index: number,
+    timestampType: TimestampType,
+    entry: android.view.inputmethod.IInputMethodClientsTraceProto
+  ): TraceTreeNode {
+    if (entry.elapsedRealtimeNanos === undefined || entry.elapsedRealtimeNanos === null) {
+      throw Error('Missing elapsedRealtimeNanos on entry');
+    }
+
+    const elapsedRealtimeNanos = BigInt(entry.elapsedRealtimeNanos.toString());
+
+    let clockTimeNanos: bigint | undefined = undefined;
+    if (this.realToElapsedTimeOffsetNs !== undefined) {
+      clockTimeNanos = elapsedRealtimeNanos + this.realToElapsedTimeOffsetNs;
+    }
+
+    const timestamp = Timestamp.from(
+      timestampType,
+      elapsedRealtimeNanos,
+      this.realToElapsedTimeOffsetNs
+    );
+
+    return {
+      name: TimeUtils.format(timestamp) + ' - ' + entry.where,
+      kind: 'InputMethodClient entry',
+      children: [
+        {
+          obj: ImeUtils.transformInputConnectionCall(entry.client),
+          kind: 'Client',
+          name: entry.client?.viewRootImpl?.view ?? '',
+          children: [],
+          stableId: 'client',
+          id: 'client',
+        },
+      ],
+      obj: entry,
+      stableId: 'entry',
+      id: 'entry',
+      elapsedRealtimeNanos,
+      clockTimeNanos,
+    };
+  }
+
+  private makeHierarchyTree(
+    entryProto: android.view.inputmethod.IInputMethodClientsTraceProto
+  ): HierarchyTreeNode {
+    const entry = new PropertiesProviderBuilder()
+      .setEagerProperties(this.makeEntryEagerPropertiesTree(entryProto))
+      .setLazyPropertiesStrategy(this.makeEntryLazyPropertiesStrategy(entryProto))
+      .setEagerOperations([ParserInputMethodClients.Operations.AddDefaultsEntryEager])
+      .setCommonOperations([ParserInputMethodClients.Operations.SetFormattersEntry])
+      .setLazyOperations([ParserInputMethodClients.Operations.AddDefaultsEntryLazy])
+      .build();
+
+    const client = new PropertiesProviderBuilder()
+      .setEagerProperties(this.makeClientEagerPropertiesTree(entryProto.client))
+      .setLazyPropertiesStrategy(this.makeClientLazyPropertiesStrategy(entryProto.client))
+      .setEagerOperations(
+        entryProto.client ? [ParserInputMethodClients.Operations.AddDefaultsClientEager] : []
+      )
+      .setCommonOperations([
+        ParserInputMethodClients.Operations.SetFormattersClient,
+        ParserInputMethodClients.Operations.TranslateIntDefClient,
+      ])
+      .setLazyOperations(
+        entryProto.client ? [ParserInputMethodClients.Operations.AddDefaultsClientLazy] : []
+      )
+      .build();
+
+    return new HierarchyTreeBuilderInputMethod().setRoot(entry).setChildren([client]).build();
+  }
+
+  private makeEntryEagerPropertiesTree(
+    entryProto: android.view.inputmethod.IInputMethodClientsTraceProto
+  ): PropertyTreeNode {
+    const denyList: string[] = [];
+    Object.getOwnPropertyNames(entryProto).forEach((it) => {
+      if (!ParserInputMethodClients.ENTRY_EAGER_PROPERTIES.includes(it)) denyList.push(it);
+    });
+
+    return new PropertyTreeBuilderFromProto()
+      .setData(entryProto)
+      .setRootId('InputMethodClients')
+      .setRootName('entry')
+      .setDenyList(denyList)
+      .build();
+  }
+
+  private makeEntryLazyPropertiesStrategy(
+    entryProto: android.view.inputmethod.IInputMethodClientsTraceProto
+  ): LazyPropertiesStrategyType {
+    return async () => {
+      return new PropertyTreeBuilderFromProto()
+        .setData(entryProto)
+        .setRootId('InputMethodClients')
+        .setRootName('entry')
+        .setDenyList(
+          ParserInputMethodClients.ENTRY_EAGER_PROPERTIES.concat(
+            ParserInputMethodClients.ENTRY_DENYLIST_PROPERTIES
+          )
+        )
+        .build();
+    };
+  }
+
+  private makeClientEagerPropertiesTree(
+    clientProto:
+      | android.view.inputmethod.InputMethodClientsTraceProto.IClientSideProto
+      | null
+      | undefined
+  ): PropertyTreeNode {
+    const denyList: string[] = [];
+    let data: any = clientProto;
+    if (clientProto) {
+      Object.getOwnPropertyNames(clientProto).forEach((it) => {
+        if (!ParserInputMethodClients.CLIENT_EAGER_PROPERTIES.includes(it)) denyList.push(it);
+      });
+    } else {
+      data = {client: null};
+    }
+
+    return new PropertyTreeBuilderFromProto()
+      .setData(data)
+      .setRootId('InputMethodClients')
+      .setRootName('client')
+      .setDenyList(denyList)
+      .setVisitPrototype(false)
+      .build();
+  }
+
+  private makeClientLazyPropertiesStrategy(
+    clientProto:
+      | android.view.inputmethod.InputMethodClientsTraceProto.IClientSideProto
+      | null
+      | undefined
+  ): LazyPropertiesStrategyType {
+    return async () => {
+      return new PropertyTreeBuilderFromProto()
+        .setData(clientProto ?? {client: null})
+        .setRootId('InputMethodClients')
+        .setRootName('client')
+        .setDenyList(ParserInputMethodClients.CLIENT_EAGER_PROPERTIES)
+        .setVisitPrototype(false)
+        .build();
+    };
+  }
+}
+
+export {ParserInputMethodClients};
diff --git a/tools/winscope/src/parsers/parser_input_method_clients_test.ts b/tools/winscope/src/parsers/input_method/parser_input_method_clients_test.ts
similarity index 100%
rename from tools/winscope/src/parsers/parser_input_method_clients_test.ts
rename to tools/winscope/src/parsers/input_method/parser_input_method_clients_test.ts
diff --git a/tools/winscope/src/parsers/input_method/parser_input_method_manager_service.ts b/tools/winscope/src/parsers/input_method/parser_input_method_manager_service.ts
new file mode 100644
index 0000000..d4a1129
--- /dev/null
+++ b/tools/winscope/src/parsers/input_method/parser_input_method_manager_service.ts
@@ -0,0 +1,272 @@
+/*
+ * 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 {assertDefined} from 'common/assert_utils';
+import {Timestamp, TimestampType} from 'common/time';
+import {TimeUtils} from 'common/time_utils';
+import {AbstractParser} from 'parsers/abstract_parser';
+import {AddDefaults} from 'parsers/operations/add_defaults';
+import {SetFormatters} from 'parsers/operations/set_formatters';
+import {TranslateIntDef} from 'parsers/operations/translate_intdef';
+import {TamperedMessageType} from 'parsers/tampered_message_type';
+import root from 'protos/ime/latest/json';
+import {android} from 'protos/ime/latest/static';
+import {TraceFile} from 'trace/trace_file';
+import {TraceTreeNode} from 'trace/trace_tree_node';
+import {TraceType} from 'trace/trace_type';
+import {HierarchyTreeNode} from 'trace/tree_node/hierarchy_tree_node';
+import {LazyPropertiesStrategyType} from 'trace/tree_node/properties_provider';
+import {PropertiesProviderBuilder} from 'trace/tree_node/properties_provider_builder';
+import {PropertyTreeBuilderFromProto} from 'trace/tree_node/property_tree_builder_from_proto';
+import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
+import {HierarchyTreeBuilderInputMethod} from './hierarchy_tree_builder_input_method';
+
+class ParserInputMethodManagerService extends AbstractParser {
+  private static readonly MAGIC_NUMBER = [0x09, 0x49, 0x4d, 0x4d, 0x54, 0x52, 0x41, 0x43, 0x45]; // .IMMTRACE
+
+  private static readonly ENTRY_DENYLIST_PROPERTIES = ['inputMethodManagerService'];
+  private static readonly ENTRY_EAGER_PROPERTIES = ['where'];
+  private static readonly SERVICE_EAGER_PROPERTIES = [
+    'curMethodId',
+    'curFocusedWindowName',
+    'lastImeTargetWindowName',
+    'inputShown',
+  ];
+
+  private static readonly InputMethodManagerServiceTraceFileProto = TamperedMessageType.tamper(
+    root.lookupType('android.view.inputmethod.InputMethodManagerServiceTraceFileProto')
+  );
+  private static readonly entryField =
+    ParserInputMethodManagerService.InputMethodManagerServiceTraceFileProto.fields['entry'];
+  private static readonly serviceField = assertDefined(
+    ParserInputMethodManagerService.entryField.tamperedMessageType
+  ).fields['inputMethodManagerService'];
+
+  private static readonly Operations = {
+    SetFormattersService: new SetFormatters(ParserInputMethodManagerService.serviceField),
+    TranslateIntDefService: new TranslateIntDef(ParserInputMethodManagerService.serviceField),
+    AddDefaultsServiceEager: new AddDefaults(
+      ParserInputMethodManagerService.serviceField,
+      ParserInputMethodManagerService.SERVICE_EAGER_PROPERTIES
+    ),
+    AddDefaultsServiceLazy: new AddDefaults(
+      ParserInputMethodManagerService.serviceField,
+      undefined,
+      ParserInputMethodManagerService.SERVICE_EAGER_PROPERTIES
+    ),
+    SetFormattersEntry: new SetFormatters(ParserInputMethodManagerService.entryField),
+    AddDefaultsEntryEager: new AddDefaults(
+      ParserInputMethodManagerService.entryField,
+      ParserInputMethodManagerService.ENTRY_EAGER_PROPERTIES
+    ),
+    AddDefaultsEntryLazy: new AddDefaults(
+      ParserInputMethodManagerService.entryField,
+      undefined,
+      ParserInputMethodManagerService.ENTRY_EAGER_PROPERTIES.concat(
+        ParserInputMethodManagerService.ENTRY_DENYLIST_PROPERTIES
+      )
+    ),
+  };
+
+  private realToElapsedTimeOffsetNs: undefined | bigint;
+
+  constructor(trace: TraceFile) {
+    super(trace);
+    this.realToElapsedTimeOffsetNs = undefined;
+  }
+
+  getTraceType(): TraceType {
+    return TraceType.INPUT_METHOD_MANAGER_SERVICE;
+  }
+
+  override getMagicNumber(): number[] {
+    return ParserInputMethodManagerService.MAGIC_NUMBER;
+  }
+
+  override decodeTrace(
+    buffer: Uint8Array
+  ): android.view.inputmethod.IInputMethodManagerServiceTraceProto[] {
+    const decoded = ParserInputMethodManagerService.InputMethodManagerServiceTraceFileProto.decode(
+      buffer
+    ) as android.view.inputmethod.IInputMethodManagerServiceTraceFileProto;
+    const timeOffset = BigInt(decoded.realToElapsedTimeOffsetNanos?.toString() ?? '0');
+    this.realToElapsedTimeOffsetNs = timeOffset !== 0n ? timeOffset : undefined;
+    return decoded.entry ?? [];
+  }
+
+  protected override getTimestamp(
+    type: TimestampType,
+    entry: android.view.inputmethod.IInputMethodManagerServiceTraceProto
+  ): undefined | Timestamp {
+    const elapsedRealtimeNanos = BigInt(assertDefined(entry.elapsedRealtimeNanos).toString());
+    if (type === TimestampType.ELAPSED) {
+      return new Timestamp(TimestampType.ELAPSED, elapsedRealtimeNanos);
+    } else if (type === TimestampType.REAL && this.realToElapsedTimeOffsetNs !== undefined) {
+      return new Timestamp(type, this.realToElapsedTimeOffsetNs + elapsedRealtimeNanos);
+    }
+    return undefined;
+  }
+
+  protected override processDecodedEntry(
+    index: number,
+    timestampType: TimestampType,
+    entry: android.view.inputmethod.IInputMethodManagerServiceTraceProto
+  ): TraceTreeNode {
+    if (entry.elapsedRealtimeNanos === undefined || entry.elapsedRealtimeNanos === null) {
+      throw Error('Missing elapsedRealtimeNanos on entry');
+    }
+
+    const elapsedRealtimeNanos = BigInt(entry.elapsedRealtimeNanos.toString());
+
+    let clockTimeNanos: bigint | undefined = undefined;
+    if (this.realToElapsedTimeOffsetNs !== undefined && entry.elapsedRealtimeNanos !== undefined) {
+      clockTimeNanos = elapsedRealtimeNanos + this.realToElapsedTimeOffsetNs;
+    }
+
+    const timestamp = Timestamp.from(
+      timestampType,
+      elapsedRealtimeNanos,
+      this.realToElapsedTimeOffsetNs
+    );
+
+    return {
+      name: TimeUtils.format(timestamp) + ' - ' + entry.where,
+      kind: 'InputMethodManagerService entry',
+      children: [
+        {
+          obj: entry.inputMethodManagerService,
+          kind: 'InputMethodManagerService',
+          name: '',
+          children: [],
+          stableId: 'managerservice',
+          id: 'managerservice',
+        },
+      ],
+      obj: entry,
+      stableId: 'entry',
+      id: 'entry',
+      elapsedRealtimeNanos,
+      clockTimeNanos,
+    };
+  }
+
+  private makeHierarchyTree(
+    entryProto: android.view.inputmethod.IInputMethodManagerServiceTraceProto
+  ): HierarchyTreeNode {
+    const entry = new PropertiesProviderBuilder()
+      .setEagerProperties(this.makeEntryEagerPropertiesTree(entryProto))
+      .setLazyPropertiesStrategy(this.makeEntryLazyPropertiesStrategy(entryProto))
+      .setEagerOperations([ParserInputMethodManagerService.Operations.AddDefaultsEntryEager])
+      .setCommonOperations([ParserInputMethodManagerService.Operations.SetFormattersEntry])
+      .setLazyOperations([ParserInputMethodManagerService.Operations.AddDefaultsEntryLazy])
+      .build();
+
+    const inputMethodManagerService = entryProto.inputMethodManagerService
+      ? new PropertiesProviderBuilder()
+          .setEagerProperties(
+            this.makeServiceEagerPropertiesTree(entryProto.inputMethodManagerService)
+          )
+          .setLazyPropertiesStrategy(
+            this.makeServiceLazyPropertiesStrategy(entryProto.inputMethodManagerService)
+          )
+          .setEagerOperations([ParserInputMethodManagerService.Operations.AddDefaultsServiceEager])
+          .setCommonOperations([
+            ParserInputMethodManagerService.Operations.SetFormattersService,
+            ParserInputMethodManagerService.Operations.TranslateIntDefService,
+          ])
+          .setLazyOperations([ParserInputMethodManagerService.Operations.AddDefaultsServiceLazy])
+          .build()
+      : undefined;
+
+    return new HierarchyTreeBuilderInputMethod()
+      .setRoot(entry)
+      .setChildren(inputMethodManagerService ? [inputMethodManagerService] : [])
+      .build();
+  }
+
+  private makeEntryEagerPropertiesTree(
+    entryProto: android.view.inputmethod.IInputMethodManagerServiceTraceProto
+  ): PropertyTreeNode {
+    const denyList: string[] = [];
+    Object.getOwnPropertyNames(entryProto).forEach((it) => {
+      if (!ParserInputMethodManagerService.ENTRY_EAGER_PROPERTIES.includes(it)) denyList.push(it);
+    });
+
+    return new PropertyTreeBuilderFromProto()
+      .setData(entryProto)
+      .setRootId('InputMethodManagerService')
+      .setRootName('entry')
+      .setDenyList(denyList)
+      .build();
+  }
+
+  private makeEntryLazyPropertiesStrategy(
+    entryProto: android.view.inputmethod.IInputMethodManagerServiceTraceProto
+  ): LazyPropertiesStrategyType {
+    return async () => {
+      return new PropertyTreeBuilderFromProto()
+        .setData(entryProto)
+        .setRootId('InputMethodManagerService')
+        .setRootName('entry')
+        .setDenyList(
+          ParserInputMethodManagerService.ENTRY_EAGER_PROPERTIES.concat(
+            ParserInputMethodManagerService.ENTRY_DENYLIST_PROPERTIES
+          )
+        )
+        .build();
+    };
+  }
+
+  private makeServiceEagerPropertiesTree(
+    serviceProto: android.server.inputmethod.IInputMethodManagerServiceProto
+  ): PropertyTreeNode {
+    const denyList: string[] = [];
+    let data: any = serviceProto;
+    if (serviceProto) {
+      Object.getOwnPropertyNames(serviceProto).forEach((it) => {
+        if (!ParserInputMethodManagerService.SERVICE_EAGER_PROPERTIES.includes(it)) {
+          denyList.push(it);
+        }
+      });
+    } else {
+      data = {inputMethodManagerService: null};
+    }
+
+    return new PropertyTreeBuilderFromProto()
+      .setData(data)
+      .setRootId('InputMethodManagerService')
+      .setRootName('inputMethodManagerService')
+      .setDenyList(denyList)
+      .setVisitPrototype(false)
+      .build();
+  }
+
+  private makeServiceLazyPropertiesStrategy(
+    serviceProto: android.server.inputmethod.IInputMethodManagerServiceProto
+  ): LazyPropertiesStrategyType {
+    return async () => {
+      return new PropertyTreeBuilderFromProto()
+        .setData(serviceProto ?? {inputMethodManagerService: null})
+        .setRootId('InputMethodManagerService')
+        .setRootName('inputMethodManagerService')
+        .setDenyList(ParserInputMethodManagerService.SERVICE_EAGER_PROPERTIES)
+        .setVisitPrototype(false)
+        .build();
+    };
+  }
+}
+
+export {ParserInputMethodManagerService};
diff --git a/tools/winscope/src/parsers/parser_input_method_manager_service_test.ts b/tools/winscope/src/parsers/input_method/parser_input_method_manager_service_test.ts
similarity index 100%
rename from tools/winscope/src/parsers/parser_input_method_manager_service_test.ts
rename to tools/winscope/src/parsers/input_method/parser_input_method_manager_service_test.ts
diff --git a/tools/winscope/src/parsers/input_method/parser_input_method_service.ts b/tools/winscope/src/parsers/input_method/parser_input_method_service.ts
new file mode 100644
index 0000000..a1e0519
--- /dev/null
+++ b/tools/winscope/src/parsers/input_method/parser_input_method_service.ts
@@ -0,0 +1,267 @@
+/*
+ * 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 {assertDefined} from 'common/assert_utils';
+import {Timestamp, TimestampType} from 'common/time';
+import {TimeUtils} from 'common/time_utils';
+import {AbstractParser} from 'parsers/abstract_parser';
+import {AddDefaults} from 'parsers/operations/add_defaults';
+import {SetFormatters} from 'parsers/operations/set_formatters';
+import {TranslateIntDef} from 'parsers/operations/translate_intdef';
+import {TamperedMessageType} from 'parsers/tampered_message_type';
+import root from 'protos/ime/latest/json';
+import {android} from 'protos/ime/latest/static';
+import {TraceFile} from 'trace/trace_file';
+import {TraceTreeNode} from 'trace/trace_tree_node';
+import {TraceType} from 'trace/trace_type';
+import {HierarchyTreeNode} from 'trace/tree_node/hierarchy_tree_node';
+import {LazyPropertiesStrategyType} from 'trace/tree_node/properties_provider';
+import {PropertiesProviderBuilder} from 'trace/tree_node/properties_provider_builder';
+import {PropertyTreeBuilderFromProto} from 'trace/tree_node/property_tree_builder_from_proto';
+import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
+import {ImeUtils} from 'viewers/common/ime_utils';
+import {HierarchyTreeBuilderInputMethod} from './hierarchy_tree_builder_input_method';
+
+class ParserInputMethodService extends AbstractParser {
+  private static readonly MAGIC_NUMBER = [0x09, 0x49, 0x4d, 0x53, 0x54, 0x52, 0x41, 0x43, 0x45]; // .IMSTRACE
+
+  private static readonly ENTRY_DENYLIST_PROPERTIES = ['inputMethodService'];
+  private static readonly ENTRY_EAGER_PROPERTIES = ['where'];
+  private static readonly SERVICE_EAGER_PROPERTIES = [
+    'windowVisible',
+    'decorViewVisible',
+    'inputEditorInfo',
+  ];
+
+  private static readonly InputMethodServiceTraceFileProto = TamperedMessageType.tamper(
+    root.lookupType('android.view.inputmethod.InputMethodServiceTraceFileProto')
+  );
+  private static readonly entryField =
+    ParserInputMethodService.InputMethodServiceTraceFileProto.fields['entry'];
+  private static readonly serviceField = assertDefined(
+    ParserInputMethodService.entryField.tamperedMessageType
+  ).fields['inputMethodService'];
+
+  private static readonly Operations = {
+    SetFormattersService: new SetFormatters(ParserInputMethodService.serviceField),
+    TranslateIntDefService: new TranslateIntDef(ParserInputMethodService.serviceField),
+    AddDefaultsServiceEager: new AddDefaults(
+      ParserInputMethodService.serviceField,
+      ParserInputMethodService.SERVICE_EAGER_PROPERTIES
+    ),
+    AddDefaultsServiceLazy: new AddDefaults(
+      ParserInputMethodService.serviceField,
+      undefined,
+      ParserInputMethodService.SERVICE_EAGER_PROPERTIES
+    ),
+    SetFormattersEntry: new SetFormatters(ParserInputMethodService.entryField),
+    AddDefaultsEntryEager: new AddDefaults(
+      ParserInputMethodService.entryField,
+      ParserInputMethodService.ENTRY_EAGER_PROPERTIES
+    ),
+    AddDefaultsEntryLazy: new AddDefaults(
+      ParserInputMethodService.entryField,
+      undefined,
+      ParserInputMethodService.ENTRY_EAGER_PROPERTIES.concat(
+        ParserInputMethodService.ENTRY_DENYLIST_PROPERTIES
+      )
+    ),
+  };
+
+  private realToElapsedTimeOffsetNs: undefined | bigint;
+
+  constructor(trace: TraceFile) {
+    super(trace);
+    this.realToElapsedTimeOffsetNs = undefined;
+  }
+
+  getTraceType(): TraceType {
+    return TraceType.INPUT_METHOD_SERVICE;
+  }
+
+  override getMagicNumber(): number[] {
+    return ParserInputMethodService.MAGIC_NUMBER;
+  }
+
+  override decodeTrace(
+    buffer: Uint8Array
+  ): android.view.inputmethod.IInputMethodServiceTraceProto[] {
+    const decoded = ParserInputMethodService.InputMethodServiceTraceFileProto.decode(
+      buffer
+    ) as android.view.inputmethod.IInputMethodServiceTraceFileProto;
+    const timeOffset = BigInt(decoded.realToElapsedTimeOffsetNanos?.toString() ?? '0');
+    this.realToElapsedTimeOffsetNs = timeOffset !== 0n ? timeOffset : undefined;
+    return decoded.entry ?? [];
+  }
+
+  override getTimestamp(
+    type: TimestampType,
+    entry: android.view.inputmethod.IInputMethodServiceTraceProto
+  ): undefined | Timestamp {
+    const elapsedRealtimeNanos = BigInt(assertDefined(entry.elapsedRealtimeNanos).toString());
+    if (type === TimestampType.ELAPSED) {
+      return new Timestamp(type, elapsedRealtimeNanos);
+    } else if (type === TimestampType.REAL && this.realToElapsedTimeOffsetNs !== undefined) {
+      return new Timestamp(type, this.realToElapsedTimeOffsetNs + elapsedRealtimeNanos);
+    }
+    return undefined;
+  }
+
+  override processDecodedEntry(
+    index: number,
+    timestampType: TimestampType,
+    entry: android.view.inputmethod.IInputMethodServiceTraceProto
+  ): TraceTreeNode {
+    if (entry.elapsedRealtimeNanos === undefined || entry.elapsedRealtimeNanos === null) {
+      throw Error('Missing elapsedRealtimeNanos on entry');
+    }
+
+    const elapsedRealtimeNanos = BigInt(entry.elapsedRealtimeNanos.toString());
+
+    let clockTimeNanos: bigint | undefined = undefined;
+    if (this.realToElapsedTimeOffsetNs !== undefined && entry.elapsedRealtimeNanos !== undefined) {
+      clockTimeNanos = elapsedRealtimeNanos + this.realToElapsedTimeOffsetNs;
+    }
+
+    const timestamp = Timestamp.from(
+      timestampType,
+      elapsedRealtimeNanos,
+      this.realToElapsedTimeOffsetNs
+    );
+
+    return {
+      name: TimeUtils.format(timestamp) + ' - ' + entry.where,
+      kind: 'InputMethodService entry',
+      children: [
+        {
+          obj: ImeUtils.transformInputConnectionCall(entry.inputMethodService),
+          kind: 'InputMethodService',
+          name: '',
+          children: [],
+          stableId: 'service',
+          id: 'service',
+        },
+      ],
+      obj: entry,
+      stableId: 'entry',
+      id: 'entry',
+      elapsedRealtimeNanos,
+      clockTimeNanos,
+    };
+  }
+
+  private makeHierarchyTree(
+    entryProto: android.view.inputmethod.IInputMethodServiceTraceProto
+  ): HierarchyTreeNode {
+    const entry = new PropertiesProviderBuilder()
+      .setEagerProperties(this.makeEntryEagerPropertiesTree(entryProto))
+      .setLazyPropertiesStrategy(this.makeEntryLazyPropertiesStrategy(entryProto))
+      .setEagerOperations([ParserInputMethodService.Operations.AddDefaultsEntryEager])
+      .setCommonOperations([ParserInputMethodService.Operations.SetFormattersEntry])
+      .setLazyOperations([ParserInputMethodService.Operations.AddDefaultsEntryLazy])
+      .build();
+
+    const inputMethodService = entryProto.inputMethodService
+      ? new PropertiesProviderBuilder()
+          .setEagerProperties(this.makeServiceEagerPropertiesTree(entryProto.inputMethodService))
+          .setLazyPropertiesStrategy(
+            this.makeServiceLazyPropertiesStrategy(entryProto.inputMethodService)
+          )
+          .setEagerOperations([ParserInputMethodService.Operations.AddDefaultsServiceEager])
+          .setCommonOperations([
+            ParserInputMethodService.Operations.SetFormattersService,
+            ParserInputMethodService.Operations.TranslateIntDefService,
+          ])
+          .setLazyOperations([ParserInputMethodService.Operations.AddDefaultsServiceLazy])
+          .build()
+      : undefined;
+
+    return new HierarchyTreeBuilderInputMethod()
+      .setRoot(entry)
+      .setChildren(inputMethodService ? [inputMethodService] : [])
+      .build();
+  }
+
+  private makeEntryEagerPropertiesTree(
+    entryProto: android.view.inputmethod.IInputMethodServiceTraceProto
+  ): PropertyTreeNode {
+    const denyList: string[] = [];
+    Object.getOwnPropertyNames(entryProto).forEach((it) => {
+      if (!ParserInputMethodService.ENTRY_EAGER_PROPERTIES.includes(it)) denyList.push(it);
+    });
+
+    return new PropertyTreeBuilderFromProto()
+      .setData(entryProto)
+      .setRootId('InputMethodService')
+      .setRootName('entry')
+      .setDenyList(denyList)
+      .build();
+  }
+
+  private makeEntryLazyPropertiesStrategy(
+    entryProto: android.view.inputmethod.IInputMethodServiceTraceProto
+  ): LazyPropertiesStrategyType {
+    return async () => {
+      return new PropertyTreeBuilderFromProto()
+        .setData(entryProto)
+        .setRootId('InputMethodService')
+        .setRootName('entry')
+        .setDenyList(
+          ParserInputMethodService.ENTRY_EAGER_PROPERTIES.concat(
+            ParserInputMethodService.ENTRY_DENYLIST_PROPERTIES
+          )
+        )
+        .build();
+    };
+  }
+
+  private makeServiceEagerPropertiesTree(
+    serviceProto: android.inputmethodservice.IInputMethodServiceProto
+  ): PropertyTreeNode {
+    const denyList: string[] = [];
+    let data: any = serviceProto;
+    if (serviceProto) {
+      Object.getOwnPropertyNames(serviceProto).forEach((it) => {
+        if (!ParserInputMethodService.SERVICE_EAGER_PROPERTIES.includes(it)) denyList.push(it);
+      });
+    } else {
+      data = {inputMethodService: null};
+    }
+    return new PropertyTreeBuilderFromProto()
+      .setData(serviceProto)
+      .setRootId('InputMethodService')
+      .setRootName('inputMethodService')
+      .setDenyList(denyList)
+      .setVisitPrototype(false)
+      .build();
+  }
+
+  private makeServiceLazyPropertiesStrategy(
+    serviceProto: android.inputmethodservice.IInputMethodServiceProto
+  ): LazyPropertiesStrategyType {
+    return async () => {
+      return new PropertyTreeBuilderFromProto()
+        .setData(serviceProto ?? {inputMethodService: null})
+        .setRootId('InputMethodService')
+        .setRootName('inputMethodService')
+        .setDenyList(ParserInputMethodService.SERVICE_EAGER_PROPERTIES)
+        .setVisitPrototype(false)
+        .build();
+    };
+  }
+}
+
+export {ParserInputMethodService};
diff --git a/tools/winscope/src/parsers/parser_input_method_service_test.ts b/tools/winscope/src/parsers/input_method/parser_input_method_service_test.ts
similarity index 100%
rename from tools/winscope/src/parsers/parser_input_method_service_test.ts
rename to tools/winscope/src/parsers/input_method/parser_input_method_service_test.ts
diff --git a/tools/winscope/src/parsers/parser_factory.ts b/tools/winscope/src/parsers/parser_factory.ts
index cf31124..721d8a1 100644
--- a/tools/winscope/src/parsers/parser_factory.ts
+++ b/tools/winscope/src/parsers/parser_factory.ts
@@ -22,10 +22,10 @@
 import {Parser} from 'trace/parser';
 import {TraceFile} from 'trace/trace_file';
 import {FileAndParser} from './file_and_parser';
+import {ParserInputMethodClients} from './input_method/parser_input_method_clients';
+import {ParserInputMethodManagerService} from './input_method/parser_input_method_manager_service';
+import {ParserInputMethodService} from './input_method/parser_input_method_service';
 import {ParserEventLog} from './parser_eventlog';
-import {ParserInputMethodClients} from './parser_input_method_clients';
-import {ParserInputMethodManagerService} from './parser_input_method_manager_service';
-import {ParserInputMethodService} from './parser_input_method_service';
 import {ParserProtoLog} from './parser_protolog';
 import {ParserScreenRecording} from './parser_screen_recording';
 import {ParserScreenRecordingLegacy} from './parser_screen_recording_legacy';
diff --git a/tools/winscope/src/parsers/parser_input_method_clients.ts b/tools/winscope/src/parsers/parser_input_method_clients.ts
deleted file mode 100644
index 42d1f51..0000000
--- a/tools/winscope/src/parsers/parser_input_method_clients.ts
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
- * 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 {assertDefined} from 'common/assert_utils';
-import {Timestamp, TimestampType} from 'common/time';
-import {TimeUtils} from 'common/time_utils';
-import root from 'protos/ime/latest/json';
-import {android} from 'protos/ime/latest/static';
-import {TraceFile} from 'trace/trace_file';
-import {TraceTreeNode} from 'trace/trace_tree_node';
-import {TraceType} from 'trace/trace_type';
-import {ImeUtils} from 'viewers/common/ime_utils';
-import {AbstractParser} from './abstract_parser';
-
-class ParserInputMethodClients extends AbstractParser {
-  private static readonly InputMethodClientsTraceFileProto = root.lookupType(
-    'android.view.inputmethod.InputMethodClientsTraceFileProto'
-  );
-
-  constructor(trace: TraceFile) {
-    super(trace);
-    this.realToElapsedTimeOffsetNs = undefined;
-  }
-
-  getTraceType(): TraceType {
-    return TraceType.INPUT_METHOD_CLIENTS;
-  }
-
-  override getMagicNumber(): number[] {
-    return ParserInputMethodClients.MAGIC_NUMBER;
-  }
-
-  override decodeTrace(
-    buffer: Uint8Array
-  ): android.view.inputmethod.IInputMethodClientsTraceProto[] {
-    const decoded = ParserInputMethodClients.InputMethodClientsTraceFileProto.decode(
-      buffer
-    ) as android.view.inputmethod.IInputMethodClientsTraceFileProto;
-    const timeOffset = BigInt(decoded.realToElapsedTimeOffsetNanos?.toString() ?? '0');
-    this.realToElapsedTimeOffsetNs = timeOffset !== 0n ? timeOffset : undefined;
-    return decoded.entry ?? [];
-  }
-
-  override getTimestamp(
-    type: TimestampType,
-    entry: android.view.inputmethod.IInputMethodClientsTraceProto
-  ): undefined | Timestamp {
-    const elapsedRealtimeNanos = BigInt(assertDefined(entry.elapsedRealtimeNanos).toString());
-    if (type === TimestampType.ELAPSED) {
-      return new Timestamp(type, elapsedRealtimeNanos);
-    } else if (type === TimestampType.REAL && this.realToElapsedTimeOffsetNs !== undefined) {
-      return new Timestamp(type, elapsedRealtimeNanos + this.realToElapsedTimeOffsetNs);
-    }
-    return undefined;
-  }
-
-  override processDecodedEntry(
-    index: number,
-    timestampType: TimestampType,
-    entry: android.view.inputmethod.IInputMethodClientsTraceProto
-  ): TraceTreeNode {
-    if (entry.elapsedRealtimeNanos === undefined || entry.elapsedRealtimeNanos === null) {
-      throw Error('Missing elapsedRealtimeNanos on entry');
-    }
-
-    const elapsedRealtimeNanos = BigInt(entry.elapsedRealtimeNanos.toString());
-
-    let clockTimeNanos: bigint | undefined = undefined;
-    if (this.realToElapsedTimeOffsetNs !== undefined) {
-      clockTimeNanos = elapsedRealtimeNanos + this.realToElapsedTimeOffsetNs;
-    }
-
-    const timestamp = Timestamp.from(
-      timestampType,
-      elapsedRealtimeNanos,
-      this.realToElapsedTimeOffsetNs
-    );
-
-    return {
-      name: TimeUtils.format(timestamp) + ' - ' + entry.where,
-      kind: 'InputMethodClient entry',
-      children: [
-        {
-          obj: ImeUtils.transformInputConnectionCall(entry.client),
-          kind: 'Client',
-          name: entry.client?.viewRootImpl?.view ?? '',
-          children: [],
-          stableId: 'client',
-          id: 'client',
-        },
-      ],
-      obj: entry,
-      stableId: 'entry',
-      id: 'entry',
-      elapsedRealtimeNanos,
-      clockTimeNanos,
-    };
-  }
-
-  private realToElapsedTimeOffsetNs: undefined | bigint;
-  private static readonly MAGIC_NUMBER = [0x09, 0x49, 0x4d, 0x43, 0x54, 0x52, 0x41, 0x43, 0x45]; // .IMCTRACE
-}
-
-export {ParserInputMethodClients};
diff --git a/tools/winscope/src/parsers/parser_input_method_manager_service.ts b/tools/winscope/src/parsers/parser_input_method_manager_service.ts
deleted file mode 100644
index 93847ac..0000000
--- a/tools/winscope/src/parsers/parser_input_method_manager_service.ts
+++ /dev/null
@@ -1,116 +0,0 @@
-/*
- * 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 {Timestamp, TimestampType} from 'common/time';
-import {TimeUtils} from 'common/time_utils';
-import root from 'protos/ime/latest/json';
-import {android} from 'protos/ime/latest/static';
-import {TraceFile} from 'trace/trace_file';
-import {TraceTreeNode} from 'trace/trace_tree_node';
-import {TraceType} from 'trace/trace_type';
-import {assertDefined} from '../common/assert_utils';
-import {AbstractParser} from './abstract_parser';
-
-class ParserInputMethodManagerService extends AbstractParser {
-  private static readonly InputMethodManagerServiceTraceFileProto = root.lookupType(
-    'android.view.inputmethod.InputMethodManagerServiceTraceFileProto'
-  );
-
-  constructor(trace: TraceFile) {
-    super(trace);
-    this.realToElapsedTimeOffsetNs = undefined;
-  }
-
-  getTraceType(): TraceType {
-    return TraceType.INPUT_METHOD_MANAGER_SERVICE;
-  }
-
-  override getMagicNumber(): number[] {
-    return ParserInputMethodManagerService.MAGIC_NUMBER;
-  }
-
-  override decodeTrace(
-    buffer: Uint8Array
-  ): android.view.inputmethod.IInputMethodManagerServiceTraceProto[] {
-    const decoded = ParserInputMethodManagerService.InputMethodManagerServiceTraceFileProto.decode(
-      buffer
-    ) as android.view.inputmethod.IInputMethodManagerServiceTraceFileProto;
-    const timeOffset = BigInt(decoded.realToElapsedTimeOffsetNanos?.toString() ?? '0');
-    this.realToElapsedTimeOffsetNs = timeOffset !== 0n ? timeOffset : undefined;
-    return decoded.entry ?? [];
-  }
-
-  protected override getTimestamp(
-    type: TimestampType,
-    entry: android.view.inputmethod.IInputMethodManagerServiceTraceProto
-  ): undefined | Timestamp {
-    const elapsedRealtimeNanos = BigInt(assertDefined(entry.elapsedRealtimeNanos).toString());
-    if (type === TimestampType.ELAPSED) {
-      return new Timestamp(TimestampType.ELAPSED, elapsedRealtimeNanos);
-    } else if (type === TimestampType.REAL && this.realToElapsedTimeOffsetNs !== undefined) {
-      return new Timestamp(type, this.realToElapsedTimeOffsetNs + elapsedRealtimeNanos);
-    }
-    return undefined;
-  }
-
-  protected override processDecodedEntry(
-    index: number,
-    timestampType: TimestampType,
-    entry: android.view.inputmethod.IInputMethodManagerServiceTraceProto
-  ): TraceTreeNode {
-    if (entry.elapsedRealtimeNanos === undefined || entry.elapsedRealtimeNanos === null) {
-      throw Error('Missing elapsedRealtimeNanos on entry');
-    }
-
-    const elapsedRealtimeNanos = BigInt(entry.elapsedRealtimeNanos.toString());
-
-    let clockTimeNanos: bigint | undefined = undefined;
-    if (this.realToElapsedTimeOffsetNs !== undefined && entry.elapsedRealtimeNanos !== undefined) {
-      clockTimeNanos = elapsedRealtimeNanos + this.realToElapsedTimeOffsetNs;
-    }
-
-    const timestamp = Timestamp.from(
-      timestampType,
-      elapsedRealtimeNanos,
-      this.realToElapsedTimeOffsetNs
-    );
-
-    return {
-      name: TimeUtils.format(timestamp) + ' - ' + entry.where,
-      kind: 'InputMethodManagerService entry',
-      children: [
-        {
-          obj: entry.inputMethodManagerService,
-          kind: 'InputMethodManagerService',
-          name: '',
-          children: [],
-          stableId: 'managerservice',
-          id: 'managerservice',
-        },
-      ],
-      obj: entry,
-      stableId: 'entry',
-      id: 'entry',
-      elapsedRealtimeNanos,
-      clockTimeNanos,
-    };
-  }
-
-  private realToElapsedTimeOffsetNs: undefined | bigint;
-  private static readonly MAGIC_NUMBER = [0x09, 0x49, 0x4d, 0x4d, 0x54, 0x52, 0x41, 0x43, 0x45]; // .IMMTRACE
-}
-
-export {ParserInputMethodManagerService};
diff --git a/tools/winscope/src/parsers/parser_input_method_service.ts b/tools/winscope/src/parsers/parser_input_method_service.ts
deleted file mode 100644
index 3399916..0000000
--- a/tools/winscope/src/parsers/parser_input_method_service.ts
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
- * 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 {Timestamp, TimestampType} from 'common/time';
-import {TimeUtils} from 'common/time_utils';
-import root from 'protos/ime/latest/json';
-import {android} from 'protos/ime/latest/static';
-import {TraceFile} from 'trace/trace_file';
-import {TraceTreeNode} from 'trace/trace_tree_node';
-import {TraceType} from 'trace/trace_type';
-import {ImeUtils} from 'viewers/common/ime_utils';
-import {assertDefined} from '../common/assert_utils';
-import {AbstractParser} from './abstract_parser';
-
-class ParserInputMethodService extends AbstractParser {
-  private static readonly InputMethodServiceTraceFileProto = root.lookupType(
-    'android.view.inputmethod.InputMethodServiceTraceFileProto'
-  );
-
-  constructor(trace: TraceFile) {
-    super(trace);
-    this.realToElapsedTimeOffsetNs = undefined;
-  }
-
-  getTraceType(): TraceType {
-    return TraceType.INPUT_METHOD_SERVICE;
-  }
-
-  override getMagicNumber(): number[] {
-    return ParserInputMethodService.MAGIC_NUMBER;
-  }
-
-  override decodeTrace(
-    buffer: Uint8Array
-  ): android.view.inputmethod.IInputMethodServiceTraceProto[] {
-    const decoded = ParserInputMethodService.InputMethodServiceTraceFileProto.decode(
-      buffer
-    ) as android.view.inputmethod.IInputMethodServiceTraceFileProto;
-    const timeOffset = BigInt(decoded.realToElapsedTimeOffsetNanos?.toString() ?? '0');
-    this.realToElapsedTimeOffsetNs = timeOffset !== 0n ? timeOffset : undefined;
-    return decoded.entry ?? [];
-  }
-
-  override getTimestamp(
-    type: TimestampType,
-    entry: android.view.inputmethod.IInputMethodServiceTraceProto
-  ): undefined | Timestamp {
-    const elapsedRealtimeNanos = BigInt(assertDefined(entry.elapsedRealtimeNanos).toString());
-    if (type === TimestampType.ELAPSED) {
-      return new Timestamp(type, elapsedRealtimeNanos);
-    } else if (type === TimestampType.REAL && this.realToElapsedTimeOffsetNs !== undefined) {
-      return new Timestamp(type, this.realToElapsedTimeOffsetNs + elapsedRealtimeNanos);
-    }
-    return undefined;
-  }
-
-  override processDecodedEntry(
-    index: number,
-    timestampType: TimestampType,
-    entry: android.view.inputmethod.IInputMethodServiceTraceProto
-  ): TraceTreeNode {
-    if (entry.elapsedRealtimeNanos === undefined || entry.elapsedRealtimeNanos === null) {
-      throw Error('Missing elapsedRealtimeNanos on entry');
-    }
-
-    const elapsedRealtimeNanos = BigInt(entry.elapsedRealtimeNanos.toString());
-
-    let clockTimeNanos: bigint | undefined = undefined;
-    if (this.realToElapsedTimeOffsetNs !== undefined && entry.elapsedRealtimeNanos !== undefined) {
-      clockTimeNanos = elapsedRealtimeNanos + this.realToElapsedTimeOffsetNs;
-    }
-
-    const timestamp = Timestamp.from(
-      timestampType,
-      elapsedRealtimeNanos,
-      this.realToElapsedTimeOffsetNs
-    );
-
-    return {
-      name: TimeUtils.format(timestamp) + ' - ' + entry.where,
-      kind: 'InputMethodService entry',
-      children: [
-        {
-          obj: ImeUtils.transformInputConnectionCall(entry.inputMethodService),
-          kind: 'InputMethodService',
-          name: '',
-          children: [],
-          stableId: 'service',
-          id: 'service',
-        },
-      ],
-      obj: entry,
-      stableId: 'entry',
-      id: 'entry',
-      elapsedRealtimeNanos,
-      clockTimeNanos,
-    };
-  }
-
-  private realToElapsedTimeOffsetNs: undefined | bigint;
-  private static readonly MAGIC_NUMBER = [0x09, 0x49, 0x4d, 0x53, 0x54, 0x52, 0x41, 0x43, 0x45]; // .IMSTRACE
-}
-
-export {ParserInputMethodService};
diff --git a/tools/winscope/src/parsers/surface_flinger/parser_surface_flinger_utils.ts b/tools/winscope/src/parsers/surface_flinger/parser_surface_flinger_utils.ts
index dd905ca..7f8c8e7 100644
--- a/tools/winscope/src/parsers/surface_flinger/parser_surface_flinger_utils.ts
+++ b/tools/winscope/src/parsers/surface_flinger/parser_surface_flinger_utils.ts
@@ -17,6 +17,7 @@
 import {assertDefined} from 'common/assert_utils';
 import {perfetto} from 'protos/surfaceflinger/latest/static';
 import {android} from 'protos/surfaceflinger/udc/static';
+import {LazyPropertiesStrategyType} from 'trace/tree_node/properties_provider';
 import {PropertyTreeBuilderFromProto} from 'trace/tree_node/property_tree_builder_from_proto';
 import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
 import {AddCompositionType} from './operations/add_composition_type';
@@ -120,7 +121,7 @@
   static makeLayerLazyPropertiesStrategy(
     layer: android.surfaceflinger.ILayerProto | perfetto.protos.ILayerProto,
     duplicateCount: number
-  ) {
+  ): LazyPropertiesStrategyType {
     return async () => {
       return new PropertyTreeBuilderFromProto()
         .setData(layer)
@@ -134,7 +135,7 @@
 
   static makeEntryLazyPropertiesStrategy(
     entry: android.surfaceflinger.ILayersTraceProto | perfetto.protos.ILayersSnapshotProto
-  ) {
+  ): LazyPropertiesStrategyType {
     return async () => {
       return new PropertyTreeBuilderFromProto()
         .setData(entry)
diff --git a/tools/winscope/src/parsers/view_capture/parser_view_capture_window.ts b/tools/winscope/src/parsers/view_capture/parser_view_capture_window.ts
index 91fd25f..a114bd7 100644
--- a/tools/winscope/src/parsers/view_capture/parser_view_capture_window.ts
+++ b/tools/winscope/src/parsers/view_capture/parser_view_capture_window.ts
@@ -30,7 +30,7 @@
 import {TraceType} from 'trace/trace_type';
 import {HierarchyTreeNode} from 'trace/tree_node/hierarchy_tree_node';
 import {Operation} from 'trace/tree_node/operations/operation';
-import {PropertiesProvider} from 'trace/tree_node/properties_provider';
+import {LazyPropertiesStrategyType, PropertiesProvider} from 'trace/tree_node/properties_provider';
 import {PropertiesProviderBuilder} from 'trace/tree_node/properties_provider_builder';
 import {PropertyTreeBuilderFromProto} from 'trace/tree_node/property_tree_builder_from_proto';
 import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
@@ -235,7 +235,9 @@
     return children.map((child) => assertDefined(child.hashcode));
   }
 
-  private makeLazyPropertiesStrategy(node: com.android.app.viewcapture.data.IViewNode) {
+  private makeLazyPropertiesStrategy(
+    node: com.android.app.viewcapture.data.IViewNode
+  ): LazyPropertiesStrategyType {
     return async () => {
       const id = `${this.classNames[assertDefined(node.classnameIndex)]}@${node.hashcode}`;
       return new PropertyTreeBuilderFromProto()
diff --git a/tools/winscope/src/parsers/window_manager/parser_window_manager_utils.ts b/tools/winscope/src/parsers/window_manager/parser_window_manager_utils.ts
index 333a5eb..98098d3 100644
--- a/tools/winscope/src/parsers/window_manager/parser_window_manager_utils.ts
+++ b/tools/winscope/src/parsers/window_manager/parser_window_manager_utils.ts
@@ -16,7 +16,7 @@
 
 import {assertDefined} from 'common/assert_utils';
 import {com} from 'protos/windowmanager/latest/static';
-import {PropertiesProvider} from 'trace/tree_node/properties_provider';
+import {LazyPropertiesStrategyType, PropertiesProvider} from 'trace/tree_node/properties_provider';
 import {PropertiesProviderBuilder} from 'trace/tree_node/properties_provider_builder';
 import {PropertyTreeBuilderFromProto} from 'trace/tree_node/property_tree_builder_from_proto';
 import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
@@ -110,7 +110,7 @@
 
   private makeEntryLazyPropertiesStrategy(
     entry: com.android.server.wm.IWindowManagerServiceDumpProto
-  ) {
+  ): LazyPropertiesStrategyType {
     return async () => {
       return new PropertyTreeBuilderFromProto()
         .setData(entry)
@@ -227,13 +227,15 @@
       .setDenyList(denyList)
       .build();
 
-    containerProperties.addOrReplaceChild(
-      DEFAULT_PROPERTY_TREE_NODE_FACTORY.makeCalculatedProperty(
-        containerProperties.id,
-        'children',
-        this.mapChildrenToTokens(children)
-      )
-    );
+    if (children.length > 0) {
+      containerProperties.addOrReplaceChild(
+        DEFAULT_PROPERTY_TREE_NODE_FACTORY.makeCalculatedProperty(
+          containerProperties.id,
+          'children',
+          this.mapChildrenToTokens(children)
+        )
+      );
+    }
 
     containerProperties.addOrReplaceChild(
       DEFAULT_PROPERTY_TREE_NODE_FACTORY.makeCalculatedProperty(
@@ -249,7 +251,7 @@
   private makeContainerChildLazyPropertiesStrategy(
     containerChild: com.android.server.wm.IWindowContainerChildProto,
     containerChildType: WmProtoType
-  ) {
+  ): LazyPropertiesStrategyType {
     return async () => {
       const identifier = this.getIdentifier(containerChild);
       const name = this.getName(containerChild, identifier);
diff --git a/tools/winscope/src/trace/tree_node/properties_provider.ts b/tools/winscope/src/trace/tree_node/properties_provider.ts
index 61792d0..329c3a0 100644
--- a/tools/winscope/src/trace/tree_node/properties_provider.ts
+++ b/tools/winscope/src/trace/tree_node/properties_provider.ts
@@ -19,6 +19,8 @@
 import {PropertySource, PropertyTreeNode} from 'trace/tree_node/property_tree_node';
 import {DEFAULT_PROPERTY_TREE_NODE_FACTORY} from './property_tree_node_factory';
 
+export type LazyPropertiesStrategyType = () => Promise<PropertyTreeNode>;
+
 export class PropertiesProvider {
   private eagerPropertiesRoot: PropertyTreeNode;
   private lazyPropertiesRoot: PropertyTreeNode | undefined;
@@ -26,7 +28,7 @@
 
   constructor(
     eagerPropertiesRoot: PropertyTreeNode,
-    private readonly lazyPropertiesStrategy: () => Promise<PropertyTreeNode>,
+    private readonly lazyPropertiesStrategy: LazyPropertiesStrategyType,
     private readonly commonOperations: OperationChain<PropertyTreeNode>,
     private readonly eagerOperations: OperationChain<PropertyTreeNode>,
     private readonly lazyOperations: OperationChain<PropertyTreeNode>
diff --git a/tools/winscope/src/trace/tree_node/properties_provider_builder.ts b/tools/winscope/src/trace/tree_node/properties_provider_builder.ts
index ce96a7a..b1fecf4 100644
--- a/tools/winscope/src/trace/tree_node/properties_provider_builder.ts
+++ b/tools/winscope/src/trace/tree_node/properties_provider_builder.ts
@@ -18,11 +18,11 @@
 import {Operation} from 'trace/tree_node/operations/operation';
 import {OperationChain} from 'trace/tree_node/operations/operation_chain';
 import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
-import {PropertiesProvider} from './properties_provider';
+import {LazyPropertiesStrategyType, PropertiesProvider} from './properties_provider';
 
 export class PropertiesProviderBuilder {
   private eagerProperties: PropertyTreeNode | undefined;
-  private lazyPropertiesStrategy: (() => Promise<PropertyTreeNode>) | undefined;
+  private lazyPropertiesStrategy: LazyPropertiesStrategyType | undefined;
   private commonOperations = OperationChain.emptyChain<PropertyTreeNode>();
   private eagerOperations = OperationChain.emptyChain<PropertyTreeNode>();
   private lazyOperations = OperationChain.emptyChain<PropertyTreeNode>();
@@ -32,7 +32,7 @@
     return this;
   }
 
-  setLazyPropertiesStrategy(value: () => Promise<PropertyTreeNode>): this {
+  setLazyPropertiesStrategy(value: LazyPropertiesStrategyType): this {
     this.lazyPropertiesStrategy = value;
     return this;
   }
diff --git a/tools/winscope/src/trace/tree_node/property_tree_builder_from_proto.ts b/tools/winscope/src/trace/tree_node/property_tree_builder_from_proto.ts
index 3e6adb4..eafd031 100644
--- a/tools/winscope/src/trace/tree_node/property_tree_builder_from_proto.ts
+++ b/tools/winscope/src/trace/tree_node/property_tree_builder_from_proto.ts
@@ -23,6 +23,7 @@
   private proto: any | undefined;
   private rootId: string | number = 'UnknownRootId';
   private rootName: string | undefined = 'UnknownRootName';
+  private visitProtoType = true;
 
   setData(value: any): this {
     this.proto = value;
@@ -49,6 +50,11 @@
     return this;
   }
 
+  setVisitPrototype(value: boolean): this {
+    this.visitProtoType = value;
+    return this;
+  }
+
   build(): PropertyTreeNode {
     if (this.proto === undefined) {
       throw Error('proto not set');
@@ -59,7 +65,7 @@
     if (this.rootName === undefined) {
       throw Error('rootName not set');
     }
-    const factory = new PropertyTreeNodeFactory(this.denylistProperties);
+    const factory = new PropertyTreeNodeFactory(this.denylistProperties, this.visitProtoType);
 
     return factory.makeProtoProperty(this.makeNodeId(), '', this.proto);
   }
diff --git a/tools/winscope/src/trace/tree_node/property_tree_node_factory.ts b/tools/winscope/src/trace/tree_node/property_tree_node_factory.ts
index 9ec4635..d74aed8 100644
--- a/tools/winscope/src/trace/tree_node/property_tree_node_factory.ts
+++ b/tools/winscope/src/trace/tree_node/property_tree_node_factory.ts
@@ -17,7 +17,7 @@
 import {PropertySource, PropertyTreeNode} from 'trace/tree_node/property_tree_node';
 
 export class PropertyTreeNodeFactory {
-  constructor(private denylistProperties: string[] = []) {}
+  constructor(private denylistProperties: string[] = [], private visitPrototype = true) {}
 
   makePropertyRoot(
     rootId: string,
@@ -141,7 +141,7 @@
           props.push(prop);
         }
       });
-      obj = Object.getPrototypeOf(obj);
+      obj = this.visitPrototype ? Object.getPrototypeOf(obj) : undefined;
     } while (obj);
     return props.sort((a, b) => (a < b ? -1 : 1));
   }