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) {