[observatory] Dramatically reduce timeline loading time.

Restore fetching from the Catapult IFrame instead of the main frame to avoid the postMessage bottleneck.

Cf. 68dede011e81de04ffc826f42732b8ce620bc52d

TEST=view timeline
Change-Id: I417ec40393fc149d0bf22657a2d9a46c3125f634
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/263160
Reviewed-by: Ben Konyi <bkonyi@google.com>
Commit-Queue: Ryan Macnak <rmacnak@google.com>
diff --git a/runtime/observatory/lib/src/elements/timeline_page.dart b/runtime/observatory/lib/src/elements/timeline_page.dart
index d4b1a26..7e30a2d 100644
--- a/runtime/observatory/lib/src/elements/timeline_page.dart
+++ b/runtime/observatory/lib/src/elements/timeline_page.dart
@@ -249,8 +249,9 @@
 
   Future _refresh() async {
     _postMessage('loading');
-    final traceData = await _repository.getTimeline(vm);
-    return _postMessage('refresh', traceData);
+    final params =
+        new Map<String, dynamic>.from(await _repository.getIFrameParams(vm));
+    return _postMessage('refresh', params);
   }
 
   Future _clear() async {
diff --git a/runtime/observatory/lib/src/models/repositories/timeline.dart b/runtime/observatory/lib/src/models/repositories/timeline.dart
index 3d528c9..bd8276e 100644
--- a/runtime/observatory/lib/src/models/repositories/timeline.dart
+++ b/runtime/observatory/lib/src/models/repositories/timeline.dart
@@ -8,5 +8,5 @@
   Future<TimelineFlags> getFlags(VMRef ref);
   Future setRecordedStreams(VMRef ref, Iterable<TimelineStream> streams);
   Future clear(VMRef ref);
-  Future<Map> getTimeline(VMRef ref);
+  Future<Map<String, dynamic>> getIFrameParams(VMRef ref);
 }
diff --git a/runtime/observatory/lib/src/repositories/timeline.dart b/runtime/observatory/lib/src/repositories/timeline.dart
index 834e637..97f0a6d 100644
--- a/runtime/observatory/lib/src/repositories/timeline.dart
+++ b/runtime/observatory/lib/src/repositories/timeline.dart
@@ -29,6 +29,17 @@
     return vm.invokeRpc('clearVMTimeline', {});
   }
 
+  Future<Map<String, dynamic>> getIFrameParams(M.VMRef ref) async {
+    S.VM vm = ref as S.VM;
+    assert(vm != null);
+    await vm.reload();
+    await vm.reloadIsolates();
+    return <String, dynamic>{
+      'vmAddress': vm.target.networkAddress,
+      'isolateIds': vm.isolates.map((i) => i.id).toList()
+    };
+  }
+
   Future<void> _formatSamples(
       M.Isolate isolate, Map traceObject, S.ServiceMap cpuSamples) async {
     const kRootFrameId = 0;
diff --git a/runtime/observatory/web/timeline.js b/runtime/observatory/web/timeline.js
index 5b431f6..0ff35f0 100644
--- a/runtime/observatory/web/timeline.js
+++ b/runtime/observatory/web/timeline.js
@@ -4,6 +4,7 @@
 
 // See also timeline_message_handler.js.
 
+var traceObject;
 var pendingRequests;
 var loadingOverlay;
 
@@ -16,6 +17,16 @@
   viewer.model = undefined;
 }
 
+function fetchUri(uri, onLoad, onError) {
+  var xhr = new XMLHttpRequest();
+  xhr.open('GET', uri, true);
+  xhr.responseType = 'text';
+  xhr.addEventListener('load', onLoad);
+  xhr.addEventListener('error', onError);
+  xhr.send();
+  console.log('GET ' + uri);
+}
+
 function onImportFail(err) {
   var overlay = new tr.ui.b.Overlay();
   overlay.textContent = tr.b.normalizeException(err).message;
@@ -542,6 +553,9 @@
 }
 
 function showLoadingOverlay(msg) {
+  if (typeof tr === "undefined") {
+    return;
+  }
   if (!loadingOverlay) {
     loadingOverlay = new tr.ui.b.Overlay();
   }
@@ -558,6 +572,139 @@
   loadingOverlay = undefined;
 }
 
+function gotReponse() {
+  pendingRequests--;
+  if (pendingRequests === 0) {
+    console.log('Got all timeline parts');
+    updateTimeline(traceObject);
+    hideLoadingOverlay();
+  }
+}
+
+function processTimelineResponse(response) {
+  if (response.error) {
+    // Maybe profiling is disabled.
+    console.log('ERROR ' + response.error.message);
+  } else {
+    var result = response['result'];
+    console.log(result);
+
+    if (result["type"] == "Timeline") {
+      traceObject.traceEvents = traceObject.traceEvents.concat(result['traceEvents']);
+    } else if (result["type"] == "CpuSamples") {
+      var pid = result["pid"];
+      var functions = result["functions"];
+      var samples = result["samples"];
+      var frames = traceObject.stackFrames;
+      var id = 0;
+      for (var i = 0; i < samples.length; i++) {
+        var sample = samples[i];
+        var stack = sample["stack"];
+        for (var j = 0; j < stack.length; j++) {
+          var profileFunc = functions[stack[j]];
+          var func = profileFunc["function"];
+          var name = func["name"];
+          while (func["owner"] != undefined) {
+            func = func["owner"];
+            name = func["name"] + "." + name;
+          }
+          var frame = {
+            'category': 'Dart',
+            'name': name,
+            'resolvedUrl': profileFunc["resolvedUrl"],
+          };
+          if (j != 0) {
+            frame['parent'] = "" + pendingRequests + "-" + id;
+          }
+          id++;
+          frames["" + pendingRequests + "-" + id] = frame;
+        }
+        traceObject.traceEvents.push({
+          'ph': 'P', // kind = sample event
+          'name': '', // Blank to keep about:tracing happy
+          'pid': pid,
+          'tid': sample['tid'],
+          'ts': sample['timestamp'],
+          'cat': 'Dart',
+          'sf': "" + pendingRequests + "-" + id,
+        });
+      }
+    } else {
+      console.log("Unknown timeline content type");
+    }
+  }
+
+  gotReponse();
+}
+
+function fetchTimelineOnLoad(event) {
+  var xhr = event.target;
+  var response = JSON.parse(xhr.responseText);
+  processTimelineResponse(response);
+}
+
+function fetchTimelineOnError(event) {
+  var xhr = event.target;
+  console.log(xhr.statusText);
+  gotReponse();
+}
+
+function fetchCPUProfile(vmAddress, isolateIds, timeOrigin, timeExtent) {
+  showLoadingOverlay('Fetching CPU profile(s) from Dart VM');
+  var parser = document.createElement('a');
+  parser.href = vmAddress;
+  pendingRequests += isolateIds.length;
+  for (var i = 0; i < isolateIds.length; i++) {
+    var isolateId = isolateIds[i];
+    var requestUri = 'http://' +
+                     parser.hostname +
+                     ':' +
+                     parser.port +
+                     parser.pathname.replace(/\/ws$/, "") +
+                     '/getCpuSamples?tags=None&isolateId=' +
+                     isolateId +
+                     '&timeOriginMicros=' + timeOrigin +
+                     '&timeExtentMicros=' + timeExtent;
+    fetchUri(requestUri, fetchTimelineOnLoad, fetchTimelineOnError);
+  }
+}
+
+function fetchTimeline(vmAddress, isolateIds, mode) {
+  // Reset combined timeline.
+  traceObject = {
+    'stackFrames': {},
+    'traceEvents': []
+  };
+  timelineMode = mode;
+  pendingRequests = 1;
+
+  showLoadingOverlay('Fetching timeline from Dart VM');
+  var parser = document.createElement('a');
+  parser.href = vmAddress;
+  var requestUri = 'http://' +
+                   parser.hostname +
+                   ':' +
+                   parser.port +
+                   parser.pathname.replace(/\/ws$/, "") +
+                   '/getVMTimeline';
+  fetchUri(requestUri, function(event) {
+    // Grab the response.
+    var xhr = event.target;
+    var response = JSON.parse(xhr.responseText);
+    // Extract the time origin and extent.
+    var timeOrigin = response['result']['timeOriginMicros'];
+    var timeExtent = response['result']['timeExtentMicros'];
+    console.assert(Number.isInteger(timeOrigin), timeOrigin);
+    console.assert(Number.isInteger(timeExtent), timeExtent);
+    console.log(timeOrigin);
+    console.log(timeExtent);
+    // fetchCPUProfile.
+    fetchCPUProfile(vmAddress, isolateIds, timeOrigin, timeExtent);
+    // This must happen after 'fetchCPUProfile';
+    processTimelineResponse(response, mode);
+  }, fetchTimelineOnError);
+}
+
 function populateTimeline() {
   updateTimeline(traceObject);
   hideLoadingOverlay();
@@ -643,7 +790,7 @@
 }
 
 function refreshTimeline() {
-  updateTimeline(traceObject);
+  fetchTimeline(timeline_vm_address, timeline_isolates);
 }
 
 window.addEventListener('DOMContentLoaded', function() {
@@ -658,9 +805,9 @@
   timeline_loaded = true;
   console.log('DOMContentLoaded');
   document.getElementById('trace-viewer').highlightVSync = true;
-  if (traceObject != undefined) {
+  if (timeline_vm_address != undefined) {
     refreshTimeline();
-    traceObject = undefined;
+    timeline_vm_address = undefined;
   }
 });
 
diff --git a/runtime/observatory/web/timeline_message_handler.js b/runtime/observatory/web/timeline_message_handler.js
index 4b3f11a..f2da8ba 100644
--- a/runtime/observatory/web/timeline_message_handler.js
+++ b/runtime/observatory/web/timeline_message_handler.js
@@ -5,7 +5,11 @@
 // This file is loaded before the about:tracing code is loaded so that we have
 // an event listener registered early.
 
-var traceObject;
+// Used to delay the initial timeline load until the timeline has finished
+// loading.
+timeline_loaded = false;
+timeline_vm_address = undefined;
+timeline_isolates = undefined;
 
 function registerForMessages() {
   window.addEventListener("message", onMessage, false);
@@ -24,11 +28,12 @@
       showLoadingOverlay('Fetching timeline...');
     break;
     case 'refresh':
-      traceObject = params;
-      if (typeof populateTimeline != 'undefined') {
-        populateTimeline();
+      timeline_vm_address = params['vmAddress'];
+      timeline_isolates = params['isolateIds'];
+      if (typeof fetchTimeline === "undefined") {
+        console.log('Delaying timeline refresh until loaded.');
       } else {
-        console.log('populateTimeline is not yet defined');
+        fetchTimeline(timeline_vm_address, timeline_isolates);
       }
     break;
     case 'clear':