Skip to content

Commit b42b9f0

Browse files
trisolaranjart
authored andcommitted
Support streaming trace viewer (#1128)
1 parent 8fe61b0 commit b42b9f0

File tree

6 files changed

+392
-52
lines changed

6 files changed

+392
-52
lines changed

tensorboard/components/tf_trace_viewer/BUILD

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ tf_web_library(
1111
"@org_chromium_catapult_vulcanized_trace_viewer//:trace_viewer_full.html",
1212
],
1313
path = "/tf-trace-viewer",
14+
deps = [
15+
":tf-trace-viewer-helper",
16+
],
1417
)
1518

1619
tf_web_library(
@@ -23,3 +26,10 @@ tf_web_library(
2326
],
2427
)
2528

29+
tf_web_library(
30+
name = "tf-trace-viewer-helper",
31+
srcs = [
32+
"tf-trace-viewer-helper.ts",
33+
],
34+
path = "/tf-trace-viewer",
35+
)
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/* Copyright 2018 The TensorFlow Authors. All Rights Reserved.
2+
3+
Licensed under the Apache License, Version 2.0 (the 'License');
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an 'AS IS' BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License.
14+
==============================================================================*/
15+
16+
/**
17+
* @fileoverview Helper utilities for the trace viewer within TensorBoard's profile plugin.
18+
*/
19+
20+
namespace tf_component_traceviewer {
21+
/** Amount of zooming allowed before re-fetching. */
22+
export const ZOOM_RATIO = 8;
23+
24+
/** Minimum safety buffer relative to viewport size. */
25+
export const PRESERVE_RATIO = 2;
26+
27+
/** Amount to fetch relative to viewport size. */
28+
export const FETCH_RATIO = 3;
29+
30+
export interface Range {
31+
min: number;
32+
max: number;
33+
}
34+
35+
/**
36+
* Expand the input range by scale, keep the center invariant.
37+
*/
38+
export function expand(range: Range, scale: number) : Range {
39+
var width = range.max - range.min;
40+
var mid = range.min + width / 2;
41+
return {
42+
min: mid - scale * width / 2,
43+
max: mid + scale * width / 2,
44+
};
45+
}
46+
/**
47+
* Check if range is within (totally included) in bounds.
48+
*/
49+
export function within(range: Range, bounds: Range): boolean {
50+
return bounds.min <= range.min && range.max <= bounds.max;
51+
}
52+
/**
53+
* Return length of the range.
54+
*/
55+
export function length(range: Range): number {
56+
return range.max - range.min;
57+
}
58+
/**
59+
* Return the intersection of two ranges.
60+
*/
61+
export function intersect(range: Range, bounds: Range): Range {
62+
return {
63+
min: Math.max(range.min, bounds.min),
64+
max: Math.min(range.max, bounds.max),
65+
};
66+
}
67+
} // namespace tf_component_traceviewer

tensorboard/components/tf_trace_viewer/tf-trace-viewer.html

Lines changed: 221 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,15 @@
5050
transition-delay: 0.3s;
5151
}
5252
</style>
53+
<script src="tf-trace-viewer-helper.js"></script>
5354
<script>
5455
"use strict";
5556

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+
*/
5662
Polymer({
5763
is: "tf-trace-viewer",
5864
properties: {
@@ -61,6 +67,7 @@
6167
type: String,
6268
value: null,
6369
},
70+
// _traceData is used for static mode.
6471
_traceData: {
6572
type: Object,
6673
observer: "_traceDataChanged"
@@ -69,7 +76,16 @@
6976
_traceContainer: Object,
7077
_traceModel: Object,
7178
_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 },
7287
},
88+
7389
ready: function() {
7490
// Initiate the trace viewer app.
7591
this._traceContainer = document.createElement("track-view-container");
@@ -97,44 +113,224 @@
97113
var components = parts[i].split('=');
98114
if (components[0] == "trace_data_url") {
99115
this.traceDataUrl = decodeURIComponent(components[1]);
100-
break;
116+
} else if (components[0] == "is_streaming") {
117+
this._isStreaming = components[1] === 'true';
101118
}
102119
}
103120
}
104121

122+
if (!this.traceDataUrl) {
123+
this._displayOverlay("Trace data URL is not provided.", "Trace Viewer");
124+
return null;
125+
}
105126
this._throbber.className = "active";
127+
106128
this._loadTrace();
107129
},
130+
108131
_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();
112153
}
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);
121185
}
186+
},
122187

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;
129198
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);
133232
}
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+
});
137240
},
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+
138334
_traceDataChanged: function(data) {
139335
if (!data) {
140336
this._displayOverlay("Trace Viewer", "No trace to display...");
@@ -152,6 +348,7 @@
152348
'Import error', tr.b.normalizeException(err).message);
153349
});
154350
},
351+
155352
_displayOverlay: function(title, content) {
156353
var overlay = new tr.ui.b.Overlay();
157354
overlay.textContent = content;

0 commit comments

Comments
 (0)