UI: Make engine source explicit

The next CL will introduce a new mode (for RPC)
and will make the current "sourc" interface too
ambiguous. Making it an explicit enum instead of
relyinig on RTTI.

Bug: 143074239
Test: manually tested permalink, download and conversion manually with both URL and file
Change-Id: I7f6222ce4980250bdd136153b26c9c052d4b688c
diff --git a/ui/src/common/actions.ts b/ui/src/common/actions.ts
index 95092be..215846b 100644
--- a/ui/src/common/actions.ts
+++ b/ui/src/common/actions.ts
@@ -28,6 +28,7 @@
   State,
   Status,
   TargetOs,
+  TraceSource,
   TraceTime,
   TrackState,
   VisibleState,
@@ -75,7 +76,7 @@
     state.engines[id] = {
       id,
       ready: false,
-      source: args.file,
+      source: {type: 'FILE', file: args.file},
     };
     state.route = `/viewer`;
   },
@@ -86,7 +87,7 @@
     state.engines[id] = {
       id,
       ready: false,
-      source: args.buffer,
+      source: {type: 'ARRAY_BUFFER', buffer: args.buffer},
     };
     state.route = `/viewer`;
   },
@@ -97,7 +98,7 @@
     state.engines[id] = {
       id,
       ready: false,
-      source: args.url,
+      source: {type: 'URL', url: args.url},
     };
     state.route = `/viewer`;
   },
@@ -113,12 +114,9 @@
     ConvertTrace(args.file, args.truncate);
   },
 
-  convertTraceToPprof(_: StateDraft, args: {
-    pid: number,
-    src: string|File|ArrayBuffer,
-    ts1: number,
-    ts2?: number
-  }): void {
+  convertTraceToPprof(
+      _: StateDraft,
+      args: {pid: number, src: TraceSource, ts1: number, ts2?: number}): void {
     ConvertTraceToPprof(args.pid, args.src, args.ts1, args.ts2);
   },
 
diff --git a/ui/src/common/actions_unittest.ts b/ui/src/common/actions_unittest.ts
index 4214c19..686da55 100644
--- a/ui/src/common/actions_unittest.ts
+++ b/ui/src/common/actions_unittest.ts
@@ -19,7 +19,8 @@
   createEmptyState,
   SCROLLING_TRACK_GROUP,
   State,
-  TrackState
+  TraceUrlSource,
+  TrackState,
 } from './state';
 
 function fakeTrack(state: State, id: string): TrackState {
@@ -248,7 +249,8 @@
   const engineKeys = Object.keys(after.engines);
   expect(after.nextId).toBe(101);
   expect(engineKeys.length).toBe(1);
-  expect(after.engines[engineKeys[0]].source).toBe('https://example.com/bar');
+  expect((after.engines[engineKeys[0]].source as TraceUrlSource).url)
+      .toBe('https://example.com/bar');
   expect(after.route).toBe('/viewer');
   expect(after.recordConfig).toBe(recordConfig);
 });
@@ -277,7 +279,8 @@
 
   const engineKeys = Object.keys(thrice.engines);
   expect(engineKeys.length).toBe(1);
-  expect(thrice.engines[engineKeys[0]].source).toBe('https://example.com/foo');
+  expect((thrice.engines[engineKeys[0]].source as TraceUrlSource).url)
+      .toBe('https://example.com/foo');
   expect(thrice.pinnedTracks.length).toBe(0);
   expect(thrice.scrollingTracks.length).toBe(0);
   expect(thrice.route).toBe('/viewer');
diff --git a/ui/src/common/engine.ts b/ui/src/common/engine.ts
index d4a6b45..d9f6760 100644
--- a/ui/src/common/engine.ts
+++ b/ui/src/common/engine.ts
@@ -53,6 +53,12 @@
    */
   abstract notifyEof(): void;
 
+  /**
+   * Resets the trace processor state by destroying any table/views created by
+   * the UI after loading.
+   */
+  abstract restoreInitialTables(): void;
+
   /*
    * Performs a SQL query and retruns a proto-encoded RawQueryResult object.
    */
diff --git a/ui/src/common/state.ts b/ui/src/common/state.ts
index b35aa91..5d6be6a 100644
--- a/ui/src/common/state.ts
+++ b/ui/src/common/state.ts
@@ -36,6 +36,23 @@
 
 export const SCROLLING_TRACK_GROUP = 'ScrollingTracks';
 
+export interface TraceFileSource {
+  type: 'FILE';
+  file: File;
+}
+
+export interface TraceArrayBufferSource {
+  type: 'ARRAY_BUFFER';
+  buffer: ArrayBuffer;
+}
+
+export interface TraceUrlSource {
+  type: 'URL';
+  url: string;
+}
+
+export type TraceSource = TraceFileSource|TraceArrayBufferSource|TraceUrlSource;
+
 export interface TrackState {
   id: string;
   engineId: string;
@@ -57,7 +74,7 @@
 export interface EngineConfig {
   id: string;
   ready: boolean;
-  source: string|File|ArrayBuffer;
+  source: TraceSource;
 }
 
 export interface QueryConfig {
diff --git a/ui/src/common/wasm_engine_proxy.ts b/ui/src/common/wasm_engine_proxy.ts
index e5ff09b..b09c662 100644
--- a/ui/src/common/wasm_engine_proxy.ts
+++ b/ui/src/common/wasm_engine_proxy.ts
@@ -99,6 +99,12 @@
     await this.queueRequest('trace_processor_notify_eof', new Uint8Array());
   }
 
+  restoreInitialTables(): Promise<void> {
+    // We should never get here, restoreInitialTables() should be called only
+    // when using the HttpRpcEngine.
+    throw new Error('restoreInitialTables() not supported by the WASM engine');
+  }
+
   rawQuery(rawQueryArgs: Uint8Array): Promise<Uint8Array> {
     return this.queueRequest('trace_processor_raw_query', rawQueryArgs);
   }
diff --git a/ui/src/controller/permalink_controller.ts b/ui/src/controller/permalink_controller.ts
index 7bafa23..d991a89 100644
--- a/ui/src/controller/permalink_controller.ts
+++ b/ui/src/controller/permalink_controller.ts
@@ -12,22 +12,18 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {Draft, produce} from 'immer';
+import {produce} from 'immer';
 import * as uuidv4 from 'uuid/v4';
 
-import {assertExists} from '../base/logging';
+import {assertExists, assertTrue} from '../base/logging';
 import {Actions} from '../common/actions';
-import {EngineConfig, State} from '../common/state';
+import {State} from '../common/state';
 
 import {Controller} from './controller';
 import {globals} from './globals';
 
 export const BUCKET_NAME = 'perfetto-ui-data';
 
-function needsToBeUploaded(obj: {}): obj is ArrayBuffer|File {
-  return obj instanceof ArrayBuffer || obj instanceof File;
-}
-
 export class PermalinkController extends Controller<'main'> {
   private lastRequestId?: string;
   constructor() {
@@ -58,29 +54,30 @@
   }
 
   private static async createPermalink() {
-    const state = globals.state;
-
-    // Upload each loaded trace.
-    const fileToUrl = new Map<File|ArrayBuffer, string>();
-    for (const engine of Object.values<EngineConfig>(state.engines)) {
-      if (!needsToBeUploaded(engine.source)) continue;
-      const name = engine.source instanceof File ? engine.source.name :
-                                                   `trace ${engine.id}`;
-      PermalinkController.updateStatus(`Uploading ${name}`);
-      const url = await this.saveTrace(engine.source);
-      fileToUrl.set(engine.source, url);
+    const engines = Object.values(globals.state.engines);
+    assertTrue(engines.length === 1);
+    const engine = engines[0];
+    let dataToUpload: File|ArrayBuffer|undefined = undefined;
+    let traceName = `trace ${engine.id}`;
+    if (engine.source.type === 'FILE') {
+      dataToUpload = engine.source.file;
+      traceName = dataToUpload.name;
+    } else if (engine.source.type === 'ARRAY_BUFFER') {
+      dataToUpload = engine.source.buffer;
+    } else if (engine.source.type !== 'URL') {
+      throw new Error(`Cannot share trace ${JSON.stringify(engine.source)}`);
     }
 
-    // Convert state to use URLs and remove permalink.
-    const uploadState = produce(state, draft => {
-      for (const engine of Object.values<Draft<EngineConfig>>(
-               draft.engines)) {
-        if (!needsToBeUploaded(engine.source)) continue;
-        const newSource = fileToUrl.get(engine.source);
-        if (newSource) engine.source = newSource;
-      }
-      draft.permalink = {};
-    });
+    let uploadState = globals.state;
+    if (dataToUpload !== undefined) {
+      PermalinkController.updateStatus(`Uploading ${traceName}`);
+      const url = await this.saveTrace(dataToUpload);
+      // Convert state to use URLs and remove permalink.
+      uploadState = produce(globals.state, draft => {
+        draft.engines[engine.id].source = {type: 'URL', url};
+        draft.permalink = {};
+      });
+    }
 
     // Upload state.
     PermalinkController.updateStatus(`Creating permalink...`);
diff --git a/ui/src/controller/trace_controller.ts b/ui/src/controller/trace_controller.ts
index ec4209f..7dab1ab 100644
--- a/ui/src/controller/trace_controller.ts
+++ b/ui/src/controller/trace_controller.ts
@@ -178,14 +178,15 @@
         createWasmEngine(this.engineId),
         LoadingManager.getInstance);
     let traceStream: TraceStream;
-    if (engineCfg.source instanceof File) {
-      traceStream = new TraceFileStream(engineCfg.source);
-    } else if (engineCfg.source instanceof ArrayBuffer) {
-      traceStream = new TraceBufferStream(engineCfg.source);
+    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);
     } else {
-      traceStream = new TraceHttpStream(engineCfg.source);
+      throw new Error(`Unknown source: ${JSON.stringify(engineCfg.source)}`);
     }
-
     const tStart = performance.now();
     for (;;) {
       const res = await traceStream.readChunk();
diff --git a/ui/src/controller/trace_converter.ts b/ui/src/controller/trace_converter.ts
index e4648a0..2a5b787 100644
--- a/ui/src/controller/trace_converter.ts
+++ b/ui/src/controller/trace_converter.ts
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 import {Actions} from '../common/actions';
+import {TraceSource} from '../common/state';
 import * as trace_to_text from '../gen/trace_to_text';
 
 import {globals} from './globals';
@@ -54,7 +55,7 @@
 }
 
 export async function ConvertTraceToPprof(
-    pid: number, src: string|File|ArrayBuffer, ts1: number, ts2?: number) {
+    pid: number, src: TraceSource, ts1: number, ts2?: number) {
   generateBlob(src).then(result => {
     const mod = trace_to_text({
       noInitialRun: true,
@@ -101,18 +102,20 @@
   });
 }
 
-async function generateBlob(src: string|ArrayBuffer|File) {
+async function generateBlob(src: TraceSource) {
   let blob: Blob = new Blob();
-  if (typeof src === 'string') {
-    const resp = await fetch(src);
+  if (src.type === 'URL') {
+    const resp = await fetch(src.url);
     if (resp.status !== 200) {
       throw new Error(`fetch() failed with HTTP error ${resp.status}`);
     }
     blob = await resp.blob();
-  } else if (src instanceof ArrayBuffer) {
-    blob = new Blob([new Uint8Array(src, 0, src.byteLength)]);
+  } else if (src.type === 'ARRAY_BUFFER') {
+    blob = new Blob([new Uint8Array(src.buffer, 0, src.buffer.byteLength)]);
+  } else if (src.type === 'FILE') {
+    blob = src.file;
   } else {
-    blob = src;
+    throw new Error(`Conversion not supported for ${JSON.stringify(src)}`);
   }
   return blob;
 }
diff --git a/ui/src/frontend/sidebar.ts b/ui/src/frontend/sidebar.ts
index 5db490d..350a17e 100644
--- a/ui/src/frontend/sidebar.ts
+++ b/ui/src/frontend/sidebar.ts
@@ -137,13 +137,13 @@
         t: 'Share',
         a: dispatchCreatePermalink,
         i: 'share',
-        disableInLocalOnlyMode: true,
+        checkDownloadDisabled: true,
       },
       {
         t: 'Download',
         a: downloadTrace,
         i: 'file_download',
-        disableInLocalOnlyMode: true,
+        checkDownloadDisabled: true,
       },
       {t: 'Legacy UI', a: openCurrentTraceWithOldUI, i: 'filter_none'},
     ],
@@ -262,15 +262,14 @@
   if (!isTraceLoaded) return;
   const engine = Object.values(globals.state.engines)[0];
   const src = engine.source;
-  if (src instanceof ArrayBuffer) {
-    openInOldUIWithSizeCheck(new Blob([src]));
-  } else if (src instanceof File) {
-    openInOldUIWithSizeCheck(src);
+  if (src.type === 'ARRAY_BUFFER') {
+    openInOldUIWithSizeCheck(new Blob([src.buffer]));
+  } else if (src.type === 'FILE') {
+    openInOldUIWithSizeCheck(src.file);
   } else {
-    console.assert(typeof src === 'string');
-    console.error('Loading from an URL to catapult is not yet supported');
+    throw new Error('Loading from a URL to catapult is not yet supported');
     // TODO(nicomazz): Find how to get the data of the current trace if it is
-    // from an URL. It seems that the trace downloaded is given to the trace
+    // from a URL. It seems that the trace downloaded is given to the trace
     // processor, but not kept somewhere accessible. Maybe the only way is to
     // download the trace (again), and then open it. An alternative can be to
     // save a copy.
@@ -412,13 +411,14 @@
   globals.dispatch(Actions.navigate({route: '/viewer'}));
 }
 
-function localOnlyMode(): boolean {
-  return globals.frontendLocalState.localOnlyMode;
+function isDownloadAndShareDisabled(): boolean {
+  if (globals.frontendLocalState.localOnlyMode) return true;
+  return false;
 }
 
 function dispatchCreatePermalink(e: Event) {
   e.preventDefault();
-  if (localOnlyMode() || !isTraceLoaded()) return;
+  if (isDownloadAndShareDisabled() || !isTraceLoaded()) return;
 
   const result = confirm(
       `Upload the trace and generate a permalink. ` +
@@ -428,27 +428,30 @@
 
 function downloadTrace(e: Event) {
   e.preventDefault();
-  if (!isTraceLoaded() || localOnlyMode()) return;
+  if (!isTraceLoaded() || isDownloadAndShareDisabled()) return;
 
   const engine = Object.values(globals.state.engines)[0];
   if (!engine) return;
-  const src = engine.source;
-  if (typeof src === 'string') {
-    window.open(src);
-    return;
-  }
-
   let url = '';
-  if (src instanceof ArrayBuffer) {
-    const blob = new Blob([src], {type: 'application/octet-stream'});
+  let fileName = 'trace.pftrace';
+  const src = engine.source;
+  if (src.type === 'URL') {
+    url = src.url;
+    fileName = url.split('/').slice(-1)[0];
+  } else if (src.type === 'ARRAY_BUFFER') {
+    const blob = new Blob([src.buffer], {type: 'application/octet-stream'});
     url = URL.createObjectURL(blob);
+  } else if (src.type === 'FILE') {
+    const file = src.file;
+    url = URL.createObjectURL(file);
+    fileName = file.name;
   } else {
-    console.assert(src instanceof File);
-    url = URL.createObjectURL(src);
+    throw new Error(`Download from ${JSON.stringify(src)} is not supported`);
   }
 
   const a = document.createElement('a');
   a.href = url;
+  a.download = fileName;
   document.body.appendChild(a);
   a.click();
   document.body.removeChild(a);
@@ -491,8 +494,8 @@
           href: typeof item.a === 'string' ? item.a : '#',
           disabled: false,
         };
-        if (globals.frontendLocalState.localOnlyMode &&
-            item.hasOwnProperty('disableInLocalOnlyMode')) {
+        if (isDownloadAndShareDisabled() &&
+            item.hasOwnProperty('checkDownloadDisabled')) {
           attrs = {
             onclick: () => alert('Can not download or share external trace.'),
             href: '#',