Merge "ui: Add initial plugin architecture"
diff --git a/tools/gen_ui_imports b/tools/gen_ui_imports
index 1f10a38..2a5f5b4 100755
--- a/tools/gen_ui_imports
+++ b/tools/gen_ui_imports
@@ -13,15 +13,24 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-"""Generates TypeScript files that import all subdirectories.
-This is useful for plugins/extensions. If you have two modules:
+"""Generates TypeScript files that import all subdirectories and
+registers them with plugin registry. If you have three modules:
 - core/
-- plugins/myplugin/
+- plugins/foo_plugin/
+- plugins/bar_plugin/
 In general you would like the dependency to only go one way:
-- plugins/myplugin/ -> core/
-But you still need some index file to import all plugins for the sake of
-bundling. This avoids having to manually edit core/ for every
-plugin you add.
+- plugins/foo_plugin/ -> core/
+We want to avoid manually editing core/ for every plugin.
+
+This generates code like:
+
+import {pluginRegistry} from '../common/plugins';
+
+import {plugin as fooPlugin} from '../plugins/foo_plugin';
+import {plugin as barPlugin} from '../plugins/bar_plugin';
+
+pluginRegistry.register(fooPlugin);
+pluginRegistry.register(barPlugin);
 """
 
 from __future__ import print_function
@@ -33,17 +42,34 @@
 
 ROOT_DIR = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
 UI_SRC_DIR = os.path.join(ROOT_DIR, 'ui', 'src')
+PLUGINS_PATH = os.path.join(UI_SRC_DIR, 'common', 'plugins')
+
+def to_camel_case(s):
+  first, *rest = s.split('_')
+  return first + ''.join(x.title() for x in rest)
 
 def gen_imports(input_dir, output_path):
   paths = [os.path.join(input_dir, p) for p in os.listdir(input_dir)]
   paths = [p for p in paths if os.path.isdir(p)]
   paths.sort()
 
-  lines = []
+  output_dir = os.path.dirname(output_path)
+  rel_plugins_path = os.path.relpath(PLUGINS_PATH, output_dir)
+
+  imports = []
+  registrations =  []
   for path in paths:
-    rel_path = os.path.relpath(path, os.path.dirname(output_path))
-    lines.append(f"import '{rel_path}';")
-  expected = '\n'.join(lines)
+    rel_path = os.path.relpath(path, output_dir)
+    snake_name = os.path.basename(path)
+    camel_name = to_camel_case(snake_name)
+    imports.append(f"import {{plugin as {camel_name}}} from '{rel_path}';")
+    registrations.append(f"pluginRegistry.register({camel_name});")
+
+  header = f"import {{pluginRegistry}} from '{rel_plugins_path}';"
+  import_text = '\n'.join(imports)
+  registration_text = '\n'.join(registrations)
+
+  expected = f"{header}\n\n{import_text}\n\n{registration_text}"
 
   with open(output_path, 'w') as f:
     f.write(expected)
diff --git a/ui/src/common/plugin_api.ts b/ui/src/common/plugin_api.ts
new file mode 100644
index 0000000..913e190
--- /dev/null
+++ b/ui/src/common/plugin_api.ts
@@ -0,0 +1,18 @@
+// 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.
+
+export interface PluginInfo {
+  pluginId: string;
+  activate: () => void;
+}
diff --git a/ui/src/common/plugins.ts b/ui/src/common/plugins.ts
new file mode 100644
index 0000000..ea1e6e8
--- /dev/null
+++ b/ui/src/common/plugins.ts
@@ -0,0 +1,20 @@
+// 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 {PluginInfo} from './plugin_api';
+import {Registry} from './registry';
+
+export const pluginRegistry = new Registry<PluginInfo>((info) => {
+  return info.pluginId;
+});
diff --git a/ui/src/common/recordingV2/target_factory_registry.ts b/ui/src/common/recordingV2/target_factory_registry.ts
index f8be843..8f8f9b1 100644
--- a/ui/src/common/recordingV2/target_factory_registry.ts
+++ b/ui/src/common/recordingV2/target_factory_registry.ts
@@ -38,4 +38,6 @@
   }
 }
 
-export const targetFactoryRegistry = new TargetFactoryRegistry();
+export const targetFactoryRegistry = new TargetFactoryRegistry((f) => {
+  return f.kind;
+});
diff --git a/ui/src/common/registry.ts b/ui/src/common/registry.ts
index 2c36a96..1658834 100644
--- a/ui/src/common/registry.ts
+++ b/ui/src/common/registry.ts
@@ -14,15 +14,21 @@
 
 export interface HasKind { kind: string; }
 
-export class Registry<T extends HasKind> {
+export class Registry<T> {
+  private key: (t: T) => string;
   protected registry: Map<string, T>;
 
-  constructor() {
+  static kindRegistry<T extends HasKind>(): Registry<T> {
+    return new Registry<T>((t) => t.kind);
+  }
+
+  constructor(key: (t: T) => string) {
     this.registry = new Map<string, T>();
+    this.key = key;
   }
 
   register(registrant: T) {
-    const kind = registrant.kind;
+    const kind = this.key(registrant);
     if (this.registry.has(kind)) {
       throw new Error(`Registrant ${kind} already exists in the registry`);
     }
@@ -41,6 +47,11 @@
     return registrant;
   }
 
+  // Support iteration: for (const foo of fooRegistry.values()) { ... }
+  * values() {
+    yield* this.registry.values();
+  }
+
   unregisterAllForTesting(): void {
     this.registry.clear();
   }
diff --git a/ui/src/common/registry_unittest.ts b/ui/src/common/registry_unittest.ts
index 672b5a9..58a04e4 100644
--- a/ui/src/common/registry_unittest.ts
+++ b/ui/src/common/registry_unittest.ts
@@ -1,4 +1,3 @@
-
 // Copyright (C) 2018 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
@@ -21,7 +20,7 @@
 }
 
 test('registry returns correct registrant', () => {
-  const registry = new Registry<Registrant>();
+  const registry = Registry.kindRegistry<Registrant>();
 
   const a: Registrant = {kind: 'a', n: 1};
   const b: Registrant = {kind: 'b', n: 2};
@@ -33,7 +32,7 @@
 });
 
 test('registry throws error on kind collision', () => {
-  const registry = new Registry<Registrant>();
+  const registry = Registry.kindRegistry<Registrant>();
 
   const a1: Registrant = {kind: 'a', n: 1};
   const a2: Registrant = {kind: 'a', n: 2};
@@ -43,6 +42,19 @@
 });
 
 test('registry throws error on non-existent track', () => {
-  const registry = new Registry<Registrant>();
+  const registry = Registry.kindRegistry<Registrant>();
   expect(() => registry.get('foo')).toThrow();
 });
+
+test('registry allows iteration', () => {
+  const registry = Registry.kindRegistry<Registrant>();
+  const a: Registrant = {kind: 'a', n: 1};
+  const b: Registrant = {kind: 'b', n: 2};
+  registry.register(a);
+  registry.register(b);
+
+  const values = [...registry.values()];
+  expect(values.length).toBe(2);
+  expect(values.includes(a)).toBe(true);
+  expect(values.includes(b)).toBe(true);
+});
diff --git a/ui/src/controller/track_controller.ts b/ui/src/controller/track_controller.ts
index bf56a35..9f8593a 100644
--- a/ui/src/controller/track_controller.ts
+++ b/ui/src/controller/track_controller.ts
@@ -289,4 +289,5 @@
   kind: string;
 }
 
-export const trackControllerRegistry = new Registry<TrackControllerFactory>();
+export const trackControllerRegistry =
+    Registry.kindRegistry<TrackControllerFactory>();
diff --git a/ui/src/frontend/index.ts b/ui/src/frontend/index.ts
index a86e2d1..f59f224 100644
--- a/ui/src/frontend/index.ts
+++ b/ui/src/frontend/index.ts
@@ -21,6 +21,7 @@
 import {createEmptyState} from '../common/empty_state';
 import {RECORDING_V2_FLAG} from '../common/feature_flags';
 import {initializeImmerJs} from '../common/immer_init';
+import {pluginRegistry} from '../common/plugins';
 import {State} from '../common/state';
 import {initWasm} from '../common/wasm_engine_proxy';
 import {ControllerWorkerInitMessage} from '../common/worker_messages';
@@ -296,6 +297,11 @@
   if (globals.testing) {
     document.body.classList.add('testing');
   }
+
+  // Initialize all plugins:
+  for (const plugin of pluginRegistry.values()) {
+    plugin.activate();
+  }
 }
 
 
diff --git a/ui/src/frontend/track_registry.ts b/ui/src/frontend/track_registry.ts
index f6c9401..92007d7 100644
--- a/ui/src/frontend/track_registry.ts
+++ b/ui/src/frontend/track_registry.ts
@@ -18,4 +18,4 @@
 /**
  * Global registry that maps types to TrackCreator.
  */
-export const trackRegistry = new Registry<TrackCreator>();
+export const trackRegistry = Registry.kindRegistry<TrackCreator>();
diff --git a/ui/src/tracks/actual_frames/index.ts b/ui/src/tracks/actual_frames/index.ts
index 308406e..7cfe94c 100644
--- a/ui/src/tracks/actual_frames/index.ts
+++ b/ui/src/tracks/actual_frames/index.ts
@@ -14,3 +14,8 @@
 
 import './controller';
 import './frontend';
+
+export const plugin = {
+  pluginId: 'perfetto.ActualFrames',
+  activate: () => {},
+};
diff --git a/ui/src/tracks/android_log/index.ts b/ui/src/tracks/android_log/index.ts
index 308406e..8bbdc7c 100644
--- a/ui/src/tracks/android_log/index.ts
+++ b/ui/src/tracks/android_log/index.ts
@@ -14,3 +14,8 @@
 
 import './controller';
 import './frontend';
+
+export const plugin = {
+  pluginId: 'perfetto.AndroidLog',
+  activate: () => {},
+};
diff --git a/ui/src/tracks/async_slices/index.ts b/ui/src/tracks/async_slices/index.ts
index 308406e..b2393fd 100644
--- a/ui/src/tracks/async_slices/index.ts
+++ b/ui/src/tracks/async_slices/index.ts
@@ -14,3 +14,8 @@
 
 import './controller';
 import './frontend';
+
+export const plugin = {
+  pluginId: 'perfetto.AsyncSlices',
+  activate: () => {},
+};
diff --git a/ui/src/tracks/chrome_slices/index.ts b/ui/src/tracks/chrome_slices/index.ts
index 308406e..cd99ee0 100644
--- a/ui/src/tracks/chrome_slices/index.ts
+++ b/ui/src/tracks/chrome_slices/index.ts
@@ -14,3 +14,8 @@
 
 import './controller';
 import './frontend';
+
+export const plugin = {
+  pluginId: 'perfetto.ChromeSlices',
+  activate: () => {},
+};
diff --git a/ui/src/tracks/counter/index.ts b/ui/src/tracks/counter/index.ts
index 308406e..c510075 100644
--- a/ui/src/tracks/counter/index.ts
+++ b/ui/src/tracks/counter/index.ts
@@ -14,3 +14,8 @@
 
 import './controller';
 import './frontend';
+
+export const plugin = {
+  pluginId: 'perfetto.Counter',
+  activate: () => {},
+};
diff --git a/ui/src/tracks/cpu_freq/index.ts b/ui/src/tracks/cpu_freq/index.ts
index 308406e..239985b 100644
--- a/ui/src/tracks/cpu_freq/index.ts
+++ b/ui/src/tracks/cpu_freq/index.ts
@@ -14,3 +14,8 @@
 
 import './controller';
 import './frontend';
+
+export const plugin = {
+  pluginId: 'perfetto.CpuFreq',
+  activate: () => {},
+};
diff --git a/ui/src/tracks/cpu_profile/index.ts b/ui/src/tracks/cpu_profile/index.ts
index 308406e..02c3cac 100644
--- a/ui/src/tracks/cpu_profile/index.ts
+++ b/ui/src/tracks/cpu_profile/index.ts
@@ -14,3 +14,8 @@
 
 import './controller';
 import './frontend';
+
+export const plugin = {
+  pluginId: 'perfetto.CpuProfile',
+  activate: () => {},
+};
diff --git a/ui/src/tracks/cpu_slices/index.ts b/ui/src/tracks/cpu_slices/index.ts
index 308406e..2f74b2d 100644
--- a/ui/src/tracks/cpu_slices/index.ts
+++ b/ui/src/tracks/cpu_slices/index.ts
@@ -14,3 +14,8 @@
 
 import './controller';
 import './frontend';
+
+export const plugin = {
+  pluginId: 'perfetto.CpuSlices',
+  activate: () => {},
+};
diff --git a/ui/src/tracks/debug_slices/index.ts b/ui/src/tracks/debug_slices/index.ts
index 308406e..681f3d2 100644
--- a/ui/src/tracks/debug_slices/index.ts
+++ b/ui/src/tracks/debug_slices/index.ts
@@ -14,3 +14,8 @@
 
 import './controller';
 import './frontend';
+
+export const plugin = {
+  pluginId: 'perfetto.DebugSlices',
+  activate: () => {},
+};
diff --git a/ui/src/tracks/expected_frames/index.ts b/ui/src/tracks/expected_frames/index.ts
index 308406e..c87320b 100644
--- a/ui/src/tracks/expected_frames/index.ts
+++ b/ui/src/tracks/expected_frames/index.ts
@@ -14,3 +14,8 @@
 
 import './controller';
 import './frontend';
+
+export const plugin = {
+  pluginId: 'perfetto.ExpectedFrames',
+  activate: () => {},
+};
diff --git a/ui/src/tracks/generic_slice_track/index.ts b/ui/src/tracks/generic_slice_track/index.ts
index 3c66953..25c6c7a 100644
--- a/ui/src/tracks/generic_slice_track/index.ts
+++ b/ui/src/tracks/generic_slice_track/index.ts
@@ -46,3 +46,8 @@
 }
 
 trackRegistry.register(GenericSliceTrack);
+
+export const plugin = {
+  pluginId: 'perfetto.GenericSliceTrack',
+  activate: () => {},
+};
diff --git a/ui/src/tracks/heap_profile/index.ts b/ui/src/tracks/heap_profile/index.ts
index 308406e..bb45aca 100644
--- a/ui/src/tracks/heap_profile/index.ts
+++ b/ui/src/tracks/heap_profile/index.ts
@@ -14,3 +14,8 @@
 
 import './controller';
 import './frontend';
+
+export const plugin = {
+  pluginId: 'perfetto.HeapProfile',
+  activate: () => {},
+};
diff --git a/ui/src/tracks/null_track/index.ts b/ui/src/tracks/null_track/index.ts
index c0f4b7e..5a9483a 100644
--- a/ui/src/tracks/null_track/index.ts
+++ b/ui/src/tracks/null_track/index.ts
@@ -36,3 +36,8 @@
 }
 
 trackRegistry.register(NullTrack);
+
+export const plugin = {
+  pluginId: 'perfetto.NullTrack',
+  activate: () => {},
+};
diff --git a/ui/src/tracks/perf_samples_profile/index.ts b/ui/src/tracks/perf_samples_profile/index.ts
index 308406e..abadb05 100644
--- a/ui/src/tracks/perf_samples_profile/index.ts
+++ b/ui/src/tracks/perf_samples_profile/index.ts
@@ -14,3 +14,8 @@
 
 import './controller';
 import './frontend';
+
+export const plugin = {
+  pluginId: 'perfetto.PerfSamplesProfile',
+  activate: () => {},
+};
diff --git a/ui/src/tracks/process_scheduling/index.ts b/ui/src/tracks/process_scheduling/index.ts
index 308406e..126071f 100644
--- a/ui/src/tracks/process_scheduling/index.ts
+++ b/ui/src/tracks/process_scheduling/index.ts
@@ -14,3 +14,8 @@
 
 import './controller';
 import './frontend';
+
+export const plugin = {
+  pluginId: 'perfetto.ProcessScheduling',
+  activate: () => {},
+};
diff --git a/ui/src/tracks/process_summary/index.ts b/ui/src/tracks/process_summary/index.ts
index 308406e..e6e93c0 100644
--- a/ui/src/tracks/process_summary/index.ts
+++ b/ui/src/tracks/process_summary/index.ts
@@ -14,3 +14,8 @@
 
 import './controller';
 import './frontend';
+
+export const plugin = {
+  pluginId: 'perfetto.ProcessSummary',
+  activate: () => {},
+};
diff --git a/ui/src/tracks/thread_state/index.ts b/ui/src/tracks/thread_state/index.ts
index 308406e..ceecf98 100644
--- a/ui/src/tracks/thread_state/index.ts
+++ b/ui/src/tracks/thread_state/index.ts
@@ -14,3 +14,8 @@
 
 import './controller';
 import './frontend';
+
+export const plugin = {
+  pluginId: 'perfetto.ThreadState',
+  activate: () => {},
+};