ui: Split TraceController in async functions

In this CL TraceController is still a controller, but gets
split into a bunch of async function, avoiding the usage of
globals as much as possible. This makes the next step (Removing the
controller) easier.

Change-Id: I8c6ee87a653f3f70c054c8b150fdbc7146933185
diff --git a/ui/src/controller/app_controller.ts b/ui/src/controller/app_controller.ts
index fa2e910..482f8b1 100644
--- a/ui/src/controller/app_controller.ts
+++ b/ui/src/controller/app_controller.ts
@@ -46,7 +46,7 @@
     }
     if (globals.state.engine !== undefined) {
       const engineCfg = globals.state.engine;
-      childControllers.push(Child(engineCfg.id, TraceController, engineCfg.id));
+      childControllers.push(Child(engineCfg.id, TraceController, engineCfg));
     }
     return childControllers;
   }
diff --git a/ui/src/controller/trace_controller.ts b/ui/src/controller/trace_controller.ts
index 22d45a1..2817ac7 100644
--- a/ui/src/controller/trace_controller.ts
+++ b/ui/src/controller/trace_controller.ts
@@ -32,7 +32,7 @@
 } from '../frontend/publish';
 import {addQueryResultsTab} from '../public/lib/query_table/query_result_tab';
 import {Router} from '../frontend/router';
-import {Engine, EngineBase, EngineMode} from '../trace_processor/engine';
+import {Engine, EngineBase} from '../trace_processor/engine';
 import {HttpRpcEngine} from '../trace_processor/http_rpc_engine';
 import {
   LONG,
@@ -61,6 +61,7 @@
 import {raf} from '../core/raf_scheduler';
 import {TraceImpl} from '../core/trace_impl';
 import {SerializedAppState} from '../public/state_serialization_schema';
+import {TraceSource} from '../public/trace_source';
 
 type States = 'init' | 'loading_trace' | 'ready';
 
@@ -157,31 +158,33 @@
 // tracks data and SQL queries. There is one TraceController instance for each
 // trace opened in the UI (for now only one trace is supported).
 export class TraceController extends Controller<States> {
-  private readonly engineId: string;
-  private engine?: EngineBase;
+  private trace?: TraceImpl = undefined;
 
-  constructor(engineId: string) {
+  constructor(private engineCfg: EngineConfig) {
     super('init');
-    this.engineId = engineId;
   }
 
   run() {
     switch (this.state) {
       case 'init':
-        this.loadTrace()
-          .then(() => AppImpl.instance.setIsLoadingTrace(false))
+        updateStatus('Opening trace');
+        this.setState('loading_trace');
+        loadTrace(this.engineCfg.source, this.engineCfg.id)
+          .then((traceImpl) => {
+            this.trace = traceImpl;
+            globals.dispatch(Actions.runControllers({}));
+            AppImpl.instance.setIsLoadingTrace(false);
+          })
           .catch((err) => {
-            this.updateStatus(`${err}`);
+            updateStatus(`${err}`);
             throw err;
           });
-        this.updateStatus('Opening trace');
-        this.setState('loading_trace');
         break;
 
       case 'loading_trace':
         // Stay in this state until loadTrace() returns and marks the engine as
         // ready.
-        if (this.engine === undefined || AppImpl.instance.isLoadingTrace) {
+        if (this.trace === undefined || AppImpl.instance.isLoadingTrace) {
           return;
         }
         this.setState('ready');
@@ -200,12 +203,25 @@
     AppImpl.instance.plugins.onTraceClose();
     AppImpl.instance.closeCurrentTrace();
   }
+}
 
-  private async loadTrace(): Promise<EngineMode> {
-    this.updateStatus('Creating trace processor');
+// TODO(primiano): the extra indentation here is purely to help Gerrit diff
+// detection algorithm. It can be re-formatted in the next CL.
+
+  async function loadTrace(
+    traceSource: TraceSource,
+    engineId: string,
+  ): Promise<TraceImpl> {
+    // TODO(primiano): in the next CL remember to invoke here clearState() because
+    // the openActions (which today do that) will be gone.
+    //  globals.dispatch(Actions.clearState({}));
+    const engine = await createEngine(engineId);
+    return await loadTraceIntoEngine(traceSource, engine);
+  }
+
+  async function createEngine(engineId: string): Promise<EngineBase> {
     // Check if there is any instance of the trace_processor_shell running in
     // HTTP RPC mode (i.e. trace_processor_shell -D).
-    let engineMode: EngineMode;
     let useRpc = false;
     if (AppImpl.instance.newEngineMode === 'USE_HTTP_RPC_IF_AVAILABLE') {
       useRpc = (await HttpRpcEngine.checkConnection()).connected;
@@ -213,12 +229,10 @@
     let engine;
     if (useRpc) {
       console.log('Opening trace using native accelerator over HTTP+RPC');
-      engineMode = 'HTTP_RPC';
-      engine = new HttpRpcEngine(this.engineId);
+      engine = new HttpRpcEngine(engineId);
     } else {
       console.log('Opening trace using built-in WASM engine');
-      engineMode = 'WASM';
-      engine = new WasmEngineProxy(this.engineId);
+      engine = new WasmEngineProxy(engineId);
       engine.resetTraceProcessor({
         cropTrackEvents: CROP_TRACK_EVENTS_FLAG.get(),
         ingestFtraceInRawTable: INGEST_FTRACE_IN_RAW_TABLE_FLAG.get(),
@@ -227,30 +241,31 @@
       });
     }
     engine.onResponseReceived = () => raf.scheduleFullRedraw();
-    this.engine = engine;
 
     if (isMetatracingEnabled()) {
-      this.engine.enableMetatrace(
-        assertExists(getEnabledMetatracingCategories()),
-      );
+      engine.enableMetatrace(assertExists(getEnabledMetatracingCategories()));
     }
+    return engine;
+  }
 
+  async function loadTraceIntoEngine(
+    traceSource: TraceSource,
+    engine: EngineBase,
+  ): Promise<TraceImpl> {
     AppImpl.instance.setIsLoadingTrace(true);
-    const engineCfg = assertExists(globals.state.engine);
-    assertTrue(engineCfg.id === this.engineId);
     let traceStream: TraceStream | undefined;
     let serializedAppState: SerializedAppState | undefined;
-    if (engineCfg.source.type === 'FILE') {
-      traceStream = new TraceFileStream(engineCfg.source.file);
-    } else if (engineCfg.source.type === 'ARRAY_BUFFER') {
-      traceStream = new TraceBufferStream(engineCfg.source.buffer);
-    } else if (engineCfg.source.type === 'URL') {
-      traceStream = new TraceHttpStream(engineCfg.source.url);
-      serializedAppState = engineCfg.source.serializedAppState;
-    } else if (engineCfg.source.type === 'HTTP_RPC') {
+    if (traceSource.type === 'FILE') {
+      traceStream = new TraceFileStream(traceSource.file);
+    } else if (traceSource.type === 'ARRAY_BUFFER') {
+      traceStream = new TraceBufferStream(traceSource.buffer);
+    } else if (traceSource.type === 'URL') {
+      traceStream = new TraceHttpStream(traceSource.url);
+      serializedAppState = traceSource.serializedAppState;
+    } else if (traceSource.type === 'HTTP_RPC') {
       traceStream = undefined;
     } else {
-      throw new Error(`Unknown source: ${JSON.stringify(engineCfg.source)}`);
+      throw new Error(`Unknown source: ${JSON.stringify(traceSource)}`);
     }
 
     // |traceStream| can be undefined in the case when we are using the external
@@ -262,7 +277,7 @@
       const tStart = performance.now();
       for (;;) {
         const res = await traceStream.readChunk();
-        await this.engine.parse(res.data);
+        await engine.parse(res.data);
         const elapsed = (performance.now() - tStart) / 1000;
         let status = 'Loading trace ';
         if (res.bytesTotal > 0) {
@@ -272,20 +287,20 @@
           status += `${Math.round(res.bytesRead / 1e6)} MB`;
         }
         status += ` - ${Math.ceil(res.bytesRead / elapsed / 1e6)} MB/s`;
-        this.updateStatus(status);
+        updateStatus(status);
         if (res.eof) break;
       }
-      await this.engine.notifyEof();
+      await engine.notifyEof();
     } else {
-      assertTrue(this.engine instanceof HttpRpcEngine);
-      await this.engine.restoreInitialTables();
+      assertTrue(engine instanceof HttpRpcEngine);
+      await engine.restoreInitialTables();
     }
     for (const p of globals.extraSqlPackages) {
-      await this.engine.registerSqlModules(p);
+      await engine.registerSqlModules(p);
     }
 
-    const traceDetails = await getTraceInfo(this.engine, engineCfg);
-    const trace = TraceImpl.newInstance(this.engine, traceDetails);
+    const traceDetails = await getTraceInfo(engine, traceSource);
+    const trace = TraceImpl.newInstance(engine, traceDetails);
     await globals.onTraceLoad(trace);
 
     AppImpl.instance.omnibox.reset();
@@ -294,17 +309,17 @@
       traceDetails.start,
       traceDetails.end,
       trace.traceInfo.traceType === 'json',
-      this.engine,
+      engine,
     );
 
-    globals.timeline.updateVisibleTime(visibleTimeSpan);
+    trace.timeline.updateVisibleTime(visibleTimeSpan);
 
     const cacheUuid = traceDetails.cached ? traceDetails.uuid : '';
     Router.navigate(`#!/viewer?local_cache_key=${cacheUuid}`);
 
     // Make sure the helper views are available before we start adding tracks.
-    await this.initialiseHelperViews();
-    await this.includeSummaryTables();
+    await initialiseHelperViews(engine);
+    await includeSummaryTables(engine);
 
     await defineMaxLayoutDepthSqlFunction(engine);
 
@@ -313,15 +328,17 @@
     }
 
     await AppImpl.instance.plugins.onTraceLoad(trace, (id) => {
-      this.updateStatus(`Running plugin: ${id}`);
+      updateStatus(`Running plugin: ${id}`);
     });
 
-    await this.listTracks();
+    updateStatus('Loading tracks');
+    await decideTracks(trace);
 
-    this.decideTabs();
+    decideTabs(trace);
 
-    await this.listThreads();
-    await this.loadTimelineOverview(
+    await listThreads(engine);
+    await loadTimelineOverview(
+      engine,
       new TimeSpan(traceDetails.start, traceDetails.end),
     );
 
@@ -340,12 +357,13 @@
     const pendingDeeplink = globals.state.pendingDeeplink;
     if (pendingDeeplink !== undefined) {
       globals.dispatch(Actions.clearPendingDeeplink({}));
-      await this.selectPendingDeeplink(pendingDeeplink);
+      await selectPendingDeeplink(trace, pendingDeeplink);
       if (
         pendingDeeplink.visStart !== undefined &&
         pendingDeeplink.visEnd !== undefined
       ) {
-        this.zoomPendingDeeplink(
+        zoomPendingDeeplink(
+          trace,
           pendingDeeplink.visStart,
           pendingDeeplink.visEnd,
         );
@@ -366,7 +384,7 @@
     ) {
       const reliableRangeStart = await computeTraceReliableRangeStart(engine);
       if (reliableRangeStart > 0) {
-        globals.noteManager.addNote({
+        trace.notes.addNote({
           timestamp: reliableRangeStart,
           color: '#ff0000',
           text: 'Reliable Range Start',
@@ -382,12 +400,15 @@
       deserializeAppStatePhase2(serializedAppState);
     }
 
-    await AppImpl.instance.plugins.onTraceReady();
+    await trace.plugins.onTraceReady();
 
-    return engineMode;
+    return trace;
   }
 
-  private async selectPendingDeeplink(link: PendingDeeplinkState) {
+  async function selectPendingDeeplink(
+    trace: TraceImpl,
+    link: PendingDeeplinkState,
+  ) {
     const conditions = [];
     const {ts, dur} = link;
 
@@ -411,7 +432,7 @@
       where ${conditions.join(' and ')}
     ;`;
 
-    const result = await assertExists(this.engine).query(query);
+    const result = await trace.engine.query(query);
     if (result.numRows() > 0) {
       const row = result.firstRow({
         id: NUM,
@@ -420,35 +441,29 @@
       });
 
       const id = row.traceProcessorTrackId;
-      const track = globals.workspace.flatTracks.find(
+      const track = trace.workspace.flatTracks.find(
         (t) =>
-          t.uri &&
-          globals.trackManager.getTrack(t.uri)?.tags?.trackIds?.includes(id),
+          t.uri && trace.tracks.getTrack(t.uri)?.tags?.trackIds?.includes(id),
       );
       if (track === undefined) {
         return;
       }
-      globals.selectionManager.selectSqlEvent('slice', row.id, {
+      trace.selection.selectSqlEvent('slice', row.id, {
         scrollToSelection: true,
         switchToCurrentSelectionTab: false,
       });
     }
   }
 
-  private async listTracks() {
-    this.updateStatus('Loading tracks');
-    await decideTracks();
-  }
-
-  // Show the list of default tabs, but don't make them active!
-  private decideTabs() {
-    for (const tabUri of globals.tabManager.defaultTabs) {
-      globals.tabManager.showTab(tabUri);
+  function decideTabs(trace: TraceImpl) {
+    // Show the list of default tabs, but don't make them active!
+    for (const tabUri of trace.tabs.defaultTabs) {
+      trace.tabs.showTab(tabUri);
     }
   }
 
-  private async listThreads() {
-    this.updateStatus('Reading thread list');
+  async function listThreads(engine: Engine) {
+    updateStatus('Reading thread list');
     const query = `select
         utid,
         tid,
@@ -461,7 +476,7 @@
         from (select * from thread order by upid) as thread
         left join (select * from process order by upid) as process
         using(upid)`;
-    const result = await assertExists(this.engine).query(query);
+    const result = await engine.query(query);
     const threads: ThreadDesc[] = [];
     const it = result.iter({
       utid: NUM,
@@ -483,9 +498,8 @@
     publishThreads(threads);
   }
 
-  private async loadTimelineOverview(trace: TimeSpan) {
+  async function loadTimelineOverview(engine: Engine, trace: TimeSpan) {
     clearOverviewData();
-    const engine = assertExists<Engine>(this.engine);
     const stepSize = Duration.max(1n, trace.duration / 100n);
     const hasSchedSql = 'select ts from sched limit 1';
     const hasSchedOverview = (await engine.query(hasSchedSql)).numRows() > 0;
@@ -498,7 +512,7 @@
       ) {
         const progress = start - trace.start;
         const ratio = Number(progress) / Number(trace.duration);
-        this.updateStatus('Loading overview ' + `${Math.round(ratio * 100)}%`);
+        updateStatus('Loading overview ' + `${Math.round(ratio * 100)}%`);
         const end = Time.add(start, stepSize);
         // The (async() => {})() queues all the 100 async promises in one batch.
         // Without that, we would wait for each step to be rendered before
@@ -528,21 +542,21 @@
 
     // Slices overview.
     const sliceResult = await engine.query(`select
-           bucket,
-           upid,
-           ifnull(sum(utid_sum) / cast(${stepSize} as float), 0) as load
-         from thread
-         inner join (
-           select
-             ifnull(cast((ts - ${trace.start})/${stepSize} as int), 0) as bucket,
-             sum(dur) as utid_sum,
-             utid
-           from slice
-           inner join thread_track on slice.track_id = thread_track.id
-           group by bucket, utid
-         ) using(utid)
-         where upid is not null
-         group by bucket, upid`);
+            bucket,
+            upid,
+            ifnull(sum(utid_sum) / cast(${stepSize} as float), 0) as load
+          from thread
+          inner join (
+            select
+              ifnull(cast((ts - ${trace.start})/${stepSize} as int), 0) as bucket,
+              sum(dur) as utid_sum,
+              utid
+            from slice
+            inner join thread_track on slice.track_id = thread_track.id
+            group by bucket, utid
+          ) using(utid)
+          where upid is not null
+          group by bucket, upid`);
 
     const slicesData: {[key: string]: QuantizedLoad[]} = {};
     const it = sliceResult.iter({bucket: LONG, upid: NUM, load: NUM});
@@ -564,10 +578,8 @@
     publishOverviewData(slicesData);
   }
 
-  async initialiseHelperViews() {
-    const engine = assertExists(this.engine);
-
-    this.updateStatus('Creating annotation counter track table');
+  async function initialiseHelperViews(engine: Engine) {
+    updateStatus('Creating annotation counter track table');
     // Create the helper tables for all the annotations related data.
     // NULL in min/max means "figure it out per track in the usual way".
     await engine.query(`
@@ -580,7 +592,7 @@
         max_value DOUBLE
       );
     `);
-    this.updateStatus('Creating annotation slice track table');
+    updateStatus('Creating annotation slice track table');
     await engine.query(`
       CREATE TABLE annotation_slice_track(
         id INTEGER PRIMARY KEY,
@@ -591,7 +603,7 @@
       );
     `);
 
-    this.updateStatus('Creating annotation counter table');
+    updateStatus('Creating annotation counter table');
     await engine.query(`
       CREATE TABLE annotation_counter(
         id BIGINT,
@@ -601,7 +613,7 @@
         PRIMARY KEY (track_id, ts)
       ) WITHOUT ROWID;
     `);
-    this.updateStatus('Creating annotation slice table');
+    updateStatus('Creating annotation slice table');
     await engine.query(`
       CREATE TABLE annotation_slice(
         id INTEGER PRIMARY KEY,
@@ -628,7 +640,7 @@
         continue;
       }
 
-      this.updateStatus(`Computing ${metric} metric`);
+      updateStatus(`Computing ${metric} metric`);
       try {
         // We don't care about the actual result of metric here as we are just
         // interested in the annotation tracks.
@@ -642,7 +654,7 @@
         }
       }
 
-      this.updateStatus(`Inserting data for ${metric} metric`);
+      updateStatus(`Inserting data for ${metric} metric`);
       try {
         const result = await engine.query(`pragma table_info(${metric}_event)`);
         let hasSliceName = false;
@@ -739,26 +751,24 @@
     }
   }
 
-  async includeSummaryTables() {
-    const engine = assertExists<Engine>(this.engine);
-
-    this.updateStatus('Creating slice summaries');
+  async function includeSummaryTables(engine: Engine) {
+    updateStatus('Creating slice summaries');
     await engine.query(`include perfetto module viz.summary.slices;`);
 
-    this.updateStatus('Creating counter summaries');
+    updateStatus('Creating counter summaries');
     await engine.query(`include perfetto module viz.summary.counters;`);
 
-    this.updateStatus('Creating thread summaries');
+    updateStatus('Creating thread summaries');
     await engine.query(`include perfetto module viz.summary.threads;`);
 
-    this.updateStatus('Creating processes summaries');
+    updateStatus('Creating processes summaries');
     await engine.query(`include perfetto module viz.summary.processes;`);
 
-    this.updateStatus('Creating track summaries');
+    updateStatus('Creating track summaries');
     await engine.query(`include perfetto module viz.summary.tracks;`);
   }
 
-  private updateStatus(msg: string): void {
+  function updateStatus(msg: string): void {
     globals.dispatch(
       Actions.updateStatus({
         msg,
@@ -767,24 +777,26 @@
     );
   }
 
-  private zoomPendingDeeplink(visStart: string, visEnd: string) {
+  function zoomPendingDeeplink(
+    trace: TraceImpl,
+    visStart: string,
+    visEnd: string,
+  ) {
     const visualStart = Time.fromRaw(BigInt(visStart));
     const visualEnd = Time.fromRaw(BigInt(visEnd));
-    const traceContext = globals.traceContext;
 
     if (
       !(
         visualStart < visualEnd &&
-        traceContext.start <= visualStart &&
-        visualEnd <= traceContext.end
+        trace.traceInfo.start <= visualStart &&
+        visualEnd <= trace.traceInfo.end
       )
     ) {
       return;
     }
 
-    globals.timeline.updateVisibleTime(new TimeSpan(visualStart, visualEnd));
+    trace.timeline.updateVisibleTime(new TimeSpan(visualStart, visualEnd));
   }
-}
 
 async function computeFtraceBounds(engine: Engine): Promise<TimeSpan | null> {
   const result = await engine.query(`
@@ -841,7 +853,7 @@
 
 async function getTraceInfo(
   engine: Engine,
-  engineCfg: EngineConfig,
+  traceSource: TraceSource,
 ): Promise<TraceInfo> {
   const traceTime = await getTraceTimeBounds(engine);
 
@@ -910,23 +922,21 @@
 
   let traceTitle = '';
   let traceUrl = '';
-  switch (engineCfg.source.type) {
+  switch (traceSource.type) {
     case 'FILE':
       // Split on both \ and / (because C:\Windows\paths\are\like\this).
-      traceTitle = engineCfg.source.file.name.split(/[/\\]/).pop()!;
-      const fileSizeMB = Math.ceil(engineCfg.source.file.size / 1e6);
+      traceTitle = traceSource.file.name.split(/[/\\]/).pop()!;
+      const fileSizeMB = Math.ceil(traceSource.file.size / 1e6);
       traceTitle += ` (${fileSizeMB} MB)`;
       break;
     case 'URL':
-      traceUrl = engineCfg.source.url;
+      traceUrl = traceSource.url;
       traceTitle = traceUrl.split('/').pop()!;
       break;
     case 'ARRAY_BUFFER':
-      traceTitle = engineCfg.source.title;
-      traceUrl = engineCfg.source.url ?? '';
-      const arrayBufferSizeMB = Math.ceil(
-        engineCfg.source.buffer.byteLength / 1e6,
-      );
+      traceTitle = traceSource.title;
+      traceUrl = traceSource.url ?? '';
+      const arrayBufferSizeMB = Math.ceil(traceSource.buffer.byteLength / 1e6);
       traceTitle += ` (${arrayBufferSizeMB} MB)`;
       break;
     case 'HTTP_RPC':
@@ -947,7 +957,7 @@
   // trace_uuid can be missing from the TP tables if the trace is empty or in
   // other similar edge cases.
   const uuid = uuidRes.numRows() > 0 ? uuidRes.firstRow({uuid: STR}).uuid : '';
-  const cached = await cacheTrace(engineCfg.source, uuid);
+  const cached = await cacheTrace(traceSource, uuid);
 
   return {
     ...traceTime,
@@ -959,7 +969,7 @@
     cpus: await getCpus(engine),
     gpuCount: await getNumberOfGpus(engine),
     importErrors: await getTraceErrors(engine),
-    source: engineCfg.source,
+    source: traceSource,
     traceType,
     uuid,
     cached,
diff --git a/ui/src/controller/track_decider.ts b/ui/src/controller/track_decider.ts
index b538917..10c7e41 100644
--- a/ui/src/controller/track_decider.ts
+++ b/ui/src/controller/track_decider.ts
@@ -12,6 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import {TraceImpl} from '../core/trace_impl';
 import {globals} from '../frontend/globals';
 import {TrackNode} from '../public/workspace';
 
@@ -197,7 +198,7 @@
   }
 }
 
-export async function decideTracks(): Promise<void> {
+export async function decideTracks(trace: TraceImpl): Promise<void> {
   groupGlobalIonTracks();
   groupGlobalIostatTracks(F2FS_IOSTAT_TAG, F2FS_IOSTAT_GROUP_NAME);
   groupGlobalIostatTracks(F2FS_IOSTAT_LAT_TAG, F2FS_IOSTAT_LAT_GROUP_NAME);
@@ -216,17 +217,17 @@
   groupMiscNonAllowlistedTracks(MISC_GROUP);
 
   // Move groups underneath tracks
-  Array.from(globals.workspace.children)
+  Array.from(trace.workspace.children)
     .sort((a, b) => {
       // Get the index in the order array
       const indexA = a.hasChildren ? 1 : 0;
       const indexB = b.hasChildren ? 1 : 0;
       return indexA - indexB;
     })
-    .forEach((n) => globals.workspace.addChildLast(n));
+    .forEach((n) => trace.workspace.addChildLast(n));
 
   // If there is only one group, expand it
-  const rootLevelChildren = globals.workspace.children;
+  const rootLevelChildren = trace.workspace.children;
   if (rootLevelChildren.length === 1 && rootLevelChildren[0].hasChildren) {
     rootLevelChildren[0].expand();
   }
diff --git a/ui/src/core/trace_impl.ts b/ui/src/core/trace_impl.ts
index 89707ae..60d1e7d 100644
--- a/ui/src/core/trace_impl.ts
+++ b/ui/src/core/trace_impl.ts
@@ -38,6 +38,7 @@
 import {PivotTableManager} from './pivot_table_manager';
 import {FlowManager} from './flow_manager';
 import {AppContext, AppImpl, CORE_PLUGIN_ID} from './app_impl';
+import {PluginManager} from './plugin_manager';
 
 /**
  * Handles the per-trace state of the UI
@@ -319,6 +320,10 @@
     return this.appImpl.omnibox;
   }
 
+  get plugins(): PluginManager {
+    return this.appImpl.plugins;
+  }
+
   scheduleRedraw(): void {
     this.appImpl.scheduleRedraw();
   }