From 510f74ca0560daead3d0ed70e4dcbb187afbea35 Mon Sep 17 00:00:00 2001 From: Volodymyr Samokhatko Date: Tue, 27 Jun 2023 10:00:49 +0200 Subject: [PATCH 1/4] Whitespace --- vis/Code/NameMap.js | 2 +- vis/Code/Remotery.js | 16 ++++++++-------- vis/Code/TimelineRow.js | 4 ++-- vis/Code/TimelineWindow.js | 2 +- vis/Code/TitleWindow.js | 2 +- vis/index.html | 2 +- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/vis/Code/NameMap.js b/vis/Code/NameMap.js index 37a966dc..cd5853d2 100644 --- a/vis/Code/NameMap.js +++ b/vis/Code/NameMap.js @@ -50,4 +50,4 @@ class NameMap return name; } -} \ No newline at end of file +} diff --git a/vis/Code/Remotery.js b/vis/Code/Remotery.js index 4b373fe0..14b2d222 100644 --- a/vis/Code/Remotery.js +++ b/vis/Code/Remotery.js @@ -22,18 +22,18 @@ Settings = (function() Remotery = (function() { // crack the url and get the parameter we want - var getUrlParameter = function getUrlParameter( search_param) + var getUrlParameter = function getUrlParameter( search_param) { var page_url = decodeURIComponent( window.location.search.substring(1) ), url_vars = page_url.split('&'), param_name, i; - for (i = 0; i < url_vars.length; i++) + for (i = 0; i < url_vars.length; i++) { param_name = url_vars[i].split('='); - if (param_name[0] === search_param) + if (param_name[0] === search_param) { return param_name[1] === undefined ? true : param_name[1]; } @@ -112,7 +112,7 @@ Remotery = (function() this.gridWindows = { }; this.propertyGridWindow = this.AddGridWindow("__rmt__global__properties__", "Global Properties", new GridConfigProperties()); - + // Clear runtime data this.FrameHistory = { }; this.ProcessorFrameHistory = { }; @@ -366,7 +366,7 @@ Remotery = (function() // Otherwise this stops a paused Remotery from loading new samples from disk if (self.Settings.IsPaused && self.Server.Connected()) return; - + let nb_processors = data_view_reader.GetUInt32(); let message_index = data_view_reader.GetUInt64(); @@ -389,7 +389,7 @@ Remotery = (function() self.ProcessorFrameHistory[processor_name] = [ ]; } let frame_history = self.ProcessorFrameHistory[processor_name]; - + if (thread_id == 0xFFFFFFFF) { continue; @@ -414,7 +414,7 @@ Remotery = (function() continue; } } - + // Discard old frames to keep memory-use constant var max_nb_frames = 10000; var extra_frames = frame_history.length - max_nb_frames; @@ -753,4 +753,4 @@ Remotery = (function() } return Remotery; -})(); \ No newline at end of file +})(); diff --git a/vis/Code/TimelineRow.js b/vis/Code/TimelineRow.js index eac66a63..738b9d5e 100644 --- a/vis/Code/TimelineRow.js +++ b/vis/Code/TimelineRow.js @@ -137,11 +137,11 @@ TimelineRow = (function() const program = gl_canvas.timelineHighlightProgram; gl_canvas.SetContainerUniforms(program, container); - + // Set row parameters const row_rect = this.LabelContainerNode.getBoundingClientRect(); glSetUniform(gl, program, "inRow.yOffset", row_rect.top); - + // Set sample parameters const float_offset = offset / 4; glSetUniform(gl, program, "inStartMs", frame.sampleFloats[float_offset + g_sampleOffsetFloats_Start]); diff --git a/vis/Code/TimelineWindow.js b/vis/Code/TimelineWindow.js index 9b4a391e..383c2f7a 100644 --- a/vis/Code/TimelineWindow.js +++ b/vis/Code/TimelineWindow.js @@ -146,7 +146,7 @@ TimelineWindow = (function() // Set viewport parameters glSetUniform(gl, program, "inViewport.width", gl.canvas.width); glSetUniform(gl, program, "inViewport.height", gl.canvas.height); - + this.glCanvas.SetContainerUniforms(program, this.TimelineContainer.Node); // Set row parameters diff --git a/vis/Code/TitleWindow.js b/vis/Code/TitleWindow.js index bae31dd6..7c55d3e2 100644 --- a/vis/Code/TitleWindow.js +++ b/vis/Code/TitleWindow.js @@ -102,4 +102,4 @@ TitleWindow = (function() return TitleWindow; -})(); \ No newline at end of file +})(); diff --git a/vis/index.html b/vis/index.html index dc4385e9..5a4234d4 100644 --- a/vis/index.html +++ b/vis/index.html @@ -66,4 +66,4 @@ - \ No newline at end of file + From 7f2d4f61c6b0ba0ecdd4b37d759986ef6b615ccf Mon Sep 17 00:00:00 2001 From: Volodymyr Samokhatko Date: Mon, 26 Jun 2023 14:40:29 +0200 Subject: [PATCH 2/4] Bash brace expansion in js --- vis/extern/balanced-match.js | 91 ++++++++++++ vis/extern/brace-expansion.js | 255 ++++++++++++++++++++++++++++++++++ vis/index.html | 4 + 3 files changed, 350 insertions(+) create mode 100644 vis/extern/balanced-match.js create mode 100644 vis/extern/brace-expansion.js diff --git a/vis/extern/balanced-match.js b/vis/extern/balanced-match.js new file mode 100644 index 00000000..689346cb --- /dev/null +++ b/vis/extern/balanced-match.js @@ -0,0 +1,91 @@ +'use strict' + +/* +(MIT) + +Copyright (c) 2013 Julian Gruber + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +/** + * @param {string | RegExp} a + * @param {string | RegExp} b + * @param {string} str + */ +function balanced (a, b, str) { + if (a instanceof RegExp) a = maybeMatch(a, str) + if (b instanceof RegExp) b = maybeMatch(b, str) + + const r = range(a, b, str) + + return ( + r && { + start: r[0], + end: r[1], + pre: str.slice(0, r[0]), + body: str.slice(r[0] + a.length, r[1]), + post: str.slice(r[1] + b.length) + } + ) +} + +/** + * @param {RegExp} reg + * @param {string} str + */ +function maybeMatch (reg, str) { + const m = str.match(reg) + return m ? m[0] : null +} + +balanced.range = range + +/** + * @param {string} a + * @param {string} b + * @param {string} str + */ +function range (a, b, str) { + let begs, beg, left, right, result + let ai = str.indexOf(a) + let bi = str.indexOf(b, ai + 1) + let i = ai + + if (ai >= 0 && bi > 0) { + if (a === b) { + return [ai, bi] + } + begs = [] + left = str.length + + while (i >= 0 && !result) { + if (i === ai) { + begs.push(i) + ai = str.indexOf(a, i + 1) + } else if (begs.length === 1) { + result = [begs.pop(), bi] + } else { + beg = begs.pop() + if (beg < left) { + left = beg + right = bi + } + + bi = str.indexOf(b, i + 1) + } + + i = ai < bi && ai >= 0 ? ai : bi + } + + if (begs.length) { + result = [left, right] + } + } + + return result +} diff --git a/vis/extern/brace-expansion.js b/vis/extern/brace-expansion.js new file mode 100644 index 00000000..57733c80 --- /dev/null +++ b/vis/extern/brace-expansion.js @@ -0,0 +1,255 @@ +/* +MIT License + +Copyright (c) 2013 Julian Gruber + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +const escSlash = '\0SLASH'+Math.random()+'\0'; +const escOpen = '\0OPEN'+Math.random()+'\0'; +const escClose = '\0CLOSE'+Math.random()+'\0'; +const escComma = '\0COMMA'+Math.random()+'\0'; +const escPeriod = '\0PERIOD'+Math.random()+'\0'; + +/** + * @return {number} + */ +function numeric(str) { + return parseInt(str, 10) == str + ? parseInt(str, 10) + : str.charCodeAt(0); +} + +/** + * @param {string} str + */ +function escapeBraces(str) { + return str.split('\\\\').join(escSlash) + .split('\\{').join(escOpen) + .split('\\}').join(escClose) + .split('\\,').join(escComma) + .split('\\.').join(escPeriod); +} + +/** + * @param {string} str + */ +function unescapeBraces(str) { + return str.split(escSlash).join('\\') + .split(escOpen).join('{') + .split(escClose).join('}') + .split(escComma).join(',') + .split(escPeriod).join('.'); +} + +/** + * Basically just str.split(","), but handling cases + * where we have nested braced sections, which should be + * treated as individual members, like {a,{b,c},d} + * @param {string} str + */ +function parseCommaParts(str) { + if (!str) + return ['']; + + const parts = []; + const m = balanced('{', '}', str); + + if (!m) + return str.split(','); + + const {pre, body, post} = m; + const p = pre.split(','); + + p[p.length-1] += '{' + body + '}'; + const postParts = parseCommaParts(post); + if (post.length) { + p[p.length-1] += postParts.shift(); + p.push.apply(p, postParts); + } + + parts.push.apply(parts, p); + + return parts; +} + +/** + * @param {string} str + */ +function expandTop(str) { + if (!str) + return []; + + // I don't know why Bash 4.3 does this, but it does. + // Anything starting with {} will have the first two bytes preserved + // but *only* at the top level, so {},a}b will not expand to anything, + // but a{},b}c will be expanded to [a}c,abc]. + // One could argue that this is a bug in Bash, but since the goal of + // this module is to match Bash's rules, we escape a leading {} + if (str.slice(0, 2) === '{}') { + str = '\\{\\}' + str.slice(2); + } + + return expand(escapeBraces(str), true).map(unescapeBraces); +} + +/** + * @param {string} str + */ +function embrace(str) { + return '{' + str + '}'; +} + +/** + * @param {string} el + */ +function isPadded(el) { + return /^-?0\d/.test(el); +} + +/** + * @param {number} i + * @param {number} y + */ +function lte(i, y) { + return i <= y; +} + +/** + * @param {number} i + * @param {number} y + */ +function gte(i, y) { + return i >= y; +} + +/** + * @param {string} str + * @param {boolean} [isTop] + */ +function expand(str, isTop) { + /** @type {string[]} */ + const expansions = []; + + const m = balanced('{', '}', str); + if (!m) return [str]; + + // no need to expand pre, since it is guaranteed to be free of brace-sets + const pre = m.pre; + const post = m.post.length + ? expand(m.post, false) + : ['']; + + if (/\$$/.test(m.pre)) { + for (let k = 0; k < post.length; k++) { + const expansion = pre+ '{' + m.body + '}' + post[k]; + expansions.push(expansion); + } + } else { + const isNumericSequence = /^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/.test(m.body); + const isAlphaSequence = /^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/.test(m.body); + const isSequence = isNumericSequence || isAlphaSequence; + const isOptions = m.body.indexOf(',') >= 0; + if (!isSequence && !isOptions) { + // {a},b} + if (m.post.match(/,.*\}/)) { + str = m.pre + '{' + m.body + escClose + m.post; + return expand(str); + } + return [str]; + } + + let n; + if (isSequence) { + n = m.body.split(/\.\./); + } else { + n = parseCommaParts(m.body); + if (n.length === 1) { + // x{{a,b}}y ==> x{a}y x{b}y + n = expand(n[0], false).map(embrace); + if (n.length === 1) { + return post.map(function(p) { + return m.pre + n[0] + p; + }); + } + } + } + + // at this point, n is the parts, and we know it's not a comma set + // with a single entry. + let N; + + if (isSequence) { + const x = numeric(n[0]); + const y = numeric(n[1]); + const width = Math.max(n[0].length, n[1].length) + let incr = n.length == 3 + ? Math.abs(numeric(n[2])) + : 1; + let test = lte; + const reverse = y < x; + if (reverse) { + incr *= -1; + test = gte; + } + const pad = n.some(isPadded); + + N = []; + + for (let i = x; test(i, y); i += incr) { + let c; + if (isAlphaSequence) { + c = String.fromCharCode(i); + if (c === '\\') + c = ''; + } else { + c = String(i); + if (pad) { + const need = width - c.length; + if (need > 0) { + const z = new Array(need + 1).join('0'); + if (i < 0) + c = '-' + z + c.slice(1); + else + c = z + c; + } + } + } + N.push(c); + } + } else { + N = []; + + for (let j = 0; j < n.length; j++) { + N.push.apply(N, expand(n[j], false)); + } + } + + for (let j = 0; j < N.length; j++) { + for (let k = 0; k < post.length; k++) { + const expansion = pre + N[j] + post[k]; + if (!isTop || isSequence || expansion) + expansions.push(expansion); + } + } + } + + return expansions; +} diff --git a/vis/index.html b/vis/index.html index 5a4234d4..bc9189fe 100644 --- a/vis/index.html +++ b/vis/index.html @@ -23,6 +23,10 @@ + + + + From 3ed44b1c9e6f6a1e991314a5ffb534dbb5af66e4 Mon Sep 17 00:00:00 2001 From: Volodymyr Samokhatko Date: Mon, 26 Jun 2023 14:42:38 +0200 Subject: [PATCH 3/4] Add messages to pass the counter_start timepoint --- lib/Remotery.c | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/lib/Remotery.c b/lib/Remotery.c index 8a1d136b..b2c93e44 100644 --- a/lib/Remotery.c +++ b/lib/Remotery.c @@ -6341,6 +6341,16 @@ static rmtError Remotery_SendLogTextMessage(Remotery* rmt, Message* message) return RMT_ERROR_NONE; } +static rmtError bin_SamplesStart(Buffer* buffer, rmtU64 counter_start) +{ + rmtU32 write_start_offset; + rmtTry(bin_MessageHeader(buffer, "SSST", &write_start_offset)); + rmtTry(Buffer_WriteU64(buffer, counter_start)); + rmtTry(bin_MessageFooter(buffer, write_start_offset)); + + return RMT_ERROR_NONE; +} + static rmtError bin_SampleName(Buffer* buffer, const char* name, rmtU32 name_hash, rmtU32 name_length) { rmtU32 write_start_offset; @@ -6899,6 +6909,14 @@ static rmtError Remotery_ReceiveMessage(void* context, char* message_data, rmtU3 break; } + + case FOURCC('G', 'S', 'S', 'T'): { + Buffer* bin_buf = rmt->server->bin_buf; + WebSocket_PrepareBuffer(bin_buf); + bin_SamplesStart(bin_buf, rmt->timer.counter_start); + + return Server_Send(rmt->server, bin_buf->data, bin_buf->bytes_used, 10); + } } #undef FOURCC From f0b7b8293fb8eb6e9624b110d5793d1a485e25f7 Mon Sep 17 00:00:00 2001 From: Volodymyr Samokhatko Date: Mon, 26 Jun 2023 14:43:33 +0200 Subject: [PATCH 4/4] Single client connecting to multiple servers/processes --- readme.md | 2 + vis/Code/Console.js | 22 ++--- vis/Code/NameMap.js | 16 ++-- vis/Code/Remotery.js | 172 +++++++++++++++++++++++++------------ vis/Code/TimelineRow.js | 25 ++++-- vis/Code/TimelineWindow.js | 20 ++++- vis/Code/TitleWindow.js | 12 ++- 7 files changed, 176 insertions(+), 93 deletions(-) diff --git a/readme.md b/readme.md index 1dcaa40d..89003cc3 100644 --- a/readme.md +++ b/readme.md @@ -92,6 +92,8 @@ Running the Viewer Double-click or launch `vis/index.html` from the browser. +Use Bash-like brace expansions in the `Connection Address Pattern` field to view multiple processes. + Sampling CUDA GPU activity -------------------------- diff --git a/vis/Code/Console.js b/vis/Code/Console.js index 1f16cfe8..9cb6591f 100644 --- a/vis/Code/Console.js +++ b/vis/Code/Console.js @@ -5,7 +5,7 @@ Console = (function() var HEIGHT = 200; - function Console(wm, server) + function Console(wm) { // Create the window and its controls this.Window = wm.AddWindow("Console", 10, 10, 100, 100); @@ -33,15 +33,17 @@ Console = (function() // At a much lower frequency this will update the console window window.setInterval(Bind(UpdateHTML, this), 500); - // Setup log requests from the server - this.Server = server; - server.SetConsole(this); - server.AddMessageHandler("LOGM", Bind(OnLog, this)); - this.Window.SetOnResize(Bind(OnUserResize, this)); } + Console.prototype.SetServer = function(server) + { + // The server which will be receiving the console input. + this.Server = server; + } + + Console.prototype.Log = function(text) { this.PageTextBuffer = LogText(this.PageTextBuffer, text); @@ -65,15 +67,15 @@ Console = (function() } - function OnLog(self, socket, data_view_reader) + Console.prototype.OnLog = function(server, socket, data_view_reader) { var text = data_view_reader.GetString(); - self.AppTextBuffer = LogText(self.AppTextBuffer, text); + this.AppTextBuffer = LogText(this.AppTextBuffer, text); // Don't register text as updating if disconnected as this implies a trace is being loaded, which we want to speed up - if (self.Server.Connected()) + if (server.Connected()) { - self.AppTextUpdatePending = true; + this.AppTextUpdatePending = true; } } diff --git a/vis/Code/NameMap.js b/vis/Code/NameMap.js index cd5853d2..5542ef63 100644 --- a/vis/Code/NameMap.js +++ b/vis/Code/NameMap.js @@ -6,10 +6,10 @@ class NameMap this.textBuffer = text_buffer; } - Get(name_hash) + Get(server_id, name_hash) { // Return immediately if it's in the hash - let name = this.names[name_hash]; + let name = this.names[[server_id, name_hash]]; if (name != undefined) { return [ true, name ]; @@ -18,9 +18,9 @@ class NameMap // Create a temporary name that uses the hash name = { string: name_hash.toString(), - hash: name_hash + hash: [server_id, name_hash] }; - this.names[name_hash] = name; + this.names[[server_id, name_hash]] = name; // Add to the text buffer the first time this name is encountered name.textEntry = this.textBuffer.AddText(name.string); @@ -28,17 +28,17 @@ class NameMap return [ false, name ]; } - Set(name_hash, name_string) + Set(server_id, name_hash, name_string) { // Create the name on-demand if its hash doesn't exist - let name = this.names[name_hash]; + let name = this.names[[server_id, name_hash]]; if (name == undefined) { name = { string: name_string, - hash: name_hash + hash: [server_id, name_hash] }; - this.names[name_hash] = name; + this.names[[server_id, name_hash]] = name; } else { diff --git a/vis/Code/Remotery.js b/vis/Code/Remotery.js index 14b2d222..295503fc 100644 --- a/vis/Code/Remotery.js +++ b/vis/Code/Remotery.js @@ -52,18 +52,18 @@ Remotery = (function() else this.ConnectionAddress = LocalStore.Get("App", "Global", "ConnectionAddress", "ws://127.0.0.1:17815/rmt"); - this.Server = new WebSocketConnection(); - this.Server.AddConnectHandler(Bind(OnConnect, this)); - this.Server.AddDisconnectHandler(Bind(OnDisconnect, this)); + this.got_first_connection = false; + + this.Servers = []; this.glCanvas = new GLCanvas(100, 100); this.glCanvas.SetOnDraw((gl, seconds) => this.OnGLCanvasDraw(gl, seconds)); // Create the console up front as everything reports to it - this.Console = new Console(this.WindowManager, this.Server); + this.Console = new Console(this.WindowManager); // Create required windows - this.TitleWindow = new TitleWindow(this.WindowManager, this.Settings, this.Server, this.ConnectionAddress); + this.TitleWindow = new TitleWindow(this.WindowManager, this.Settings, this.ConnectionAddress); this.TitleWindow.SetConnectionAddressChanged(Bind(OnAddressChanged, this)); this.SampleTimelineWindow = new TimelineWindow(this.WindowManager, "Sample Timeline", this.Settings, Bind(OnTimelineCheck, this), this.glCanvas); this.SampleTimelineWindow.SetOnHover(Bind(OnSampleHover, this)); @@ -82,11 +82,6 @@ Remotery = (function() this.PropertyFrameHistory = [ ]; this.SelectedFrames = { }; - this.Server.AddMessageHandler("SMPL", Bind(OnSamples, this)); - this.Server.AddMessageHandler("SSMP", Bind(OnSampleName, this)); - this.Server.AddMessageHandler("PRTH", Bind(OnProcessorThreads, this)); - this.Server.AddMessageHandler("PSNP", Bind(OnPropertySnapshots, this)); - // Kick-off the auto-connect loop AutoConnect(this); @@ -110,8 +105,13 @@ Remotery = (function() } this.nbGridWindows = 0; this.gridWindows = { }; + this.propertyGridWindows = []; - this.propertyGridWindow = this.AddGridWindow("__rmt__global__properties__", "Global Properties", new GridConfigProperties()); + for (let server_id = 0; server_id < this.Servers.length; ++server_id) { + const window_name = "__rmt__global__properties" + (this.Servers.length > 1 ? "_" + server_id : "") + "__"; + const window_display_name = "Global Properties" + (this.Servers.length > 1 ? " " + server_id : ""); + this.propertyGridWindows.push(this.AddGridWindow(window_name, window_display_name, new GridConfigProperties())); + } // Clear runtime data this.FrameHistory = { }; @@ -168,10 +168,38 @@ Remotery = (function() function AutoConnect(self) { - // Only attempt to connect if there isn't already a connection or an attempt to connect - if (!self.Server.Connected() && !self.Server.Connecting()) - { - self.Server.Connect(self.ConnectionAddress); + const connection_addresses = expandTop(self.ConnectionAddress); + const servers_length = self.Servers.length; + + for (let server_id = servers_length; server_id < connection_addresses.length; ++server_id) { + connection_address = connection_addresses[server_id]; + let server = new WebSocketConnection(); + server.AddConnectHandler(Bind(OnConnect, self, server_id)); + server.AddDisconnectHandler(Bind(OnDisconnect, self, server_id)); + // Setup log requests from the server + server.SetConsole(self.Console); + server.AddMessageHandler("LOGM", Bind(self.Console.OnLog.bind(self.Console), server)); + server.AddMessageHandler("PING", Bind(self.TitleWindow.OnPing.bind(self.TitleWindow), server_id, connection_addresses.length)); + server.AddMessageHandler("SSST", Bind(OnSamplesStart, self, server_id)); + server.AddMessageHandler("SMPL", Bind(OnSamples, self, server_id)); + server.AddMessageHandler("SSMP", Bind(OnSampleName, self, server_id)); + server.AddMessageHandler("PRTH", Bind(OnProcessorThreads, self, server_id)); + server.AddMessageHandler("PSNP", Bind(OnPropertySnapshots, self, server_id)); + self.Servers.push(server); + } + + for (let server_id = 0; server_id < connection_addresses.length; ++server_id) { + // Only attempt to connect if there isn't already a connection or an attempt to connect + if (!self.Servers[server_id].Connected() && !self.Servers[server_id].Connecting()) + { + self.Servers[server_id].Connect(connection_addresses[server_id]); + } + } + + self.Servers.length = connection_addresses.length; + + if (self.Servers.length > 0) { + self.Console.SetServer(self.Servers[0]); } // Always schedule another check @@ -179,21 +207,31 @@ Remotery = (function() } - function OnConnect(self) + function OnConnect(self, server_id) { - // Connection address has been validated - LocalStore.Set("App", "Global", "ConnectionAddress", self.ConnectionAddress); + self.Servers[server_id].Send("GSST"); + + if (!self.got_first_connection) { + // Connection address has been validated + LocalStore.Set("App", "Global", "ConnectionAddress", self.ConnectionAddress); - self.Clear(); + self.Clear(); - // Ensure the viewer is ready for realtime updates - self.TitleWindow.Unpause(); + // Ensure the viewer is ready for realtime updates + self.TitleWindow.Unpause(); + + self.got_first_connection = true; + } } - function OnDisconnect(self) + function OnDisconnect(self, server_id) { - // Pause so the user can inspect the trace - self.TitleWindow.Pause(); + if (self.Servers.every((server) => !server.Connected())) { + // Pause so the user can inspect the trace + self.TitleWindow.Pause(); + + self.got_first_connection = false; + } } @@ -201,7 +239,11 @@ Remotery = (function() { // Update and disconnect, relying on auto-connect to reconnect self.ConnectionAddress = node.value; - self.Server.Disconnect(); + this.got_first_connection = true; + + for (const server of self.Servers) { + server.Disconnect(); + } // Give input focus away return false; @@ -250,7 +292,7 @@ Remotery = (function() } - function ProcessSampleTree(self, sample_data, message) + function ProcessSampleTree(self, server_id, sample_data, message) { const empty_text_entry = { offset: 0, @@ -265,14 +307,14 @@ Remotery = (function() { // Get name hash and lookup in name map const name_hash = samples_view.getUint32(offset, true); - const [ name_exists, name ] = self.glCanvas.nameMap.Get(name_hash); + const [ name_exists, name ] = self.glCanvas.nameMap.Get(server_id, name_hash); // If the name doesn't exist in the map yet, request it from the server if (!name_exists) { - if (self.Server.Connected()) + if (self.Servers[server_id].Connected()) { - self.Server.Send("GSMP" + name_hash); + self.Servers[server_id].Send("GSMP" + name_hash); } } @@ -296,11 +338,28 @@ Remotery = (function() message.sampleFloats = new Float32Array(sample_data, message.samplesStart, message.nb_samples * g_nbFloatsPerSample); } - function OnSamples(self, socket, data_view_reader, length) + + function OnSamplesStart(self, server_id, socket, data_view_reader) + { + self.Servers[server_id].counter_start = data_view_reader.GetUInt64(); + } + + + function UpdateTimelineOffsets(self, timeline_window) + { + const counter_start_min = Math.min.apply(null, self.Servers.map(function(server) { return server.counter_start; })); + for (let i = 0; i < self.Servers.length; ++i) { + const counter_offset = self.Servers[i].counter_start - counter_start_min; + timeline_window.SetCounterOffset(i, counter_offset); + } + } + + + function OnSamples(self, server_id, socket, data_view_reader, length) { // Discard any new samples while paused and connected // Otherwise this stops a paused Remotery from loading new samples from disk - if (self.Settings.IsPaused && self.Server.Connected()) + if (self.Settings.IsPaused && self.Servers[server_id].Connected()) return; // Binary decode incoming sample data @@ -309,8 +368,8 @@ Remotery = (function() { return; } - var name = message.thread_name; - ProcessSampleTree(self, data_view_reader.DataView.buffer, message); + var name = (self.Servers.length > 1 ? server_id + "_" : "") + message.thread_name; + ProcessSampleTree(self, server_id, data_view_reader.DataView.buffer, message); // Add to frame history for this thread var thread_frame = new ThreadFrame(message); @@ -342,29 +401,30 @@ Remotery = (function() } // Set on the window and timeline if connected as this implies a trace is being loaded, which we want to speed up - if (self.Server.Connected()) + if (self.Servers[server_id].Connected()) { self.gridWindows[name].UpdateEntries(message.nb_samples, message.sampleFloats); - self.SampleTimelineWindow.OnSamples(name, frame_history); + self.SampleTimelineWindow.OnSamples(server_id, name, frame_history); + UpdateTimelineOffsets(self, self.SampleTimelineWindow); } } - function OnSampleName(self, socket, data_view_reader) + function OnSampleName(self, server_id, socket, data_view_reader) { // Add any names sent by the server to the local map let name_hash = data_view_reader.GetUInt32(); let name_string = data_view_reader.GetString(); - self.glCanvas.nameMap.Set(name_hash, name_string); + self.glCanvas.nameMap.Set(server_id, name_hash, name_string); } - function OnProcessorThreads(self, socket, data_view_reader) + function OnProcessorThreads(self, server_id, socket, data_view_reader) { // Discard any new samples while paused and connected // Otherwise this stops a paused Remotery from loading new samples from disk - if (self.Settings.IsPaused && self.Server.Connected()) + if (self.Settings.IsPaused && self.Servers[server_id].Connected()) return; let nb_processors = data_view_reader.GetUInt32(); @@ -383,7 +443,7 @@ Remotery = (function() let sample_time = data_view_reader.GetUInt64(); // Add frame history for this processor - let processor_name = "Processor " + i.toString(); + let processor_name = (self.Servers.length > 1 ? server_id + " " : "") + "Processor " + i.toString(); if (!(processor_name in self.ProcessorFrameHistory)) { self.ProcessorFrameHistory[processor_name] = [ ]; @@ -422,16 +482,16 @@ Remotery = (function() { frame_history.splice(0, extra_frames); } - + // Lookup the thread name - let [ name_exists, thread_name ] = self.glCanvas.nameMap.Get(thread_name_hash); + let [ name_exists, thread_name ] = self.glCanvas.nameMap.Get(server_id, thread_name_hash); // If the name doesn't exist in the map yet, request it from the server if (!name_exists) { - if (self.Server.Connected()) + if (self.Servers[server_id].Connected()) { - self.Server.Send("GSMP" + thread_name_hash); + self.Servers[server_id].Send("GSMP" + thread_name_hash); } } @@ -463,14 +523,16 @@ Remotery = (function() // Create a thread frame and annotate with data required to merge processor samples let thread_frame = new ThreadFrame(thread_message); + thread_frame.serverId = server_id; thread_frame.threadId = thread_id; thread_frame.messageIndex = message_index; thread_frame.usLastStart = sample_time; frame_history.push(thread_frame); - if (self.Server.Connected()) + if (self.Servers[server_id].Connected()) { - self.ProcessorTimelineWindow.OnSamples(processor_name, frame_history); + self.ProcessorTimelineWindow.OnSamples(server_id, processor_name, frame_history); + UpdateTimelineOffsets(self, self.ProcessorTimelineWindow); } } } @@ -527,7 +589,7 @@ Remotery = (function() } - function ProcessSnapshots(self, snapshot_data, message) + function ProcessSnapshots(self, server_id, snapshot_data, message) { if (self.Settings.IsPaused) { @@ -546,14 +608,14 @@ Remotery = (function() { // Get name hash and lookup in name map const name_hash = snapshots_view.getUint32(offset, true); - const [ name_exists, name ] = self.glCanvas.nameMap.Get(name_hash); + const [ name_exists, name ] = self.glCanvas.nameMap.Get(server_id, name_hash); // If the name doesn't exist in the map yet, request it from the server if (!name_exists) { - if (self.Server.Connected()) + if (self.Servers[server_id].Connected()) { - self.Server.Send("GSMP" + name_hash); + self.Servers[server_id].Send("GSMP" + name_hash); } } @@ -603,16 +665,16 @@ Remotery = (function() } - function OnPropertySnapshots(self, socket, data_view_reader, length) + function OnPropertySnapshots(self, server_id, socket, data_view_reader, length) { // Discard any new snapshots while paused and connected // Otherwise this stops a paused Remotery from loading new samples from disk - if (self.Settings.IsPaused && self.Server.Connected()) + if (self.Settings.IsPaused && self.Servers[server_id].Connected()) return; // Binary decode incoming snapshot data const message = DecodeSnapshotHeader(self, data_view_reader, length); - message.snapshotFloats = ProcessSnapshots(self, data_view_reader.DataView.buffer, message); + message.snapshotFloats = ProcessSnapshots(self, server_id, data_view_reader.DataView.buffer, message); // Add to frame history const thread_frame = new PropertySnapshotFrame(message); @@ -626,9 +688,9 @@ Remotery = (function() frame_history.splice(0, extra_frames); // Set on the window if connected as this implies a trace is being loaded, which we want to speed up - if (self.Server.Connected()) + if (self.Servers[server_id].Connected()) { - self.propertyGridWindow.UpdateEntries(message.nbSnapshots, message.snapshotFloats); + self.propertyGridWindows[server_id].UpdateEntries(message.nbSnapshots, message.snapshotFloats); } } diff --git a/vis/Code/TimelineRow.js b/vis/Code/TimelineRow.js index 738b9d5e..b2ade521 100644 --- a/vis/Code/TimelineRow.js +++ b/vis/Code/TimelineRow.js @@ -23,8 +23,9 @@ TimelineRow = (function() var SAMPLE_Y_SPACING = SAMPLE_HEIGHT + SAMPLE_BORDER * 2; - function TimelineRow(gl, name, timeline, frame_history, check_handler) + function TimelineRow(gl, server_id, name, timeline, frame_history, check_handler) { + this.server_id = server_id; this.Name = name; this.timeline = timeline; @@ -82,6 +83,12 @@ TimelineRow = (function() } + TimelineRow.prototype.SetCounterOffset = function(counter_offset) + { + this.counter_offset = counter_offset; + } + + TimelineRow.prototype.SetVisibleFrames = function(time_range) { // Clear previous visible list @@ -103,7 +110,7 @@ TimelineRow = (function() while (start_frame_index > 0) { var frame = this.FrameHistory[start_frame_index]; - if (time_range.Start_us > frame.StartTime_us) + if (time_range.Start_us > frame.StartTime_us + this.counter_offset) break; start_frame_index--; } @@ -112,7 +119,7 @@ TimelineRow = (function() while (start_frame_index < this.FrameHistory.length) { var frame = this.FrameHistory[start_frame_index]; - if (frame.EndTime_us > time_range.Start_us) + if (frame.EndTime_us + this.counter_offset > time_range.Start_us) break; start_frame_index++; } @@ -122,7 +129,7 @@ TimelineRow = (function() for (var i = start_frame_index; i < this.FrameHistory.length; i++) { var frame = this.FrameHistory[i]; - if (frame.StartTime_us > time_range.End_us) + if (frame.StartTime_us + this.counter_offset > time_range.End_us) break; this.VisibleFrames.push(frame); } @@ -144,7 +151,7 @@ TimelineRow = (function() // Set sample parameters const float_offset = offset / 4; - glSetUniform(gl, program, "inStartMs", frame.sampleFloats[float_offset + g_sampleOffsetFloats_Start]); + glSetUniform(gl, program, "inStartMs", frame.sampleFloats[float_offset + g_sampleOffsetFloats_Start] + this.counter_offset / 1000.); glSetUniform(gl, program, "inLengthMs", frame.sampleFloats[float_offset + g_sampleOffsetFloats_Length]); glSetUniform(gl, program, "inDepth", depth); @@ -177,7 +184,7 @@ TimelineRow = (function() glSetUniform(gl, program, "inRow.yOffset", row_rect.top); // Set sample parameters - const length_ms = frame.sampleFloats[float_offset + g_sampleOffsetFloats_Start] - start_ms; + const length_ms = frame.sampleFloats[float_offset + g_sampleOffsetFloats_Start] - start_ms + this.counter_offset / 1000.; glSetUniform(gl, program, "inStartMs", start_ms); glSetUniform(gl, program, "inLengthMs", length_ms); glSetUniform(gl, program, "inDepth", depth); @@ -277,7 +284,7 @@ TimelineRow = (function() { const float_offset = offset / 4; - cpu_samples[sample_pos + 0] = frame.sampleFloats[float_offset + g_sampleOffsetFloats_Start]; + cpu_samples[sample_pos + 0] = frame.sampleFloats[float_offset + g_sampleOffsetFloats_Start] + this.counter_offset / 1000.; cpu_samples[sample_pos + 1] = frame.sampleFloats[float_offset + g_sampleOffsetFloats_Length]; cpu_samples[sample_pos + 2] = depth; cpu_samples[sample_pos + 3] = frame.sampleFloats[float_offset + g_sampleOffsetFloats_NameOffset]; @@ -360,7 +367,7 @@ TimelineRow = (function() // smaller than a pixel. This feels pretty odd and the closed interval fixes this feeling well. // TODO(don): There are still inconsistencies, need to shift to pixel range checking to match exactly. const frame = this.VisibleFrames[i]; - if (time_us >= frame.StartTime_us && time_us <= frame.EndTime_us) + if (time_us >= frame.StartTime_us + this.counter_offset && time_us <= frame.EndTime_us + this.counter_offset) { const found_sample = FindSample(this, frame, time_us, depth, 1); if (found_sample != null) @@ -383,7 +390,7 @@ TimelineRow = (function() depth = sample_data_view.getUint8(offset + g_sampleOffsetBytes_Depth) + 1; if (depth == target_depth) { - const us_start = sample_data_view.getFloat32(offset + g_sampleOffsetBytes_Start, true) * 1000.0; + const us_start = sample_data_view.getFloat32(offset + g_sampleOffsetBytes_Start, true) * 1000.0 + self.counter_offset; const us_length = sample_data_view.getFloat32(offset + g_sampleOffsetBytes_Length, true) * 1000.0; if (time_us >= us_start && time_us < us_start + us_length) { diff --git a/vis/Code/TimelineWindow.js b/vis/Code/TimelineWindow.js index 383c2f7a..9220f3e8 100644 --- a/vis/Code/TimelineWindow.js +++ b/vis/Code/TimelineWindow.js @@ -96,17 +96,29 @@ TimelineWindow = (function() } - TimelineWindow.prototype.OnSamples = function(thread_name, frame_history) + TimelineWindow.prototype.SetCounterOffset = function(server_id, counter_offset) + { + for (let row of this.ThreadRows) + { + if (row.server_id == server_id) + { + row.SetCounterOffset(counter_offset); + } + } + } + + + TimelineWindow.prototype.OnSamples = function(server_id, thread_name, frame_history) { // Shift the timeline to the last entry on this thread var last_frame = frame_history[frame_history.length - 1]; - this.TimeRange.SetEnd(last_frame.EndTime_us); + this.TimeRange.SetEnd(Math.max(this.TimeRange.End_us, last_frame.EndTime_us)); // Search for the index of this thread var thread_index = -1; for (var i in this.ThreadRows) { - if (this.ThreadRows[i].Name == thread_name) + if (this.ThreadRows[i].server_id == server_id && this.ThreadRows[i].Name == thread_name) { thread_index = i; break; @@ -116,7 +128,7 @@ TimelineWindow = (function() // If this thread has not been seen before, add a new row to the list if (thread_index == -1) { - var row = new TimelineRow(this.glCanvas.gl, thread_name, this, frame_history, this.CheckHandler); + var row = new TimelineRow(this.glCanvas.gl, server_id, thread_name, this, frame_history, this.CheckHandler); this.ThreadRows.push(row); // Sort thread rows in the collection by name diff --git a/vis/Code/TitleWindow.js b/vis/Code/TitleWindow.js index 7c55d3e2..7f18b9b9 100644 --- a/vis/Code/TitleWindow.js +++ b/vis/Code/TitleWindow.js @@ -1,7 +1,7 @@ TitleWindow = (function() { - function TitleWindow(wm, settings, server, connection_address) + function TitleWindow(wm, settings, connection_address) { this.Settings = settings; @@ -11,7 +11,7 @@ TitleWindow = (function() this.PingContainer = this.Window.AddControlNew(new WM.Container(4, -13, 10, 10)); DOM.Node.AddClass(this.PingContainer.Node, "PingContainer"); - this.EditBox = this.Window.AddControlNew(new WM.EditBox(10, 5, 300, 18, "Connection Address", connection_address)); + this.EditBox = this.Window.AddControlNew(new WM.EditBox(10, 5, 300, 18, "Connection Address Pattern", connection_address)); // Setup pause button this.PauseButton = this.Window.AddControlNew(new WM.Button("Pause", 5, 5, { toggle: true })); @@ -21,8 +21,6 @@ TitleWindow = (function() this.SyncButton.SetOnClick(Bind(OnSyncPressed, this)); this.SyncButton.SetState(this.Settings.SyncTimelines); - server.AddMessageHandler("PING", Bind(OnPing, this)); - this.Window.SetOnResize(Bind(OnUserResize, this)); } @@ -90,14 +88,14 @@ TitleWindow = (function() } - function OnPing(self, server) + TitleWindow.prototype.OnPing = function(server_id, servers_length) { // Set the ping container as active and take it off half a second later - DOM.Node.AddClass(self.PingContainer.Node, "PingContainerActive"); + DOM.Node.AddClass(this.PingContainer.Node, "PingContainerActive"); window.setTimeout(Bind(function(self) { DOM.Node.RemoveClass(self.PingContainer.Node, "PingContainerActive"); - }, self), 500); + }, this), 500); }