UI: seamlessly migrate to catapult UI for legacy traces

- Add UI code for loading traces in catapult, specifically:
  A) For legacy JSON traces, pass them directly to the old UI.
  B) For newer perfetto traces, convert them to JSON invoking
     trace_to_text into the controller worker and then pass
     them back to the main thread and pass the over to the old
     Catapult UI.
- Build trace_to_text as a WASM module.
- Tweak WASM build files to support >1 wasm targets.
- Add a script to roll Catapult's trace viewer into
  GCS and build it as part of assets.
- Fix some bugs in trace_to_text when building for a
  32-bit target.

Bug: 117977226
Change-Id: I5dced437677b8b978707de5bcef6b02785f32786
diff --git a/buildtools/.gitignore b/buildtools/.gitignore
index 733861d..36d52c4 100644
--- a/buildtools/.gitignore
+++ b/buildtools/.gitignore
@@ -3,6 +3,7 @@
 aosp-*/
 benchmark/
 bionic/
+catapult_trace_viewer/
 clang/
 clang_format/
 d8/
diff --git a/gn/BUILD.gn b/gn/BUILD.gn
index ee14601..bb2b04b 100644
--- a/gn/BUILD.gn
+++ b/gn/BUILD.gn
@@ -78,13 +78,12 @@
 }
 
 group("protobuf_full_deps") {
-  testonly = true
-
   if (!build_with_chromium) {
     public_deps = [
       "//buildtools:protobuf_full",
     ]
   } else {
+    testonly = true
     public_deps = [
       "//third_party/protobuf:protobuf_full",
     ]
diff --git a/gn/standalone/wasm.gni b/gn/standalone/wasm.gni
index fa6f58b..3b26f58 100644
--- a/gn/standalone/wasm.gni
+++ b/gn/standalone/wasm.gni
@@ -41,15 +41,10 @@
   _lib_name = invoker.name
 
   if (is_wasm) {
-    assert(defined(invoker.sources))
     _target_ldflags = [
       "-s",
-      "MODULARIZE=1",
-      "-s",
       "WASM=1",
       "-s",
-      "NO_FILESYSTEM=1",
-      "-s",
       "DISABLE_EXCEPTION_CATCHING=1",
       "-s",
       "NO_DYNAMIC_EXECUTION=1",
@@ -64,7 +59,22 @@
       "-s",
       "EXPORT_FUNCTION_TABLES=1",
       "-s",
-      "EXTRA_EXPORTED_RUNTIME_METHODS=['ccall', 'cwrap', 'addFunction']",
+      "EXTRA_EXPORTED_RUNTIME_METHODS=['ccall', 'cwrap', 'addFunction', 'FS']",
+
+      # This forces the MEMFS filesystem library to always use typed arrays
+      # instead of building strings/arrays when appending to a file. This allows
+      # to deal with pseudo-files larger than 128 MB when calling trace_to_text.
+      "-s",
+      "MEMFS_APPEND_TO_TYPED_ARRAYS=1",
+
+      # Reduces global namespace pollution.
+      "-s",
+      "MODULARIZE=1",
+
+      # This is to prevent that two different wasm modules end up generating
+      # JS that overrides the same global variable (var Module = ...)
+      "-s",
+      "EXPORT_NAME=${target_name}",
     ]
     if (is_debug) {
       _target_ldflags += [ "-g4" ]
diff --git a/gn/standalone/wasm_typescript_declaration.d.ts b/gn/standalone/wasm_typescript_declaration.d.ts
index f16fbd8..4ac92e9 100644
--- a/gn/standalone/wasm_typescript_declaration.d.ts
+++ b/gn/standalone/wasm_typescript_declaration.d.ts
@@ -22,7 +22,30 @@
     (_: ModuleArgs): Module;
   }
 
+  export interface FileSystemType {}
+
+  export interface FileSystemTypes {
+    MEMFS: FileSystemType;
+    IDBFS: FileSystemType;
+    WORKERFS: FileSystemType;
+  }
+
+  export interface FileSystemNode {
+    contents: Uint8Array;
+    usedBytes: number;
+  }
+
+  export interface FileSystem {
+    mkdir(path: string, mode?: number): any;
+    mount(type: Wasm.FileSystemType, opts: any, mountpoint: string): any;
+    unmount(mountpoint: string): void;
+    unlink(mountpoint: string): void;
+    lookupPath(path: string): {path: string, node: Wasm.FileSystemNode};
+    filesystems: Wasm.FileSystemTypes;
+  }
+
   export interface Module {
+    callMain(args: string[]): void;
     addFunction(f: any, argTypes: string): void;
     ccall(
         ident: string,
@@ -31,9 +54,11 @@
         args: any[],
     ): void;
     HEAPU8: Uint8Array;
+    FS: FileSystem;
   }
 
   export interface ModuleArgs {
+    noInitialRun?: boolean;
     locateFile(s: string): string;
     print(s: string): void;
     printErr(s: string): void;
diff --git a/gn/wasm.gni b/gn/wasm.gni
index 32dd089..3a0733c 100644
--- a/gn/wasm.gni
+++ b/gn/wasm.gni
@@ -22,7 +22,7 @@
 
   # Create a dummy template to avoid GN warnings in non-standalone builds.
   template("wasm_lib") {
-    source_set("${target_name}_unused") {
+    source_set("${target_name}_${invoker.name}_unused") {
       forward_variables_from(invoker, "*")
     }
   }
diff --git a/tools/install-build-deps b/tools/install-build-deps
index 24a2ce6..bb2fa41 100755
--- a/tools/install-build-deps
+++ b/tools/install-build-deps
@@ -241,6 +241,9 @@
   ),
 ]
 
+# This variable is updated by tools/roll-catapult-trace-viewer.
+CATAPULT_SHA1 = 'ff5d8fd7244680b4d4456c25d5fdc04c76f9ef66'
+
 UI_DEPS = [
   ('buildtools/nodejs.tgz',
    'https://storage.googleapis.com/perfetto/node-v10.3.0-darwin-x64.tar.gz',
@@ -277,6 +280,11 @@
    '1abd630619bb1977ab62095570a113d782a1545d',
    'darwin'
   ),
+  ('buildtools/catapult_trace_viewer.tgz',
+   'https://storage.googleapis.com/perfetto/catapult_trace_viewer-%s.tar.gz' % CATAPULT_SHA1,
+    CATAPULT_SHA1,
+   'all'
+  ),
 ]
 
 ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
diff --git a/tools/roll-catapult-trace-viewer b/tools/roll-catapult-trace-viewer
new file mode 100755
index 0000000..a8234b0
--- /dev/null
+++ b/tools/roll-catapult-trace-viewer
@@ -0,0 +1,64 @@
+#!/bin/bash
+# Copyright (C) 2018 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.
+
+# Builds the current version of catapult, uploads it to GCS and updates the
+# pinned SHA1 in install-build-deps.
+
+set -e
+
+PROJECT_ROOT="$(cd -P ${BASH_SOURCE[0]%/*}/..; pwd)"
+
+if [ "$1" == "" ]; then
+  echo "Usage: $0 /path/to/catapult/repo"
+  exit 1
+fi
+
+CATAPULT="$1"
+if [ ! -d "$CATAPULT/.git" ]; then
+  echo "$CATAPULT must point to a valid catapult repo"
+  exit 1
+fi
+
+REVISION=$(git -C "$CATAPULT" rev-parse --short HEAD)
+OUTDIR="$(mktemp -d)"
+echo "Building vulcanized Trace Viewer @ $REVISION into $OUTDIR"
+git -C "$CATAPULT" log -1 | cat
+echo
+set -x
+"$CATAPULT/tracing/bin/generate_about_tracing_contents" --outdir "$OUTDIR"
+ARCHIVE="$OUTDIR/catapult_trace_viewer.tar.gz"
+
+(
+  cd "$OUTDIR"
+  mv about_tracing.html catapult_trace_viewer.html
+  mv about_tracing.js catapult_trace_viewer.js
+  sed -i '' -e \
+      's|src="tracing.js"|src="/assets/catapult_trace_viewer.js"|g' \
+      catapult_trace_viewer.html
+  tar -zcf "$ARCHIVE" catapult_trace_viewer.{js,html}
+)
+
+SHA1CMD='import hashlib; import sys; sha1=hashlib.sha1(); sha1.update(sys.stdin.read()); print(sha1.hexdigest())'
+SHA1=$(python -c "$SHA1CMD" < "$ARCHIVE")
+GCS_TARGET="gs://perfetto/catapult_trace_viewer-$SHA1.tar.gz"
+gsutil cp -n -a public-read "$ARCHIVE" "$GCS_TARGET"
+rm -rf "$OUTDIR"
+
+# Update the reference to the new prebuilt in tools/install-build-deps.
+sed -i '' -e \
+    "s/^CATAPULT_SHA1 =.*/CATAPULT_SHA1 = '"$SHA1"'/g" \
+     "$PROJECT_ROOT/tools/install-build-deps"
+
+"$PROJECT_ROOT/tools/install-build-deps" --ui --no-android
\ No newline at end of file
diff --git a/tools/trace_to_text/BUILD.gn b/tools/trace_to_text/BUILD.gn
index e606369..4c1d37d 100644
--- a/tools/trace_to_text/BUILD.gn
+++ b/tools/trace_to_text/BUILD.gn
@@ -13,9 +13,9 @@
 # limitations under the License.
 
 import("../../gn/perfetto.gni")
+import("../../gn/wasm.gni")
 
 source_set("lib") {
-  testonly = true
   deps = [
     "../../gn:default_deps",
     "../../gn:protobuf_full_deps",
@@ -36,7 +36,6 @@
 
 if (current_toolchain == host_toolchain) {
   executable("trace_to_text_host") {
-    testonly = true
     deps = [
       ":lib",
       "../../gn:default_deps",
@@ -44,6 +43,14 @@
   }
 }
 
+wasm_lib("trace_to_text_wasm") {
+  name = "trace_to_text"
+  deps = [
+    ":lib",
+    "../../gn:default_deps",
+  ]
+}
+
 # The one for the android tree is defined in the top-level BUILD.gn.
 if (!build_with_android) {
   copy("trace_to_text") {
diff --git a/tools/trace_to_text/ftrace_event_formatter.cc b/tools/trace_to_text/ftrace_event_formatter.cc
index 5e6d835..dd15ce6 100644
--- a/tools/trace_to_text/ftrace_event_formatter.cc
+++ b/tools/trace_to_text/ftrace_event_formatter.cc
@@ -3365,7 +3365,7 @@
 }
 
 std::string FormatPrefix(uint64_t timestamp,
-                         uint64_t cpu,
+                         uint32_t cpu,
                          uint32_t pid,
                          uint32_t tgid,
                          std::string name) {
@@ -3393,7 +3393,7 @@
 
 std::string FormatFtraceEvent(
     uint64_t timestamp,
-    size_t cpu,
+    uint32_t cpu,
     const protos::FtraceEvent& event,
     const std::unordered_map<uint32_t /*tid*/, uint32_t /*tgid*/>& thread_map) {
   // Sched_switch events contain the thread name so use that in the prefix.
diff --git a/tools/trace_to_text/ftrace_event_formatter.h b/tools/trace_to_text/ftrace_event_formatter.h
index 27b0142..593cee2 100644
--- a/tools/trace_to_text/ftrace_event_formatter.h
+++ b/tools/trace_to_text/ftrace_event_formatter.h
@@ -28,7 +28,7 @@
 
 std::string FormatFtraceEvent(
     uint64_t timestamp,
-    size_t cpu,
+    uint32_t cpu,
     const protos::FtraceEvent&,
     const std::unordered_map<uint32_t /*tid*/, uint32_t /*tgid*/>& thread_map);
 
diff --git a/tools/trace_to_text/main.cc b/tools/trace_to_text/main.cc
index 1cdfe98..79bc012 100644
--- a/tools/trace_to_text/main.cc
+++ b/tools/trace_to_text/main.cc
@@ -40,6 +40,7 @@
 #include <google/protobuf/util/field_comparator.h>
 #include <google/protobuf/util/message_differencer.h>
 
+#include "perfetto/base/build_config.h"
 #include "perfetto/base/logging.h"
 #include "perfetto/trace/ftrace/ftrace_stats.pb.h"
 #include "perfetto/trace/trace.pb.h"
@@ -49,9 +50,41 @@
 #include "tools/trace_to_text/ftrace_inode_handler.h"
 #include "tools/trace_to_text/process_formatter.h"
 
+// When running in Web Assembly, fflush() is a no-op and the stdio buffering
+// sends progress updates to JS only when a write ends with \n.
+#if PERFETTO_BUILDFLAG(PERFETTO_OS_WASM)
+#define PROGRESS_CHAR "\n"
+#else
+#define PROGRESS_CHAR "\r"
+#endif
+
 namespace perfetto {
 namespace {
 
+using google::protobuf::Descriptor;
+using google::protobuf::DynamicMessageFactory;
+using google::protobuf::FileDescriptor;
+using google::protobuf::Message;
+using google::protobuf::TextFormat;
+using google::protobuf::compiler::DiskSourceTree;
+using google::protobuf::compiler::Importer;
+using google::protobuf::compiler::MultiFileErrorCollector;
+using google::protobuf::io::OstreamOutputStream;
+
+using protos::FtraceEvent;
+using protos::FtraceEventBundle;
+using protos::InodeFileMap;
+using protos::PrintFtraceEvent;
+using protos::ProcessTree;
+using protos::Trace;
+using protos::TracePacket;
+using protos::FtraceStats;
+using protos::FtraceStats_Phase_START_OF_TRACE;
+using protos::FtraceStats_Phase_END_OF_TRACE;
+using protos::SysStats;
+using Entry = protos::InodeFileMap::Entry;
+using Process = protos::ProcessTree::Process;
+
 // Having an empty traceEvents object is necessary for trace viewer to
 // load the json properly.
 const char kTraceHeader[] = R"({
@@ -87,31 +120,7 @@
     "#           TASK-PID    TGID   CPU#  ||||    TIMESTAMP  FUNCTION\\n"
     "#              | |        |      |   ||||       |         |\\n";
 
-using google::protobuf::Descriptor;
-using google::protobuf::DynamicMessageFactory;
-using google::protobuf::FileDescriptor;
-using google::protobuf::Message;
-using google::protobuf::TextFormat;
-using google::protobuf::compiler::DiskSourceTree;
-using google::protobuf::compiler::Importer;
-using google::protobuf::compiler::MultiFileErrorCollector;
-using google::protobuf::io::OstreamOutputStream;
-
-using protos::FtraceEvent;
-using protos::FtraceEventBundle;
-using protos::InodeFileMap;
-using protos::PrintFtraceEvent;
-using protos::ProcessTree;
-using protos::Trace;
-using protos::TracePacket;
-using protos::FtraceStats;
-using protos::FtraceStats_Phase_START_OF_TRACE;
-using protos::FtraceStats_Phase_END_OF_TRACE;
-using protos::SysStats;
-using Entry = protos::InodeFileMap::Entry;
-using Process = protos::ProcessTree::Process;
-
-// TODO(hjd): Add tests.
+bool g_output_is_tty = false;
 
 size_t GetWidth() {
   if (!isatty(STDOUT_FILENO))
@@ -148,9 +157,12 @@
   // that a trace is merely a sequence of TracePackets. Here we just manually
   // tokenize the repeated TracePacket messages and parse them individually
   // using libprotobuf.
-  for (;;) {
-    fprintf(stderr, "Processing trace: %8zu KB\r", bytes_processed / 1024);
-    fflush(stderr);
+  for (uint32_t i = 0;; i++) {
+    if ((i & 0x3f) == 0) {
+      fprintf(stderr, "Processing trace: %8zu KB" PROGRESS_CHAR,
+              bytes_processed / 1024);
+      fflush(stderr);
+    }
     // A TracePacket consists in one byte stating its field id and type ...
     char preamble;
     input->get(preamble);
@@ -285,11 +297,14 @@
   size_t total_events = ftrace_sorted.size();
   size_t written_events = 0;
   for (auto it = ftrace_sorted.begin(); it != ftrace_sorted.end(); it++) {
-    *output << it->second << (wrap_in_json ? "\\n" : "\n");
-    if (written_events++ % 100 == 0 && !isatty(STDOUT_FILENO)) {
-      fprintf(stderr, "Writing trace: %.2f %%\r",
+    *output << it->second;
+    *output << (wrap_in_json ? "\\n" : "\n");
+    if (!g_output_is_tty && (written_events++ % 1000 == 0 ||
+                             written_events == ftrace_sorted.size())) {
+      fprintf(stderr, "Writing trace: %.2f %%" PROGRESS_CHAR,
               written_events * 100.0 / total_events);
       fflush(stderr);
+      output->flush();
     }
   }
 
@@ -707,8 +722,8 @@
 
 int Usage(const char* argv0) {
   printf(
-      "Usage: %s [systrace|json|text|summary|short_summary] < trace.proto > "
-      "trace.txt\n",
+      "Usage: %s systrace|json|text|summary|short_summary [trace.proto] "
+      "[trace.txt]\n",
       argv0);
   return 1;
 }
@@ -716,25 +731,56 @@
 }  // namespace
 
 int main(int argc, char** argv) {
-  if (argc != 2)
+  if (argc < 2)
     return Usage(argv[0]);
 
+  std::istream* input_stream;
+  std::ifstream file_istream;
+  if (argc > 2) {
+    const char* file_path = argv[2];
+    file_istream.open(file_path, std::ios_base::in | std::ios_base::binary);
+    if (!file_istream.is_open())
+      PERFETTO_FATAL("Could not open %s", file_path);
+    input_stream = &file_istream;
+  } else {
+    if (isatty(STDIN_FILENO)) {
+      PERFETTO_ELOG("Reading from stdin but it's connected to a TTY");
+      PERFETTO_LOG("It is unlikely that you want to type in some binary.");
+      PERFETTO_LOG("Either pass a file path to the cmdline or pipe stdin");
+      return Usage(argv[0]);
+    }
+    input_stream = &std::cin;
+  }
+
+  std::ostream* output_stream;
+  std::ofstream file_ostream;
+  if (argc > 3) {
+    const char* file_path = argv[3];
+    file_ostream.open(file_path, std::ios_base::out | std::ios_base::trunc);
+    if (!file_ostream.is_open())
+      PERFETTO_FATAL("Could not open %s", file_path);
+    output_stream = &file_ostream;
+  } else {
+    output_stream = &std::cout;
+    perfetto::g_output_is_tty = isatty(STDOUT_FILENO);
+  }
+
   std::string format(argv[1]);
 
   if (format == "json")
-    return perfetto::TraceToSystrace(&std::cin, &std::cout,
+    return perfetto::TraceToSystrace(input_stream, output_stream,
                                      /*wrap_in_json=*/true);
   if (format == "systrace")
-    return perfetto::TraceToSystrace(&std::cin, &std::cout,
+    return perfetto::TraceToSystrace(input_stream, output_stream,
                                      /*wrap_in_json=*/false);
   if (format == "text")
-    return perfetto::TraceToText(&std::cin, &std::cout);
+    return perfetto::TraceToText(input_stream, output_stream);
 
   if (format == "summary")
-    return perfetto::TraceToSummary(&std::cin, &std::cout,
+    return perfetto::TraceToSummary(input_stream, output_stream,
                                     /* compact_output */ false);
   if (format == "short_summary")
-    return perfetto::TraceToSummary(&std::cin, &std::cout,
+    return perfetto::TraceToSummary(input_stream, output_stream,
                                     /* compact_output */ true);
 
   return Usage(argv[0]);
diff --git a/ui/BUILD.gn b/ui/BUILD.gn
index 43f3337..7439c5f 100644
--- a/ui/BUILD.gn
+++ b/ui/BUILD.gn
@@ -27,6 +27,7 @@
 group("ui") {
   deps = [
     ":assets_dist",
+    ":catapult_dist",
     ":controller_bundle_dist",
     ":engine_bundle_dist",
     ":frontend_bundle_dist",
@@ -325,9 +326,11 @@
 copy("wasm_dist") {
   deps = [
     "//src/trace_processor:trace_processor.wasm($wasm_toolchain)",
+    "//tools/trace_to_text:trace_to_text.wasm($wasm_toolchain)",
   ]
   sources = [
     "$root_build_dir/wasm/trace_processor.wasm",
+    "$root_build_dir/wasm/trace_to_text.wasm",
   ]
   outputs = [
     "$ui_dir/{{source_file_part}}",
@@ -337,23 +340,50 @@
 copy("wasm_gen") {
   deps = [
     ":dist_symlink",
+
+    # trace_processor
     "//src/trace_processor:trace_processor.d.ts($wasm_toolchain)",
     "//src/trace_processor:trace_processor.js($wasm_toolchain)",
     "//src/trace_processor:trace_processor.wasm($wasm_toolchain)",
+
+    # trace_to_text
+    "//tools/trace_to_text:trace_to_text.d.ts($wasm_toolchain)",
+    "//tools/trace_to_text:trace_to_text.js($wasm_toolchain)",
+    "//tools/trace_to_text:trace_to_text.wasm($wasm_toolchain)",
   ]
   sources = [
+    # trace_processor
     "$root_build_dir/wasm/trace_processor.d.ts",
     "$root_build_dir/wasm/trace_processor.js",
     "$root_build_dir/wasm/trace_processor.wasm",
+
+    # trace_to_text
+    "$root_build_dir/wasm/trace_to_text.d.ts",
+    "$root_build_dir/wasm/trace_to_text.js",
+    "$root_build_dir/wasm/trace_to_text.wasm",
   ]
   if (is_debug) {
-    sources += [ "$root_build_dir/wasm/trace_processor.wasm.map" ]
+    sources += [
+      "$root_build_dir/wasm/trace_processor.wasm.map",
+      "$root_build_dir/wasm/trace_to_text.wasm.map",
+    ]
   }
   outputs = [
     "$ui_gen_dir/{{source_file_part}}",
   ]
 }
 
+# Copy over the vulcanized legacy trace viewer.
+copy("catapult_dist") {
+  sources = [
+    "../buildtools/catapult_trace_viewer/catapult_trace_viewer.html",
+    "../buildtools/catapult_trace_viewer/catapult_trace_viewer.js",
+  ]
+  outputs = [
+    "$ui_dir/assets/{{source_file_part}}",
+  ]
+}
+
 # +----------------------------------------------------------------------------+
 # | Node JS: Creates a symlink in the out directory to node_modules.           |
 # +----------------------------------------------------------------------------+
diff --git a/ui/rollup.config.js b/ui/rollup.config.js
index 8c884f1..e63c7be 100644
--- a/ui/rollup.config.js
+++ b/ui/rollup.config.js
@@ -7,14 +7,15 @@
   plugins: [
     nodeResolve({browser: true}),
 
-    // emscripten conditionally executes require('fs') and require('path'),
-    // when running under node, rollup can't find a library named 'fs' or
-    // 'path' so expects these to be present in the global scope (which fails
-    // at runtime). To avoid this we ignore require('fs') and require('path').
+    // emscripten conditionally executes require('fs') (likewise for others),
+    // when running under node. Rollup can't find those libraries so expects
+    // these to be present in the global scope, which then fails at runtime.
+    // To avoid this we ignore require('fs') and the like.
     commonjs({
       ignore: [
         'fs',
         'path',
+        'crypto',
       ]
     }),
 
diff --git a/ui/src/common/actions.ts b/ui/src/common/actions.ts
index 0f8c88b..132c983 100644
--- a/ui/src/common/actions.ts
+++ b/ui/src/common/actions.ts
@@ -15,13 +15,14 @@
 import {DraftObject} from 'immer';
 
 import {assertExists} from '../base/logging';
+import {ConvertTrace} from '../controller/trace_converter';
 
 import {
   defaultTraceTime,
   SCROLLING_TRACK_GROUP,
   State,
   Status,
-  TraceTime
+  TraceTime,
   RecordConfig,
 } from './state';
 
@@ -52,6 +53,10 @@
     state.route = `/viewer`;
   },
 
+  convertTraceToJson(_: StateDraft, args: {file: File}): void {
+    ConvertTrace(args.file);
+  },
+
   openTraceFromUrl(state: StateDraft, args: {url: string}): void {
     clearTraceState(state);
     const id = `${state.nextId++}`;
diff --git a/ui/src/controller/globals.ts b/ui/src/controller/globals.ts
index 8201edc..a48db76 100644
--- a/ui/src/controller/globals.ts
+++ b/ui/src/controller/globals.ts
@@ -31,8 +31,9 @@
 export interface App {
   state: State;
   dispatch(action: DeferredAction): void;
-  publish(what: 'OverviewData'|'TrackData'|'Threads'|'QueryResult', data: {}):
-      void;
+  publish(
+      what: 'OverviewData'|'TrackData'|'Threads'|'QueryResult'|'LegacyTrace',
+      data: {}, transferList?: Array<{}>): void;
 }
 
 /**
@@ -104,8 +105,11 @@
   }
 
   // TODO: this needs to be cleaned up.
-  publish(what: 'OverviewData'|'TrackData'|'Threads'|'QueryResult', data: {}) {
-    assertExists(this._frontend).send<void>(`publish${what}`, [data]);
+  publish(
+      what: 'OverviewData'|'TrackData'|'Threads'|'QueryResult'|'LegacyTrace',
+      data: {}, transferList?: Array<{}>) {
+    assertExists(this._frontend)
+        .send<void>(`publish${what}`, [data], transferList);
   }
 
   get state(): State {
diff --git a/ui/src/controller/index.ts b/ui/src/controller/index.ts
index 28e095c..fc9bbb0 100644
--- a/ui/src/controller/index.ts
+++ b/ui/src/controller/index.ts
@@ -37,3 +37,6 @@
 }
 
 main(self as {} as MessagePort);
+
+// For devtools-based debugging.
+(self as {} as {globals: {}}).globals = globals;
diff --git a/ui/src/controller/trace_converter.ts b/ui/src/controller/trace_converter.ts
new file mode 100644
index 0000000..865ee50
--- /dev/null
+++ b/ui/src/controller/trace_converter.ts
@@ -0,0 +1,57 @@
+// Copyright (C) 2018 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 {Actions} from '../common/actions';
+import * as trace_to_text from '../gen/trace_to_text';
+
+import {globals} from './globals';
+
+export function ConvertTrace(trace: Blob) {
+  const mod = trace_to_text({
+    noInitialRun: true,
+    locateFile: (s: string) => s,
+    print: updateStatus,
+    printErr: updateStatus,
+    onRuntimeInitialized: () => {
+      updateStatus('Converting trace');
+      const outPath = '/trace.json';
+      mod.callMain(['json', '/fs/trace.proto', outPath]);
+      updateStatus('Trace conversion completed');
+      const fsNode = mod.FS.lookupPath(outPath).node;
+      const data = fsNode.contents.buffer;
+      const size = fsNode.usedBytes;
+      globals.publish('LegacyTrace', {data, size}, /*transfer=*/[data]);
+      mod.FS.unlink(outPath);
+    },
+    onAbort: () => {
+      console.log('ABORT');
+    },
+  });
+  mod.FS.mkdir('/fs');
+  mod.FS.mount(
+      mod.FS.filesystems.WORKERFS,
+      {blobs: [{name: 'trace.proto', data: trace}]},
+      '/fs');
+
+  // TODO removeme.
+  (self as {} as {mod: {}}).mod = mod;
+}
+
+function updateStatus(msg: {}) {
+  console.log(msg);
+  globals.dispatch(Actions.updateStatus({
+    msg: msg.toString(),
+    timestamp: Date.now() / 1000,
+  }));
+}
diff --git a/ui/src/engine/wasm_bridge_unittest.ts b/ui/src/engine/wasm_bridge_unittest.ts
index 5aa8b0b..b1ae0dd 100644
--- a/ui/src/engine/wasm_bridge_unittest.ts
+++ b/ui/src/engine/wasm_bridge_unittest.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {Module, ModuleArgs} from '../gen/trace_processor';
+import {FileSystem, Module, ModuleArgs} from '../gen/trace_processor';
 
 import {WasmBridge} from './wasm_bridge';
 
@@ -47,6 +47,12 @@
     heap.set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 0);
     return heap;
   }
+
+  get FS(): FileSystem {
+    return ({} as FileSystem);
+  }
+
+  callMain() {}
 }
 
 test('wasm bridge should locate files', async () => {
diff --git a/ui/src/frontend/globals.ts b/ui/src/frontend/globals.ts
index ea0c248..37bf7a5 100644
--- a/ui/src/frontend/globals.ts
+++ b/ui/src/frontend/globals.ts
@@ -44,6 +44,7 @@
  */
 class Globals {
   private _dispatch?: Dispatch = undefined;
+  private _controllerWorker?: Worker = undefined;
   private _state?: State = undefined;
   private _frontendLocalState?: FrontendLocalState = undefined;
   private _rafScheduler?: RafScheduler = undefined;
@@ -54,8 +55,9 @@
   private _overviewStore?: OverviewStore = undefined;
   private _threadMap?: ThreadMap = undefined;
 
-  initialize(dispatch?: Dispatch) {
+  initialize(dispatch: Dispatch, controllerWorker: Worker) {
     this._dispatch = dispatch;
+    this._controllerWorker = controllerWorker;
     this._state = createEmptyState();
     this._frontendLocalState = new FrontendLocalState();
     this._rafScheduler = new RafScheduler();
@@ -116,6 +118,15 @@
     this._overviewStore = undefined;
     this._threadMap = undefined;
   }
+
+  // Used when switching to the legacy TraceViewer UI.
+  // Most resources are cleaned up by replacing the current |window| object,
+  // however pending RAFs and workers seem to outlive the |window| and need to
+  // be cleaned up explicitly.
+  shutdown() {
+    this._controllerWorker!.terminate();
+    this._rafScheduler!.shutdown();
+  }
 }
 
 export const globals = new Globals();
diff --git a/ui/src/frontend/index.ts b/ui/src/frontend/index.ts
index ea3c15b..68f80b5 100644
--- a/ui/src/frontend/index.ts
+++ b/ui/src/frontend/index.ts
@@ -20,8 +20,10 @@
 import {Actions} from '../common/actions';
 import {State} from '../common/state';
 import {TimeSpan} from '../common/time';
+
 import {globals, QuantizedLoad, ThreadDesc} from './globals';
 import {HomePage} from './home_page';
+import {openBufferWithLegacyTraceViewer} from './legacy_trace_viewer';
 import {RecordPage} from './record_page';
 import {Router} from './router';
 import {ViewerPage} from './viewer_page';
@@ -44,7 +46,6 @@
       globals.frontendLocalState.updateVisibleTime(
           new TimeSpan(vizTraceTime.startSec, vizTraceTime.endSec));
     }
-
     this.redraw();
   }
 
@@ -84,6 +85,13 @@
     this.redraw();
   }
 
+  // For opening JSON/HTML traces with the legacy catapult viewer.
+  publishLegacyTrace(args: {data: ArrayBuffer, size: number}) {
+    const arr = new Uint8Array(args.data, 0, args.size);
+    const str = (new TextDecoder('utf-8')).decode(arr);
+    openBufferWithLegacyTraceViewer('trace.json', str, 0);
+  }
+
   private redraw(): void {
     if (globals.state.route &&
         globals.state.route !== this.router.getRouteFromHash()) {
@@ -111,7 +119,7 @@
       },
       dispatch);
   forwardRemoteCalls(channel.port2, new FrontendApi(router));
-  globals.initialize(dispatch);
+  globals.initialize(dispatch, controller);
 
   globals.rafScheduler.domRedraw = () =>
       m.render(document.body, m(router.resolve(globals.state.route)));
diff --git a/ui/src/frontend/legacy_trace_viewer.ts b/ui/src/frontend/legacy_trace_viewer.ts
new file mode 100644
index 0000000..cbc3739
--- /dev/null
+++ b/ui/src/frontend/legacy_trace_viewer.ts
@@ -0,0 +1,102 @@
+// Copyright (C) 2018 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 {assertTrue} from '../base/logging';
+import {globals} from './globals';
+
+export function isLegacyTrace(fileName: string): boolean {
+  fileName = fileName.toLowerCase();
+  return (
+      fileName.endsWith('.json') || fileName.endsWith('.json.gz') ||
+      fileName.endsWith('.zip') || fileName.endsWith('.ctrace'));
+}
+
+export function openFileWithLegacyTraceViewer(file: File) {
+  const reader = new FileReader();
+  reader.onload = () => {
+    if (reader.result instanceof ArrayBuffer) {
+      return openBufferWithLegacyTraceViewer(
+          file.name, reader.result, reader.result.byteLength);
+    } else {
+      const str = reader.result as string;
+      return openBufferWithLegacyTraceViewer(file.name, str, str.length);
+    }
+  };
+  reader.onerror = err => {
+    console.error(err);
+  };
+  if (file.name.endsWith('.gz') || file.name.endsWith('.zip')) {
+    reader.readAsArrayBuffer(file);
+  } else {
+    reader.readAsText(file);
+  }
+}
+
+export function openBufferWithLegacyTraceViewer(
+    name: string, data: ArrayBuffer|string, size: number) {
+  if (data instanceof ArrayBuffer) {
+    assertTrue(size <= data.byteLength);
+    if (size !== data.byteLength) {
+      data = data.slice(0, size);
+    }
+  }
+  document.body.style.transition =
+      'filter 1s ease, transform 1s cubic-bezier(0.985, 0.005, 1.000, 0.225)';
+  document.body.style.filter = 'grayscale(1) blur(10px) opacity(0)';
+  document.body.style.transform = 'scale(0)';
+  const transitionPromise = new Promise(resolve => {
+    document.body.addEventListener('transitionend', (e: TransitionEvent) => {
+      if (e.propertyName === 'transform') {
+        resolve();
+      }
+    });
+  });
+
+  const loadPromise = new Promise(resolve => {
+    fetch('/assets/catapult_trace_viewer.html').then(resp => {
+      resp.text().then(content => {
+        resolve(content);
+      });
+    });
+  });
+
+  Promise.all([loadPromise, transitionPromise]).then(args => {
+    const fetchResult = args[0] as string;
+    replaceWindowWithTraceViewer(name, data, fetchResult);
+  });
+}
+
+// Replaces the contents of the current window with the Catapult's legacy
+// trace viewer HTML, passed in |htmlContent|.
+// This is in its own function to avoid leaking variables from the current
+// document we are about to destroy.
+function replaceWindowWithTraceViewer(
+    name: string, data: ArrayBuffer|string, htmlContent: string) {
+  globals.shutdown();
+  const newWin = window.open('', '_self') as Window;
+  newWin.document.open('text/html', 'replace');
+  newWin.document.addEventListener('readystatechange', () => {
+    const doc = newWin.document;
+    if (doc.readyState !== 'complete') return;
+    const ctl = doc.querySelector('x-profiling-view') as TraceViewerAPI;
+    ctl.setActiveTrace(name, data);
+  });
+  newWin.document.write(htmlContent);
+  newWin.document.close();
+}
+
+// TraceViewer method that we wire up to trigger the file load.
+interface TraceViewerAPI extends Element {
+  setActiveTrace(name: string, data: ArrayBuffer|string): void;
+}
\ No newline at end of file
diff --git a/ui/src/frontend/raf_scheduler.ts b/ui/src/frontend/raf_scheduler.ts
index bfe2017..ef39f32 100644
--- a/ui/src/frontend/raf_scheduler.ts
+++ b/ui/src/frontend/raf_scheduler.ts
@@ -61,6 +61,7 @@
   private hasScheduledNextFrame = false;
   private requestedFullRedraw = false;
   private isRedrawing = false;
+  private _shutdown = false;
 
   private perfStats = {
     rafActions: new RunningStatistics(),
@@ -91,6 +92,10 @@
     this.maybeScheduleAnimationFrame(true);
   }
 
+  shutdown() {
+    this._shutdown = true;
+  }
+
   set domRedraw(cb: RedrawCallback|null) {
     this._syncDomRedraw = cb || (_ => {});
   }
@@ -128,6 +133,7 @@
   }
 
   private onAnimationFrame(nowMs: number) {
+    if (this._shutdown) return;
     const rafStart = debugNow();
     this.hasScheduledNextFrame = false;
 
diff --git a/ui/src/frontend/sidebar.ts b/ui/src/frontend/sidebar.ts
index d85eb3a..aa02d37 100644
--- a/ui/src/frontend/sidebar.ts
+++ b/ui/src/frontend/sidebar.ts
@@ -17,6 +17,10 @@
 import {Actions} from '../common/actions';
 
 import {globals} from './globals';
+import {
+  isLegacyTrace,
+  openFileWithLegacyTraceViewer,
+} from './legacy_trace_viewer';
 
 const ALL_PROCESSES_QUERY = 'select name, pid from process order by name;';
 
@@ -74,8 +78,13 @@
     expanded: true,
     items: [
       {t: 'Open trace file', a: popupFileSelectionDialog, i: 'folder_open'},
+      {
+        t: 'Open with legacy UI',
+        a: popupFileSelectionDialogOldUI,
+        i: 'folder_open'
+      },
       {t: 'Record new trace', a: navigateRecord, i: 'fiber_smart_record'},
-      {t: 'Show timeline', a: navigateViewer, i: 'fiber_smart_record'},
+      {t: 'Show timeline', a: navigateViewer, i: 'line_style'},
       {t: 'Share current trace', a: dispatchCreatePermalink, i: 'share'},
     ],
   },
@@ -124,9 +133,20 @@
   },
 ];
 
+function getFileElement(): HTMLInputElement {
+  return document.querySelector('input[type=file]')! as HTMLInputElement;
+}
+
 function popupFileSelectionDialog(e: Event) {
   e.preventDefault();
-  (document.querySelector('input[type=file]')! as HTMLInputElement).click();
+  delete getFileElement().dataset['useCatapultLegacyUi'];
+  getFileElement().click();
+}
+
+function popupFileSelectionDialogOldUI(e: Event) {
+  e.preventDefault();
+  getFileElement().dataset['useCatapultLegacyUi'] = '1';
+  getFileElement().click();
 }
 
 function openTraceUrl(url: string): (e: Event) => void {
@@ -135,13 +155,25 @@
     globals.dispatch(Actions.openTraceFromUrl({url}));
   };
 }
-
 function onInputElementFileSelectionChanged(e: Event) {
   if (!(e.target instanceof HTMLInputElement)) {
     throw new Error('Not an input element');
   }
   if (!e.target.files) return;
-  globals.dispatch(Actions.openTraceFromFile({file: e.target.files[0]}));
+  const file = e.target.files[0];
+
+  if (e.target.dataset['useCatapultLegacyUi'] === '1') {
+    // Switch back the old catapult UI.
+    if (isLegacyTrace(file.name)) {
+      openFileWithLegacyTraceViewer(file);
+    } else {
+      globals.dispatch(Actions.convertTraceToJson({file}));
+    }
+    return;
+  }
+
+  // Open with the current UI.
+  globals.dispatch(Actions.openTraceFromFile({file}));
 }
 
 function navigateRecord(e: Event) {