|
50 | 50 | transition-delay: 0.3s; |
51 | 51 | } |
52 | 52 | </style> |
| 53 | +<script src="tf-trace-viewer-helper.js"></script> |
53 | 54 | <script> |
54 | 55 | "use strict"; |
55 | 56 |
|
| 57 | + /* tf-trace-viewer will work in two modes: static mode and streaming mode. |
| 58 | + * in static mode, data are load at 'ready' time, |
| 59 | + * in streaming mode, data are load on demand when resolution and view port is changed. |
| 60 | + * static mode limit the amount of trace that we can collect and show to the users. |
| 61 | + */ |
56 | 62 | Polymer({ |
57 | 63 | is: "tf-trace-viewer", |
58 | 64 | properties: { |
|
61 | 67 | type: String, |
62 | 68 | value: null, |
63 | 69 | }, |
| 70 | + // _traceData is used for static mode. |
64 | 71 | _traceData: { |
65 | 72 | type: Object, |
66 | 73 | observer: "_traceDataChanged" |
|
69 | 76 | _traceContainer: Object, |
70 | 77 | _traceModel: Object, |
71 | 78 | _throbber: Object, |
| 79 | + _isStreaming: { type: Boolean, value: false }, |
| 80 | + _loadedRange: Object, |
| 81 | + _loadedTraceEents: Object, |
| 82 | + _fullBounds: Object, |
| 83 | + _isLoading: { type: Boolean, value: false }, |
| 84 | + _dirty: { type: Boolean, value: false }, |
| 85 | + _model: Object, |
| 86 | + _resolution: { type: Number, value: 1000 }, |
72 | 87 | }, |
| 88 | + |
73 | 89 | ready: function() { |
74 | 90 | // Initiate the trace viewer app. |
75 | 91 | this._traceContainer = document.createElement("track-view-container"); |
|
97 | 113 | var components = parts[i].split('='); |
98 | 114 | if (components[0] == "trace_data_url") { |
99 | 115 | this.traceDataUrl = decodeURIComponent(components[1]); |
100 | | - break; |
| 116 | + } else if (components[0] == "is_streaming") { |
| 117 | + this._isStreaming = components[1] === 'true'; |
101 | 118 | } |
102 | 119 | } |
103 | 120 | } |
104 | 121 |
|
| 122 | + if (!this.traceDataUrl) { |
| 123 | + this._displayOverlay("Trace data URL is not provided.", "Trace Viewer"); |
| 124 | + return null; |
| 125 | + } |
105 | 126 | this._throbber.className = "active"; |
| 127 | + |
106 | 128 | this._loadTrace(); |
107 | 129 | }, |
| 130 | + |
108 | 131 | _loadTrace : function() { |
109 | | - if (!this.traceDataUrl) { |
110 | | - this._displayOverlay("Trace data URL is not provided.", "Trace Viewer"); |
111 | | - return null; |
| 132 | + if (!this._isStreaming) { |
| 133 | + // Send HTTP request to get the trace data. |
| 134 | + var req = new XMLHttpRequest(); |
| 135 | + req.open('GET', this.traceDataUrl, true); |
| 136 | + |
| 137 | + req.onreadystatechange = event => { |
| 138 | + if (req.readyState !== 4) { |
| 139 | + return; |
| 140 | + } |
| 141 | + window.setTimeout(() => { |
| 142 | + if (req.status === 200) { |
| 143 | + this._throbber.className = "inactive"; |
| 144 | + this.set("_traceData", req.responseText); |
| 145 | + } else { |
| 146 | + this._displayOverlay(req.status, "Failed to fetch data"); |
| 147 | + } |
| 148 | + }, 0); |
| 149 | + }; |
| 150 | + req.send(null); |
| 151 | + } else { |
| 152 | + this._loadStreamingTrace(); |
112 | 153 | } |
113 | | - // Send HTTP request to get the trace data. |
114 | | - var req = new XMLHttpRequest(); |
115 | | - var is_binary = / [.] gz$ /.test(this.traceDataUrl) || |
116 | | - / [.] zip$ /.test(this.traceDataUrl); |
117 | | - req.overrideMimeType('text/plain; charset=x-user-defined'); |
118 | | - req.open('GET', this.traceDataUrl, true); |
119 | | - if (is_binary) { |
120 | | - req.responseType = 'arraybuffer'; |
| 154 | + }, |
| 155 | + |
| 156 | + // Something has changed, so consider reloading the data: |
| 157 | + // - if we have zoomed in enough to need more detail |
| 158 | + // - if we have scrolled too close to missing data regions |
| 159 | + // We ensure there's only ever one request in flight. |
| 160 | + _maybeLoad : function() { |
| 161 | + if (this._isLoading || this._resolution == 0) return; |
| 162 | + // We have several ranges of interest: |
| 163 | + // [viewport] - what's on-screen |
| 164 | + // [----preserve----] - issue loads to keep this full of data |
| 165 | + // [---------fetch----------] - fetch this much data with each load |
| 166 | + // [-----------full bounds--------] - the whole profile |
| 167 | + var viewport = this._trackViewRange(this._traceViewer.trackView); |
| 168 | + var PRESERVE_RATIO = tf_component_traceviewer.PRESERVE_RATIO; |
| 169 | + var preserve = tf_component_traceviewer.intersect( |
| 170 | + tf_component_traceviewer.expand(viewport, PRESERVE_RATIO), this._fullBounds); |
| 171 | + var FETCH_RATIO = tf_component_traceviewer.FETCH_RATIO; |
| 172 | + var fetch = tf_component_traceviewer.expand(viewport, FETCH_RATIO); |
| 173 | + var zoomFactor = tf_component_traceviewer.length(this._loadedRange) / |
| 174 | + tf_component_traceviewer.length(fetch); |
| 175 | + if (!tf_component_traceviewer.within(preserve, this._loadedRange) || |
| 176 | + zoomFactor > tf_component_traceviewer.ZOOM_RATIO) { |
| 177 | + console.log("loading more data: ", { |
| 178 | + zoomFactor: zoomFactor, |
| 179 | + loadedRange: this._loadedRange, |
| 180 | + viewport: viewport, |
| 181 | + preserve: preserve, |
| 182 | + fetch: fetch, |
| 183 | + }); |
| 184 | + this._loadTrace(fetch, /*replaceModel=*/false); |
121 | 185 | } |
| 186 | + }, |
122 | 187 |
|
123 | | - req.onreadystatechange = function(event) { |
124 | | - if (req.readyState !== 4) { |
125 | | - return; |
126 | | - } |
127 | | - window.setTimeout(function() { |
128 | | - if (req.status === 200) { |
| 188 | + _loadStreamingTrace : function(requestedRange, replaceModel) { |
| 189 | + var success = true; |
| 190 | + this._isLoading = true; |
| 191 | + |
| 192 | + this._loadJSON(requestedRange). |
| 193 | + then((data) => { this._updateModel(data, replaceModel); }). |
| 194 | + then(() => { this._updateView(requestedRange); }). |
| 195 | + catch((err) => { this._displayOverlay("Trace Viewer", err);}) |
| 196 | + .then(() => { |
| 197 | + this._isLoading = false; |
129 | 198 | this._throbber.className = "inactive"; |
130 | | - this.set("_traceData", is_binary ? req.response : req.responseText); |
131 | | - } else { |
132 | | - this._displayOverlay(req.status, "Failed to fetch data"); |
| 199 | + // Don't immediately load new data after the very first load. When |
| 200 | + // we first load the trace viewer, the actual view is not properly |
| 201 | + // initialized and we get an incorrect viewport leading to a spurious |
| 202 | + // load of data. |
| 203 | + if (success && requestedRange) this._maybeLoad(); |
| 204 | + }); |
| 205 | + }, |
| 206 | + |
| 207 | + // Loads a time window (the whole trace if requestedRange is null). |
| 208 | + // Returns a promise for the JSON event data. |
| 209 | + _loadJSON : function(requestedRange) { |
| 210 | + // Set up an XMLHTTPRequest to the JSON endpoint, populating range and |
| 211 | + // resolution if appropriate. |
| 212 | + var requestURL = this._buildBaseURL(); |
| 213 | + var ZOOM_RATIO = tf_component_traceviewer.ZOOM_RATIO; |
| 214 | + requestURL.searchParams.set("resolution", this._resolution * ZOOM_RATIO); |
| 215 | + if (requestedRange != null) { |
| 216 | + requestURL.searchParams.set("start_time_ms", requestedRange.min); |
| 217 | + requestURL.searchParams.set("end_time_ms", requestedRange.max); |
| 218 | + } |
| 219 | + |
| 220 | + return new Promise(function(resolve, reject) { |
| 221 | + var xhr = new XMLHttpRequest(); |
| 222 | + xhr.open('GET', requestURL); |
| 223 | + xhr.onload = function() { |
| 224 | + var contentType = this.getResponseHeader('Content-Type'); |
| 225 | + if (this.status !== 200 || |
| 226 | + !contentType.startsWith('application/json')) { |
| 227 | + var msg = requestURL + ' could not be loaded'; |
| 228 | + if (contentType.startsWith('text/plain')) { |
| 229 | + msg = msg + ': ' + xhr.statusText; |
| 230 | + } |
| 231 | + reject(msg); |
133 | 232 | } |
134 | | - }.bind(this), 0); |
135 | | - }.bind(this); |
136 | | - req.send(null); |
| 233 | + resolve(xhr.response); |
| 234 | + }; |
| 235 | + xhr.onerror = function () { |
| 236 | + reject(requestURL + 'could not be loaded: ' + xhr.statusText); |
| 237 | + }; |
| 238 | + xhr.send(); |
| 239 | + }); |
137 | 240 | }, |
| 241 | + // Decodes the JSON trace events, removes all events that were loaded before |
| 242 | + // and serializes to JSON again. |
| 243 | + _filterKnownTraceEvents: function(data) { |
| 244 | + var traceEvents = data.traceEvents; |
| 245 | + data.traceEvents = []; |
| 246 | + for (var i = 0; i < traceEvents.length; i++) { |
| 247 | + // This is inefficient as we are serializing the events we just |
| 248 | + // deserialized. If this becomes a problem in practice, we should assign |
| 249 | + // IDs on the server. |
| 250 | + var asString = JSON.stringify(traceEvents[i]); |
| 251 | + if (!this._loadedTraceEvents.has(asString)) { |
| 252 | + this._loadedTraceEvents.add(asString); |
| 253 | + data.traceEvents.push(traceEvents[i]); |
| 254 | + } |
| 255 | + } |
| 256 | + return data; |
| 257 | + }, |
| 258 | + |
| 259 | + // Updates the model with data returned by the JSON endpoint. |
| 260 | + // If replaceModel is true, the data set is completely replaced; otherwise, |
| 261 | + // the new data is merged with the old data. |
| 262 | + // Returns a void promise. |
| 263 | + _updateModel: function(data, replaceModel) { |
| 264 | + data = JSON.parse(data); |
| 265 | + if (!this._model /* first load */ || replaceModel) { |
| 266 | + this._dirty = true; |
| 267 | + this._model = new tr.Model(); |
| 268 | + this._loadedTraceEvents = new Set(); |
| 269 | + } else { |
| 270 | + // Delete metadata and displayTimeUnits as otherwise traceviewer |
| 271 | + // accumulates them. |
| 272 | + delete data['metadata']; |
| 273 | + delete data['displayTimeUnit']; |
| 274 | + } |
| 275 | + |
| 276 | + data = this._filterKnownTraceEvents(data); |
| 277 | + if (data.traceEvents.length > 0) { |
| 278 | + var opt = new tr.importer.ImportOptions(); |
| 279 | + opt.shiftWorldToZero = false; |
| 280 | + new tr.importer.Import(this._model, opt).importTraces([data]); |
| 281 | + this._dirty = true; |
| 282 | + } |
| 283 | + return Promise.resolve(); |
| 284 | + }, |
| 285 | + |
| 286 | + // Updates the view based on the current model. |
| 287 | + _updateView: function(requestedRange) { |
| 288 | + if (requestedRange == null) { |
| 289 | + this._fullBounds = {min: this._model.bounds.min, max: this._model.bounds.max}; |
| 290 | + this._loadedRange = tf_component_traceviewer.expand( |
| 291 | + this._fullBounds, tf_component_traceviewer.FETCH_RATIO); |
| 292 | + } else { |
| 293 | + this._loadedRange = requestedRange; |
| 294 | + } |
| 295 | + if (!this._dirty){ |
| 296 | + return; |
| 297 | + } |
| 298 | + this._dirty = false; |
| 299 | + // We can't assign the model until the viewer is attached. This may be |
| 300 | + // delayed indefinitely if the tab is backgrounded. This version of polymer |
| 301 | + // doesn't provide a direct way to observe the viewer being attached. |
| 302 | + // This is a hack: the browser won't paint until the viewer is attached. |
| 303 | + window.requestAnimationFrame(function() { |
| 304 | + this._traceViewer.model = this._model; |
| 305 | + if (this._traceViewer.trackView != null) { // Only initialized if data in nonempty! |
| 306 | + // Wait 200ms to let an animated zoom/pan operation complete. Ideally, |
| 307 | + // we could just explicitly wait for its end. |
| 308 | + |
| 309 | + this._traceViewer.trackView.viewport.addEventListener( |
| 310 | + "change", () => setTimeout(this._maybeLoad.bind(this), 200)); |
| 311 | + } |
| 312 | + this._traceViewer.viewTitle = ""; |
| 313 | + }.bind(this)); |
| 314 | + }, |
| 315 | + |
| 316 | + // Access the {min, max} range of a trackView. |
| 317 | + _trackViewRange: function(trackView) { |
| 318 | + var xfm = trackView.viewport.currentDisplayTransform; |
| 319 | + const pixelRatio = window.devicePixelRatio || 1; |
| 320 | + const devicePixelWidth = pixelRatio * trackView.viewWidth_; |
| 321 | + return { |
| 322 | + min: xfm.xViewToWorld(0), |
| 323 | + max: xfm.xViewToWorld(devicePixelWidth), |
| 324 | + }; |
| 325 | + }, |
| 326 | + |
| 327 | + // Builds a base URL for fetching json data. The URL will be assembled with |
| 328 | + // all filtering URL parameters, except resolution and range. |
| 329 | + _buildBaseURL: function() { |
| 330 | + var requestURL = new URL(this.traceDataUrl, window.location.href); |
| 331 | + return requestURL; |
| 332 | + }, |
| 333 | + |
138 | 334 | _traceDataChanged: function(data) { |
139 | 335 | if (!data) { |
140 | 336 | this._displayOverlay("Trace Viewer", "No trace to display..."); |
|
152 | 348 | 'Import error', tr.b.normalizeException(err).message); |
153 | 349 | }); |
154 | 350 | }, |
| 351 | + |
155 | 352 | _displayOverlay: function(title, content) { |
156 | 353 | var overlay = new tr.ui.b.Overlay(); |
157 | 354 | overlay.textContent = content; |
|
0 commit comments