Add profiling with Remotery

This commit is contained in:
David 2021-08-24 20:55:39 +02:00
commit 6331a2bf79
50 changed files with 16864 additions and 11 deletions

218
vis/Code/Console.js Normal file
View file

@ -0,0 +1,218 @@
Console = (function()
{
var BORDER = 10;
var HEIGHT = 200;
function Console(wm, server)
{
// Create the window and its controls
this.Window = wm.AddWindow("Console", 10, 10, 100, 100);
this.PageContainer = this.Window.AddControlNew(new WM.Container(10, 10, 400, 160));
DOM.Node.AddClass(this.PageContainer.Node, "ConsoleText");
this.AppContainer = this.Window.AddControlNew(new WM.Container(10, 10, 400, 160));
DOM.Node.AddClass(this.AppContainer.Node, "ConsoleText");
this.UserInput = this.Window.AddControlNew(new WM.EditBox(10, 5, 400, 30, "Input", ""));
this.UserInput.SetChangeHandler(Bind(ProcessInput, this));
this.Window.ShowNoAnim();
// This accumulates log text as fast as is required
this.PageTextBuffer = "";
this.PageTextUpdatePending = false;
this.AppTextBuffer = "";
this.AppTextUpdatePending = false;
// Setup command history control
this.CommandHistory = LocalStore.Get("App", "Global", "CommandHistory", [ ]);
this.CommandIndex = 0;
this.MaxNbCommands = 10000;
DOM.Event.AddHandler(this.UserInput.EditNode, "keydown", Bind(OnKeyPress, this));
DOM.Event.AddHandler(this.UserInput.EditNode, "focus", Bind(OnFocus, this));
// 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.Log = function(text)
{
this.PageTextBuffer = LogText(this.PageTextBuffer, text);
this.PageTextUpdatePending = true;
}
Console.prototype.WindowResized = function(width, height)
{
// Place window
this.Window.SetPosition(BORDER, height - BORDER - 200);
this.Window.SetSize(width - 2 * BORDER, HEIGHT);
ResizeInternals(this);
}
Console.prototype.TriggerUpdate = function()
{
this.AppTextUpdatePending = true;
}
function OnLog(self, socket, data_view_reader)
{
var text = data_view_reader.GetString();
self.AppTextBuffer = LogText(self.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())
{
self.AppTextUpdatePending = true;
}
}
function LogText(existing_text, new_text)
{
// Filter the text a little to make it safer
if (new_text == null)
new_text = "NULL";
// Find and convert any HTML entities, ensuring the browser doesn't parse any embedded HTML code
// This also allows the log to contain arbitrary C++ code (e.g. assert comparison operators)
new_text = Convert.string_to_html_entities(new_text);
// Prefix date and end with new line
var d = new Date();
new_text = "[" + d.toLocaleTimeString() + "] " + new_text + "<br>";
// Append to local text buffer and ensure clip the oldest text to ensure a max size
existing_text = existing_text + new_text;
var max_len = 100 * 1024;
var len = existing_text.length;
if (len > max_len)
existing_text = existing_text.substr(len - max_len, max_len);
return existing_text;
}
function OnUserResize(self, evt)
{
ResizeInternals(self);
}
function ResizeInternals(self)
{
// Place controls
var parent_size = self.Window.Size;
var mid_w = parent_size[0] / 3;
self.UserInput.SetPosition(BORDER, parent_size[1] - 2 * BORDER - 30);
self.UserInput.SetSize(parent_size[0] - 100, 18);
var output_height = self.UserInput.Position[1] - 2 * BORDER;
self.PageContainer.SetPosition(BORDER, BORDER);
self.PageContainer.SetSize(mid_w - 2 * BORDER, output_height);
self.AppContainer.SetPosition(mid_w, BORDER);
self.AppContainer.SetSize(parent_size[0] - mid_w - BORDER, output_height);
}
function UpdateHTML(self)
{
// Reset the current text buffer as html
if (self.PageTextUpdatePending)
{
var page_node = self.PageContainer.Node;
page_node.innerHTML = self.PageTextBuffer;
page_node.scrollTop = page_node.scrollHeight;
self.PageTextUpdatePending = false;
}
if (self.AppTextUpdatePending)
{
var app_node = self.AppContainer.Node;
app_node.innerHTML = self.AppTextBuffer;
app_node.scrollTop = app_node.scrollHeight;
self.AppTextUpdatePending = false;
}
}
function ProcessInput(self, node)
{
// Send the message exactly
var msg = node.value;
self.Server.Send("CONI" + msg);
// Emit to console and clear
self.Log("> " + msg);
self.UserInput.SetValue("");
// Keep track of recently issued commands, with an upper bound
self.CommandHistory.push(msg);
var extra_commands = self.CommandHistory.length - self.MaxNbCommands;
if (extra_commands > 0)
self.CommandHistory.splice(0, extra_commands);
// Set command history index to the most recent command
self.CommandIndex = self.CommandHistory.length;
// Backup to local store
LocalStore.Set("App", "Global", "CommandHistory", self.CommandHistory);
// Keep focus with the edit box
return true;
}
function OnKeyPress(self, evt)
{
evt = DOM.Event.Get(evt);
if (evt.keyCode == Keyboard.Codes.UP)
{
if (self.CommandHistory.length > 0)
{
// Cycle backwards through the command history
self.CommandIndex--;
if (self.CommandIndex < 0)
self.CommandIndex = self.CommandHistory.length - 1;
var command = self.CommandHistory[self.CommandIndex];
self.UserInput.SetValue(command);
}
// Stops default behaviour of moving cursor to the beginning
DOM.Event.StopDefaultAction(evt);
}
else if (evt.keyCode == Keyboard.Codes.DOWN)
{
if (self.CommandHistory.length > 0)
{
// Cycle fowards through the command history
self.CommandIndex = (self.CommandIndex + 1) % self.CommandHistory.length;
var command = self.CommandHistory[self.CommandIndex];
self.UserInput.SetValue(command);
}
// Stops default behaviour of moving cursor to the end
DOM.Event.StopDefaultAction(evt);
}
}
function OnFocus(self)
{
// Reset command index on focus
self.CommandIndex = self.CommandHistory.length;
}
return Console;
})();

View file

@ -0,0 +1,52 @@
//
// Simple wrapper around DataView that auto-advances the read offset and provides
// a few common data type conversions specific to this app
//
DataViewReader = (function ()
{
function DataViewReader(data_view, offset)
{
this.DataView = data_view;
this.Offset = offset;
}
DataViewReader.prototype.AtEnd = function()
{
return this.Offset >= this.DataView.byteLength;
}
DataViewReader.prototype.GetUInt32 = function ()
{
var v = this.DataView.getUint32(this.Offset, true);
this.Offset += 4;
return v;
}
DataViewReader.prototype.GetUInt64 = function ()
{
var v = this.DataView.getFloat64(this.Offset, true);
this.Offset += 8;
return v;
}
DataViewReader.prototype.GetStringOfLength = function (string_length)
{
var string = "";
for (var i = 0; i < string_length; i++)
{
string += String.fromCharCode(this.DataView.getInt8(this.Offset));
this.Offset++;
}
return string;
}
DataViewReader.prototype.GetString = function ()
{
var string_length = this.GetUInt32();
return this.GetStringOfLength(string_length);
}
return DataViewReader;
})();

53
vis/Code/NameMap.js Normal file
View file

@ -0,0 +1,53 @@
class NameMap
{
constructor(text_buffer)
{
this.names = { };
this.textBuffer = text_buffer;
}
Get(name_hash)
{
// Return immediately if it's in the hash
let name = this.names[name_hash];
if (name != undefined)
{
return [ true, name ];
}
// Create a temporary name that uses the hash
name = {
string: name_hash.toString(),
hash: name_hash
};
this.names[name_hash] = name;
// Add to the text buffer the first time this name is encountered
name.textEntry = this.textBuffer.AddText(name.string);
return [ false, name ];
}
Set(name_hash, name_string)
{
// Create the name on-demand if its hash doesn't exist
let name = this.names[name_hash];
if (name == undefined)
{
name = {
string: name_string,
hash: name_hash
};
this.names[name_hash] = name;
}
else
{
name.string = name_string;
}
// Apply the updated text to the buffer
name.textEntry = this.textBuffer.AddText(name_string);
return name;
}
}

View file

@ -0,0 +1,61 @@
class PixelTimeRange
{
constructor(start_us, span_us, span_px)
{
this.Span_px = span_px;
this.Set(start_us, span_us);
}
Set(start_us, span_us)
{
this.Start_us = start_us;
this.Span_us = span_us;
this.End_us = this.Start_us + span_us;
this.usPerPixel = this.Span_px / this.Span_us;
}
SetStart(start_us)
{
this.Start_us = start_us;
this.End_us = start_us + this.Span_us;
}
SetEnd(end_us)
{
this.End_us = end_us;
this.Start_us = end_us - this.Span_us;
}
SetPixelSpan(span_px)
{
this.Span_px = span_px;
this.usPerPixel = this.Span_px / this.Span_us;
}
PixelOffset(time_us)
{
return Math.floor((time_us - this.Start_us) * this.usPerPixel);
}
PixelSize(time_us)
{
return Math.floor(time_us * this.usPerPixel);
}
TimeAtPosition(position)
{
return this.Start_us + position / this.usPerPixel;
}
Clone()
{
return new PixelTimeRange(this.Start_us, this.Span_us, this.Span_px);
}
SetAsUniform(gl, program)
{
glSetUniform(gl, program, "inTimeRange.usStart", this.Start_us);
glSetUniform(gl, program, "inTimeRange.usPerPixel", this.usPerPixel);
}
}

540
vis/Code/Remotery.js Normal file
View file

@ -0,0 +1,540 @@
//
// TODO: Window resizing needs finer-grain control
// TODO: Take into account where user has moved the windows
// TODO: Controls need automatic resizing within their parent windows
//
Settings = (function()
{
function Settings()
{
this.IsPaused = false;
this.SyncTimelines = true;
}
return Settings;
})();
Remotery = (function()
{
// crack the url and get the parameter we want
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++)
{
param_name = url_vars[i].split('=');
if (param_name[0] === search_param)
{
return param_name[1] === undefined ? true : param_name[1];
}
}
};
function Remotery()
{
this.WindowManager = new WM.WindowManager();
this.Settings = new Settings();
// "addr" param is ip:port and will override the local store version if passed in the URL
var addr = getUrlParameter( "addr" );
if ( addr != null )
this.ConnectionAddress = "ws://" + addr + "/rmt";
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));
// Create the console up front as everything reports to it
this.Console = new Console(this.WindowManager, this.Server);
// Create required windows
this.TitleWindow = new TitleWindow(this.WindowManager, this.Settings, this.Server, this.ConnectionAddress);
this.TitleWindow.SetConnectionAddressChanged(Bind(OnAddressChanged, this));
this.SampleTimelineWindow = new TimelineWindow(this.WindowManager, "Sample Timeline", this.Settings, Bind(OnTimelineCheck, this));
this.SampleTimelineWindow.SetOnHover(Bind(OnSampleHover, this));
this.SampleTimelineWindow.SetOnSelected(Bind(OnSampleSelected, this));
this.ProcessorTimelineWindow = new TimelineWindow(this.WindowManager, "Processor Timeline", this.Settings, null);
this.SampleTimelineWindow.SetOnMoved(Bind(OnTimelineMoved, this));
this.ProcessorTimelineWindow.SetOnMoved(Bind(OnTimelineMoved, this));
this.TraceDrop = new TraceDrop(this);
this.NbSampleWindows = 0;
this.SampleWindows = { };
this.FrameHistory = { };
this.ProcessorFrameHistory = { };
this.SelectedFrames = { };
this.sampleNames = new NameMap(this.SampleTimelineWindow.textBuffer);
this.threadNames = new NameMap(this.ProcessorTimelineWindow.textBuffer);
this.Server.AddMessageHandler("SMPL", Bind(OnSamples, this));
this.Server.AddMessageHandler("SSMP", Bind(OnSampleName, this));
this.Server.AddMessageHandler("PRTH", Bind(OnProcessorThreads, this));
this.Server.AddMessageHandler("THRN", Bind(OnThreadNames, this));
// Kick-off the auto-connect loop
AutoConnect(this);
// Hook up resize event handler
DOM.Event.AddHandler(window, "resize", Bind(OnResizeWindow, this));
OnResizeWindow(this);
// Hook up browser-native canvas refresh
this.DisplayFrame = 0;
this.LastKnownPause = this.Settings.IsPaused;
var self = this;
(function display_loop()
{
window.requestAnimationFrame(display_loop);
DrawTimeline(self);
})();
}
Remotery.prototype.Clear = function()
{
// Clear timelines
this.SampleTimelineWindow.Clear();
this.ProcessorTimelineWindow.Clear();
// Close and clear all sample windows
for (var i in this.SampleWindows)
{
var sample_window = this.SampleWindows[i];
sample_window.Close();
}
this.NbSampleWindows = 0;
this.SampleWindows = { };
// Clear runtime data
this.FrameHistory = { };
this.ProcessorFrameHistory = { };
this.SelectedFrames = { };
this.sampleNames = new NameMap(this.SampleTimelineWindow.textBuffer);
this.threadNames = new NameMap(this.ProcessorTimelineWindow.textBuffer);
// Resize everything to fit new layout
OnResizeWindow(this);
}
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.Connect(self.ConnectionAddress);
// Always schedule another check
window.setTimeout(Bind(AutoConnect, self), 2000);
}
function OnConnect(self)
{
// Connection address has been validated
LocalStore.Set("App", "Global", "ConnectionAddress", self.ConnectionAddress);
self.Clear();
// Ensure the viewer is ready for realtime updates
self.TitleWindow.Unpause();
}
function OnDisconnect(self)
{
// Pause so the user can inspect the trace
self.TitleWindow.Pause();
}
function OnAddressChanged(self, node)
{
// Update and disconnect, relying on auto-connect to reconnect
self.ConnectionAddress = node.value;
self.Server.Disconnect();
// Give input focus away
return false;
}
function DrawTimeline(self)
{
// Has pause state changed?
if (self.Settings.IsPaused != self.LastKnownPaused)
{
// When switching TO paused, draw one last frame to ensure the sample text gets drawn
self.LastKnownPaused = self.Settings.IsPaused;
self.SampleTimelineWindow.DrawAllRows();
self.ProcessorTimelineWindow.DrawAllRows();
return;
}
// Don't waste time drawing the timeline when paused
if (self.Settings.IsPaused)
return;
// requestAnimationFrame can run up to 60hz which is way too much for drawing the timeline
// Assume it's running at 60hz and skip frames to achieve 10hz instead
// Doing this instead of using setTimeout because it's better for browser rendering (or; will be once WebGL is in use)
// TODO: Expose as config variable because high refresh rate is great when using a separate viewiing machine
if ((self.DisplayFrame % 10) == 0)
{
self.SampleTimelineWindow.DrawAllRows();
self.ProcessorTimelineWindow.DrawAllRows();
}
self.DisplayFrame++;
}
function DecodeSample(self, data_view_reader)
{
var sample = {};
// Get name hash and lookup name it map
sample.name_hash = data_view_reader.GetUInt32();
let [ name_exists, name ] = self.sampleNames.Get(sample.name_hash);
sample.name = name;
// If the name doesn't exist in the map yet, request it from the server
if (!name_exists)
{
if (self.Server.Connected())
{
self.Server.Send("GSMP" + sample.name_hash);
}
}
// Get the rest of the sample data
sample.id = data_view_reader.GetUInt32();
sample.colour = data_view_reader.GetStringOfLength(7);
sample.us_start = data_view_reader.GetUInt64();
sample.us_length = data_view_reader.GetUInt64();
sample.us_self = data_view_reader.GetUInt64();
sample.call_count = data_view_reader.GetUInt32();
sample.recurse_depth = data_view_reader.GetUInt32();
// TODO(don): Get the profiler to pass these directly instead of hex colour
const colour = parseInt(sample.colour.slice(1), 16);
const r = (colour >> 16) & 255;
const g = (colour >> 8) & 255;
const b = colour & 255;
sample.rgbColour = [ r, g, b ];
// Calculate dependent properties
sample.ms_length = (sample.us_length / 1000.0).toFixed(3);
sample.ms_self = (sample.us_self / 1000.0).toFixed(3);
// Recurse into children
sample.children = [];
DecodeSampleArray(self, data_view_reader, sample.children);
return sample;
}
function DecodeSampleArray(self, data_view_reader, samples)
{
var nb_samples = data_view_reader.GetUInt32();
for (var i = 0; i < nb_samples; i++)
{
var sample = DecodeSample(self, data_view_reader);
samples.push(sample)
}
}
function DecodeSamples(self, data_view_reader)
{
// Message-specific header
let message = { };
message.sample_tree_bytes = data_view_reader.GetUInt32();
message.thread_name = data_view_reader.GetString();
message.nb_samples = data_view_reader.GetUInt32();
message.sample_digest = data_view_reader.GetUInt32();
message.partial_tree = data_view_reader.GetUInt32();
// Read samples
message.samples = [];
message.samples.push(DecodeSample(self, data_view_reader));
return message;
}
function OnSamples(self, 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())
return;
// Binary decode incoming sample data
var message = DecodeSamples(self, data_view_reader);
var name = message.thread_name;
// Add to frame history for this thread
var thread_frame = new ThreadFrame(message);
if (!(name in self.FrameHistory))
{
self.FrameHistory[name] = [ ];
}
var frame_history = self.FrameHistory[name];
if (frame_history.length > 0 && frame_history[frame_history.length - 1].PartialTree)
{
// Always overwrite partial trees with new information
frame_history[frame_history.length - 1] = thread_frame;
}
else
{
frame_history.push(thread_frame);
}
// Discard old frames to keep memory-use constant
var max_nb_frames = 10000;
var extra_frames = frame_history.length - max_nb_frames;
if (extra_frames > 0)
frame_history.splice(0, extra_frames);
// Create sample windows on-demand
if (!(name in self.SampleWindows))
{
self.SampleWindows[name] = new SampleWindow(self.WindowManager, name, self.NbSampleWindows);
self.SampleWindows[name].WindowResized(self.SampleTimelineWindow.Window, self.Console.Window);
self.NbSampleWindows++;
MoveSampleWindows(this);
}
// 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())
{
self.SampleWindows[name].OnSamples(message.nb_samples, message.sample_digest, message.samples);
self.SampleTimelineWindow.OnSamples(name, frame_history);
}
}
function OnSampleName(self, 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.sampleNames.Set(name_hash, name_string);
}
function OnProcessorThreads(self, socket, data_view_reader)
{
let nb_processors = data_view_reader.GetUInt32();
let message_index = data_view_reader.GetUInt64();
// Decode each processor
for (let i = 0; i < nb_processors; i++)
{
let thread_id = data_view_reader.GetUInt32();
let thread_name_hash = data_view_reader.GetUInt32();
let sample_time = data_view_reader.GetUInt64();
// Add frame history for this processor
let processor_name = "Processor " + i.toString();
if (!(processor_name in self.ProcessorFrameHistory))
{
self.ProcessorFrameHistory[processor_name] = [ ];
}
let frame_history = self.ProcessorFrameHistory[processor_name];
if (thread_id == 0xFFFFFFFF)
{
continue;
}
// Try to merge this frame's samples with the previous frame if the are the same thread
if (frame_history.length > 0)
{
let last_thread_frame = frame_history[frame_history.length - 1];
if (last_thread_frame.threadId == thread_id && last_thread_frame.messageIndex == message_index - 1)
{
// Update last frame message index so that the next frame can check for continuity
last_thread_frame.messageIndex = message_index;
// Sum time elapsed on the previous frame
let us_length = sample_time - last_thread_frame.usLastStart;
last_thread_frame.usLastStart = sample_time;
last_thread_frame.EndTime_us += us_length;
last_thread_frame.Samples[0].us_length += us_length;
continue;
}
}
// Discard old frames to keep memory-use constant
var max_nb_frames = 10000;
var extra_frames = frame_history.length - max_nb_frames;
if (extra_frames > 0)
{
frame_history.splice(0, extra_frames);
}
// Lookup the thread name
let [ _, thread_name ] = self.threadNames.Get(thread_name_hash);
// Make a pastel-y colour from the thread name hash
let hash = thread_name.hash;
let r = 127 + (hash & 255) / 2;
let g = 127 + ((hash >> 4) & 255) / 2;
let b = 127 + ((hash >> 8) & 255) / 2;
// We are co-opting the sample rendering functionality of the timeline window to display processor threads as
// thread samples. Fabricate a thread frame message, packing the processor info into one root sample.
// TODO(don): Abstract the timeline window for pure range display as this is quite inefficient.
let thread_message = {
nb_samples: 1,
sample_digest: 0,
samples : [
{
name_hash: thread_name_hash,
name: thread_name,
id: thread_id,
colour: "#FFFFFF",
us_start: sample_time,
us_length: 250,
rgbColour: [ r, g, b ],
children: []
}
]
};
// Create a thread frame and annotate with data required to merge processor samples
let thread_frame = new ThreadFrame(thread_message);
thread_frame.threadId = thread_id;
thread_frame.messageIndex = message_index;
thread_frame.usLastStart = sample_time;
frame_history.push(thread_frame);
if (self.Server.Connected())
{
self.ProcessorTimelineWindow.OnSamples(processor_name, frame_history);
}
}
}
function OnThreadNames(self, socket, data_view_reader)
{
let name_hash = data_view_reader.GetUInt32();
let name_length = data_view_reader.GetUInt32();
let name_string = data_view_reader.GetStringOfLength(name_length);
self.threadNames.Set(name_hash, name_string);
}
function OnTimelineCheck(self, name, evt)
{
// Show/hide the equivalent sample window and move all the others to occupy any left-over space
var target = DOM.Event.GetNode(evt);
self.SampleWindows[name].SetVisible(target.checked);
MoveSampleWindows(self);
}
function MoveSampleWindows(self)
{
// Stack all windows next to each other
var xpos = 0;
for (var i in self.SampleWindows)
{
var sample_window = self.SampleWindows[i];
if (sample_window.Visible)
{
sample_window.SetXPos(xpos++, self.SampleTimelineWindow.Window, self.Console.Window);
}
}
}
function OnSampleHover(self, thread_name, hover)
{
if (!self.Settings.IsPaused)
{
return;
}
for (let window_thread_name in self.SampleWindows)
{
let sample_window = self.SampleWindows[window_thread_name];
if (window_thread_name == thread_name && hover != null)
{
// Populate with sample under hover
let frame = hover[0];
sample_window.OnSamples(frame.NbSamples, frame.SampleDigest, frame.Samples);
}
else
{
// When there's no hover, go back to the selected frame
if (self.SelectedFrames[window_thread_name])
{
const frame = self.SelectedFrames[window_thread_name];
sample_window.OnSamples(frame.NbSamples, frame.SampleDigest, frame.Samples);
}
}
}
}
function OnSampleSelected(self, thread_name, select)
{
// Lookup sample window set the frame samples on it
if (select && thread_name in self.SampleWindows)
{
var sample_window = self.SampleWindows[thread_name];
var frame = select[0];
self.SelectedFrames[thread_name] = frame;
sample_window.OnSamples(frame.NbSamples, frame.SampleDigest, frame.Samples);
}
}
function OnResizeWindow(self)
{
// Resize windows
var w = window.innerWidth;
var h = window.innerHeight;
self.Console.WindowResized(w, h);
self.TitleWindow.WindowResized(w, h);
self.SampleTimelineWindow.WindowResized(10, w / 2 - 5, self.TitleWindow.Window);
self.ProcessorTimelineWindow.WindowResized(w / 2 + 5, w / 2 - 5, self.TitleWindow.Window);
for (var i in self.SampleWindows)
{
self.SampleWindows[i].WindowResized(self.SampleTimelineWindow.Window, self.Console.Window);
}
}
function OnTimelineMoved(self, timeline)
{
if (self.Settings.SyncTimelines)
{
let other_timeline = timeline == self.ProcessorTimelineWindow ? self.SampleTimelineWindow : self.ProcessorTimelineWindow;
other_timeline.SetTimeRange(timeline.TimeRange.Start_us, timeline.TimeRange.Span_us);
other_timeline.DrawAllRows();
}
}
return Remotery;
})();

221
vis/Code/SampleWindow.js Normal file
View file

@ -0,0 +1,221 @@
SampleWindow = (function()
{
function SampleWindow(wm, name, offset)
{
// Sample digest for checking if grid needs to be repopulated
this.NbSamples = 0;
this.SampleDigest = null;
// Source sample reference to reduce repopulation
this.Samples = null;
this.XPos = 10 + offset * 410;
this.Window = wm.AddWindow(name, 100, 100, 100, 100);
this.Window.ShowNoAnim();
this.Visible = true;
// Create a grid that's indexed by the unique sample ID
this.Grid = this.Window.AddControlNew(new WM.Grid());
var cell_data =
{
Name: "Samples",
Length: "Time (ms)",
Self: "Self (ms)",
Calls: "Calls",
Recurse: "Recurse",
};
var cell_classes =
{
Name: "SampleTitleNameCell",
Length: "SampleTitleTimeCell",
Self: "SampleTitleTimeCell",
Calls: "SampleTitleCountCell",
Recurse: "SampleTitleCountCell",
};
this.RootRow = this.Grid.Rows.Add(cell_data, "GridGroup", cell_classes);
this.RootRow.Rows.AddIndex("_ID");
}
SampleWindow.prototype.Close = function()
{
this.Window.Close();
}
SampleWindow.prototype.SetXPos = function(xpos, top_window, bottom_window)
{
Anim.Animate(
Bind(AnimatedMove, this, top_window, bottom_window),
this.XPos, 10 + xpos * 410, 0.25);
}
function AnimatedMove(self, top_window, bottom_window, val)
{
self.XPos = val;
self.WindowResized(top_window, bottom_window);
}
SampleWindow.prototype.SetVisible = function(visible)
{
if (visible != this.Visible)
{
if (visible == true)
this.Window.ShowNoAnim();
else
this.Window.HideNoAnim();
this.Visible = visible;
}
}
SampleWindow.prototype.WindowResized = function(top_window, bottom_window)
{
var top = top_window.Position[1] + top_window.Size[1] + 10;
this.Window.SetPosition(this.XPos, top_window.Position[1] + top_window.Size[1] + 10);
this.Window.SetSize(400, bottom_window.Position[1] - 10 - top);
}
SampleWindow.prototype.OnSamples = function(nb_samples, sample_digest, samples)
{
if (!this.Visible)
return;
// If the source hasn't changed, don't repopulate
if (this.Samples == samples)
return;
this.Samples = samples;
// Recreate all the HTML if the number of samples gets bigger
if (nb_samples > this.NbSamples)
{
GrowGrid(this.RootRow, nb_samples);
this.NbSamples = nb_samples;
}
// If the content of the samples changes from previous update, update them all
if (this.SampleDigest != sample_digest)
{
this.RootRow.Rows.ClearIndex("_ID");
var index = UpdateAllSampleFields(this.RootRow, samples, 0, "");
this.SampleDigest = sample_digest;
// Clear out any left-over rows
for (var i = index; i < this.RootRow.Rows.Rows.length; i++)
{
var row = this.RootRow.Rows.Rows[i];
DOM.Node.Hide(row.Node);
}
}
else if (this.Visible)
{
// Otherwise just update the existing sample fields
UpdateChangedSampleFields(this.RootRow, samples, "");
}
}
function GrowGrid(parent_row, nb_samples)
{
parent_row.Rows.Clear();
for (var i = 0; i < nb_samples; i++)
{
var cell_data =
{
_ID: i,
Name: "",
Length: "",
Self: "",
Calls: "",
Recurse: "",
};
var cell_classes =
{
Name: "SampleNameCell",
Length: "SampleTimeCell",
Self: "SampleTimeCell",
Calls: "SampleCountCell",
Recurse: "SampleCountCell",
};
parent_row.Rows.Add(cell_data, null, cell_classes);
}
}
function UpdateAllSampleFields(parent_row, samples, index, indent)
{
for (var i in samples)
{
var sample = samples[i];
// Match row allocation in GrowGrid
var row = parent_row.Rows.Rows[index++];
// Sample row may have been hidden previously
DOM.Node.Show(row.Node);
// Assign unique ID so that the common fast path of updating sample times only
// can lookup target samples in the grid
row.CellData._ID = sample.id;
parent_row.Rows.AddRowToIndex("_ID", sample.id, row);
// Record sample name for later comparison
row.CellData.Name = sample.name.string;
// Set sample name and colour
var name_node = row.CellNodes["Name"];
name_node.innerHTML = indent + sample.name.string;
DOM.Node.SetColour(name_node, sample.colour);
row.CellNodes["Length"].innerHTML = sample.ms_length;
row.CellNodes["Self"].innerHTML = sample.ms_self;
row.CellNodes["Calls"].innerHTML = sample.call_count;
row.CellNodes["Recurse"].innerHTML = sample.recurse_depth;
index = UpdateAllSampleFields(parent_row, sample.children, index, indent + "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;");
}
return index;
}
function UpdateChangedSampleFields(parent_row, samples, indent)
{
for (var i in samples)
{
var sample = samples[i];
var row = parent_row.Rows.GetBy("_ID", sample.id);
if (row)
{
row.CellNodes["Length"].innerHTML = sample.ms_length;
row.CellNodes["Self"].innerHTML = sample.ms_self;
row.CellNodes["Calls"].innerHTML = sample.call_count;
row.CellNodes["Recurse"].innerHTML = sample.recurse_depth;
// Sample name will change when it switches from hash ID to network-retrieved
// name. Quickly check that before re-applying the HTML for the name.
if (row.CellData.Name != sample.name.string)
{
var name_node = row.CellNodes["Name"];
row.CellData.Name = sample.name.string;
name_node.innerHTML = indent + sample.name.string;
}
}
UpdateChangedSampleFields(parent_row, sample.children, indent + "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;");
}
}
return SampleWindow;
})();

275
vis/Code/Shaders.js Normal file
View file

@ -0,0 +1,275 @@
const TimelineVShader =`#version 300 es
#define CANVAS_BORDER 1.0
#define SAMPLE_HEIGHT 16.0
#define SAMPLE_BORDER 1.0
#define SAMPLE_Y_SPACING (SAMPLE_HEIGHT + SAMPLE_BORDER * 2.0)
#define SAMPLE_Y_OFFSET (CANVAS_BORDER + 1.0)
struct Viewport
{
float width;
float height;
};
struct TimeRange
{
float usStart;
float usPerPixel;
};
struct Row
{
float yOffset;
};
uniform Viewport inViewport;
uniform TimeRange inTimeRange;
uniform Row inRow;
in vec4 inSample_TextOffset;
in vec4 inColour_TextLength;
out vec4 varColour_TimeMs;
out vec4 varPosInBoxPx_TextEntry;
out float varTimeChars;
//#define PIXEL_ROUNDED_OFFSETS
float PixelOffset(float time_us)
{
float offset = (time_us - inTimeRange.usStart) * inTimeRange.usPerPixel;
#ifdef PIXEL_ROUNDED_OFFSETS
return floor(offset);
#else
return offset;
#endif
}
float PixelSize(float time_us)
{
float size = time_us * inTimeRange.usPerPixel;
#ifdef PIXEL_ROUNDED_OFFSETS
return floor(size);
#else
return size;
#endif
}
void main()
{
// Unpack input data
float us_start = inSample_TextOffset.x;
float us_length = inSample_TextOffset.y;
float depth = inSample_TextOffset.z;
float text_buffer_offset = inSample_TextOffset.w;
vec3 box_colour = inColour_TextLength.rgb;
float text_length_chars = inColour_TextLength.w;
// Determine pixel range of the sample
float x0 = PixelOffset(us_start);
float x1 = x0 + PixelSize(us_length);
// Calculate box to render
float offset_x = x0;
float offset_y = inRow.yOffset + SAMPLE_Y_OFFSET + (depth - 1.0) * SAMPLE_Y_SPACING;
float size_x = max(x1 - x0, 1.0);
float size_y = SAMPLE_HEIGHT;
// Box range
float min_x = offset_x;
float min_y = offset_y;
float max_x = offset_x + size_x;
float max_y = offset_y + size_y;
// Quad indices are:
//
// 2 3
// +----+
// | |
// +----+
// 0 1
//
vec2 position;
position.x = (gl_VertexID & 1) == 0 ? min_x : max_x;
position.y = (gl_VertexID & 2) == 0 ? min_y : max_y;
//
// NDC is:
// -1 to 1, left to right
// -1 to 1, bottom to top
//
vec4 ndc_pos;
ndc_pos.x = (position.x / inViewport.width) * 2.0 - 1.0;
ndc_pos.y = 1.0 - (position.y / inViewport.height) * 2.0;
ndc_pos.z = 0.0;
ndc_pos.w = 1.0;
// Calculate number of characters required to display the millisecond time
float time_ms = us_length / 1000.0;
float time_ms_int = floor(time_ms);
float time_chars = time_ms_int == 0.0 ? 1.0 : floor(log(time_ms_int) / 2.302585092994046) + 1.0;
gl_Position = ndc_pos;
varColour_TimeMs = vec4(box_colour / 255.0, time_ms);
varPosInBoxPx_TextEntry = vec4(position.x - offset_x, position.y - offset_y, text_buffer_offset, text_length_chars);
varTimeChars = time_chars;
}
`;
const TimelineFShader = `#version 300 es
precision mediump float;
#define SAMPLE_HEIGHT 16.0
struct TextBufferDesc
{
float fontWidth;
float fontHeight;
float textBufferLength;
};
uniform sampler2D inFontAtlasTextre;
uniform sampler2D inTextBuffer;
uniform TextBufferDesc inTextBufferDesc;
in vec4 varColour_TimeMs;
in vec4 varPosInBoxPx_TextEntry;
in float varTimeChars;
out vec4 outColour;
vec4 LookupCharacter(float char_ascii, float pos_x, float pos_y, float font_width_px, float font_height_px)
{
// 2D index of the ASCII character in the font atlas
float char_index_y = floor(char_ascii / 16.0);
float char_index_x = char_ascii - char_index_y * 16.0;
// Start UV of the character in the font atlas
float char_base_uv_x = char_index_x / 16.0;
float char_base_uv_y = char_index_y / 16.0;
// UV within the character itself, scaled to the font atlas
float char_uv_x = pos_x / (font_width_px * 16.0);
float char_uv_y = pos_y / (font_height_px * 16.0);
vec2 uv;
uv.x = char_base_uv_x + char_uv_x;
uv.y = char_base_uv_y + char_uv_y;
// Apply colour to the text in premultiplied alpha space
vec4 t = texture(inFontAtlasTextre, uv);
vec3 colour = vec3(1.0, 1.0, 1.0) * 0.25;
return vec4(colour * t.a, t.a);
}
void main()
{
// Font description
float font_width_px = inTextBufferDesc.fontWidth;
float font_height_px = inTextBufferDesc.fontHeight;
float text_buffer_length = inTextBufferDesc.textBufferLength;
// Text range in the text buffer
vec2 pos_in_box_px = varPosInBoxPx_TextEntry.xy;
float text_buffer_offset = varPosInBoxPx_TextEntry.z;
float text_length_chars = varPosInBoxPx_TextEntry.w;
float text_length_px = text_length_chars * font_width_px;
// Text placement offset within the box
const vec2 text_offset_px = vec2(4.0, 3.0);
vec4 box_colour = vec4(varColour_TimeMs.rgb, 1.0);
// Add a subtle border to the box so that you can visually separate samples when they are next to each other
vec2 top_left = min(pos_in_box_px.xy, 2.0);
float both = min(top_left.x, top_left.y);
box_colour.rgb *= (0.8 + both * 0.1);
vec4 text_colour = vec4(0.0);
float text_end_px = text_length_px + text_offset_px.x + font_width_px;
float time_length_px = (varTimeChars + 4.0) * font_width_px;
if (pos_in_box_px.x > text_end_px && pos_in_box_px.x < text_end_px + time_length_px)
{
float time_ms = varColour_TimeMs.w;
vec2 time_pixel_pos;
time_pixel_pos.x = max(min(pos_in_box_px.x - text_end_px, time_length_px), 0.0);
time_pixel_pos.y = max(min(pos_in_box_px.y - text_offset_px.y, font_height_px - 1.0), 0.0);
float time_index = floor(time_pixel_pos.x / font_width_px);
if (time_index < varTimeChars)
{
// Use base-10 integer digit counting to calculate the divisor needed to bring this digit below 10
float time_divisor = 1.0;
for (int i = 0; i < int(varTimeChars - time_index - 1.0); i++)
{
time_divisor *= 10.0;
}
// Calculate digit
float time_shifted_int = floor(time_ms / time_divisor);
float time_digit = floor(mod(time_shifted_int, 10.0));
text_colour = LookupCharacter(48.0 + time_digit,
time_pixel_pos.x - time_index * font_width_px,
time_pixel_pos.y,
font_width_px, font_height_px);
}
else if (time_index == varTimeChars)
{
text_colour = LookupCharacter(46.0,
time_pixel_pos.x - time_index * font_width_px,
time_pixel_pos.y,
font_width_px, font_height_px);
}
else if (time_index == varTimeChars + 1.0)
{
float time_digit = floor(mod(time_ms * 10.0, 10.0));
text_colour = LookupCharacter(48.0 + time_digit,
time_pixel_pos.x - time_index * font_width_px,
time_pixel_pos.y,
font_width_px, font_height_px);
}
else if (time_index == varTimeChars + 2.0)
{
float time_digit = floor(mod(time_ms * 10.0, 10.0));
text_colour = LookupCharacter(109.0,
time_pixel_pos.x - time_index * font_width_px,
time_pixel_pos.y,
font_width_px, font_height_px);
}
else if (time_index == varTimeChars + 3.0)
{
float time_digit = floor(mod(time_ms * 10.0, 10.0));
text_colour = LookupCharacter(115.0, time_pixel_pos.x - time_index * font_width_px, time_pixel_pos.y, font_width_px, font_height_px);
}
}
else
{
// Text pixel position clamped to the bounds of the full word, allowing leakage to neighbouring NULL characters to pad zeroes
vec2 text_pixel_pos;
text_pixel_pos.x = max(min(pos_in_box_px.x - text_offset_px.x, text_length_px), -1.0);
text_pixel_pos.y = max(min(pos_in_box_px.y - text_offset_px.y, font_height_px - 1.0), 0.0);
// Index of the current character in the text buffer
float text_index = text_buffer_offset + floor(text_pixel_pos.x / font_width_px);
// Sample the 1D text buffer to get the ASCII character index
vec2 char_uv = vec2((text_index + 0.5) / text_buffer_length, 0.5);
float char_ascii = texture(inTextBuffer, char_uv).a * 255.0;
text_colour = LookupCharacter(char_ascii,
text_pixel_pos.x - (text_index - text_buffer_offset) * font_width_px,
text_pixel_pos.y,
font_width_px, font_height_px);
}
// Bring out of premultiplied alpha space and lerp with the box colour
float inv_alpha = text_colour.a == 0.0 ? 1.0 : 1.0 / text_colour.a;
outColour = mix(box_colour, vec4(text_colour.rgb * inv_alpha, 1.0), text_colour.a);
}
`;

29
vis/Code/ThreadFrame.js Normal file
View file

@ -0,0 +1,29 @@
ThreadFrame = (function()
{
function ThreadFrame(message)
{
// Persist the required message data
this.NbSamples = message.nb_samples;
this.SampleDigest = message.sample_digest;
this.Samples = message.samples;
this.PartialTree = message.partial_tree > 0 ? true : false;
// Calculate the frame start/end times
this.StartTime_us = 0;
this.EndTime_us = 0;
var nb_root_samples = this.Samples.length;
if (nb_root_samples > 0)
{
var last_sample = this.Samples[nb_root_samples - 1];
this.StartTime_us = this.Samples[0].us_start;
this.EndTime_us = last_sample.us_start + last_sample.us_length;
}
this.Length_us = this.EndTime_us - this.StartTime_us;
}
return ThreadFrame;
})();

186
vis/Code/TimelineMarkers.js Normal file
View file

@ -0,0 +1,186 @@
function GetTimeText(seconds)
{
if (seconds < 0)
{
return "";
}
var text = "";
// Add any contributing hours
var h = Math.floor(seconds / 3600);
seconds -= h * 3600;
if (h)
{
text += h + "h ";
}
// Add any contributing minutes
var m = Math.floor(seconds / 60);
seconds -= m * 60;
if (m)
{
text += m + "m ";
}
// Add any contributing seconds or always add seconds when hours or minutes have no contribution
// This ensures the 0s marker displays
var s = Math.floor(seconds);
seconds -= s;
if (s || text == "")
{
text += s + "s ";
}
// Add remaining milliseconds
var ms = Math.floor(seconds * 1000);
if (ms)
{
text += ms + "ms";
}
return text;
}
class TimelineMarkers
{
constructor(timeline)
{
this.timeline = timeline;
// Need a 2D drawing context
this.markerContainer = timeline.Window.AddControlNew(new WM.Container(10, 10, 10, 10));
this.markerCanvas = document.createElement("canvas");
this.markerContainer.Node.appendChild(this.markerCanvas);
this.markerContext = this.markerCanvas.getContext("2d");
}
Draw(time_range)
{
let ctx = this.markerContext;
ctx.clearRect(0, 0, this.markerCanvas.width, this.markerCanvas.height);
// Setup render state for the time line markers
ctx.strokeStyle = "#BBB";
ctx.fillStyle = "#BBB";
ctx.lineWidth = 1;
ctx.font = "9px LocalFiraCode";
// A list of all supported units of time (measured in seconds) that require markers
let units = [ 0.001, 0.01, 0.1, 1, 10, 60, 60 * 5, 60 * 60, 60 * 60 * 24 ];
// Given the current pixel size of a second, calculate the spacing for each unit marker
let second_pixel_size = time_range.PixelSize(1000 * 1000);
let sizeof_units = [ ];
for (let unit of units)
{
sizeof_units.push(unit * second_pixel_size);
}
// Calculate whether each unit marker is visible at the current zoom level
var show_unit = [ ];
for (let sizeof_unit of sizeof_units)
{
show_unit.push(Math.max(Math.min((sizeof_unit - 4) * 0.25, 1), 0));
}
// Find the first visible unit
for (let i = 0; i < units.length; i++)
{
if (show_unit[i] > 0)
{
// Cut out unit information for the first set of units not visible
units = units.slice(i);
sizeof_units = sizeof_units.slice(i);
show_unit = show_unit.slice(i);
break;
}
}
let timeline_end = this.markerCanvas.width;
for (let i = 0; i < 3; i++)
{
// Round the start time up to the next visible unit
let time_start = time_range.Start_us / (1000 * 1000);
let unit_time_start = Math.ceil(time_start / units[i]) * units[i];
// Calculate the canvas offset required to step to the first visible unit
let pre_step_x = time_range.PixelOffset(unit_time_start * (1000 * 1000));
// Draw lines for every unit at this level, keeping tracking of the seconds
var seconds = unit_time_start;
for (let x = pre_step_x; x <= timeline_end; x += sizeof_units[i])
{
// For the first two units, don't draw the units above it to prevent
// overdraw and the visual errors that causes
// The last unit always draws
if (i > 1 || (seconds % units[i + 1]))
{
// Only the first two units scale with unit visibility
// The last unit maintains its size
let height = Math.min(i * 4 + 4 * show_unit[i], 16);
// Draw the line on an integer boundary, shifted by 0.5 to get an un-anti-aliased 1px line
let ix = Math.floor(x);
ctx.beginPath();
ctx.moveTo(ix + 0.5, 1);
ctx.lineTo(ix + 0.5, 1 + height);
ctx.stroke();
}
seconds += units[i];
}
if (i == 1)
{
// Draw text labels for the second unit, fading them out as they slowly
// become the first unit
ctx.globalAlpha = show_unit[0];
var seconds = unit_time_start;
for (let x = pre_step_x; x <= timeline_end; x += sizeof_units[i])
{
if (seconds % units[2])
{
this.DrawTimeText(seconds, x, 16);
}
seconds += units[i];
}
// Restore alpha
ctx.globalAlpha = 1;
}
else if (i == 2)
{
// Draw text labels for the third unit with no fade
var seconds = unit_time_start;
for (let x = pre_step_x; x <= timeline_end; x += sizeof_units[i])
{
this.DrawTimeText(seconds, x, 16);
seconds += units[i];
}
}
}
}
DrawTimeText(seconds, x, y)
{
// Use text measuring to centre the text horizontally on the input x
var text = GetTimeText(seconds);
var width = this.markerContext.measureText(text).width;
this.markerContext.fillText(text, Math.floor(x) - width / 2, y);
}
Resize(x, y, w, h)
{
this.markerContainer.SetPosition(x, y);
this.markerContainer.SetSize(w, h);
// Match canvas size to container
this.markerCanvas.width = this.markerContainer.Node.clientWidth;
this.markerCanvas.height = this.markerContainer.Node.clientHeight;
}
}

389
vis/Code/TimelineRow.js Normal file
View file

@ -0,0 +1,389 @@
TimelineRow = (function()
{
const RowLabelTemplate = `
<div class='TimelineRow'>
<div class='TimelineRowCheck TimelineBox'>
<input class='TimelineRowCheckbox' type='checkbox' />
</div>
<div class='TimelineRowExpand TimelineBox NoSelect'>
<div class='TimelineRowExpandButton'>+</div>
</div>
<div class='TimelineRowExpand TimelineBox NoSelect'>
<div class='TimelineRowExpandButton'>-</div>
</div>
<div class='TimelineRowLabel TimelineBox'></div>
<div style="clear:left"></div>
</div>`
var CANVAS_BORDER = 1;
var SAMPLE_HEIGHT = 16;
var SAMPLE_BORDER = 1;
var SAMPLE_Y_SPACING = SAMPLE_HEIGHT + SAMPLE_BORDER * 2;
function TimelineRow(gl, name, timeline, frame_history, check_handler)
{
this.Name = name;
this.timeline = timeline;
// Create the row HTML and add to the parent
this.LabelContainerNode = DOM.Node.CreateHTML(RowLabelTemplate);
const label_node = DOM.Node.FindWithClass(this.LabelContainerNode, "TimelineRowLabel");
label_node.innerHTML = name;
timeline.TimelineLabels.Node.appendChild(this.LabelContainerNode);
// All sample view windows visible by default
const checkbox_node = DOM.Node.FindWithClass(this.LabelContainerNode, "TimelineRowCheckbox");
checkbox_node.checked = true;
checkbox_node.addEventListener("change", (e) => check_handler(name, e));
// Manually hook-up events to simulate div:active
// I can't get the equivalent CSS to work in Firefox, so...
const expand_node_0 = DOM.Node.FindWithClass(this.LabelContainerNode, "TimelineRowExpand", 0);
const expand_node_1 = DOM.Node.FindWithClass(this.LabelContainerNode, "TimelineRowExpand", 1);
const inc_node = DOM.Node.FindWithClass(expand_node_0, "TimelineRowExpandButton");
const dec_node = DOM.Node.FindWithClass(expand_node_1, "TimelineRowExpandButton");
inc_node.addEventListener("mousedown", ExpandButtonDown);
inc_node.addEventListener("mouseup", ExpandButtonUp);
inc_node.addEventListener("mouseleave", ExpandButtonUp);
dec_node.addEventListener("mousedown", ExpandButtonDown);
dec_node.addEventListener("mouseup", ExpandButtonUp);
dec_node.addEventListener("mouseleave", ExpandButtonUp);
// Pressing +/i increases/decreases depth
inc_node.addEventListener("click", () => this.IncDepth());
dec_node.addEventListener("click", () => this.DecDepth());
// Frame index to start at when looking for first visible sample
this.StartFrameIndex = 0;
this.FrameHistory = frame_history;
this.VisibleFrames = [ ];
this.VisibleTimeRange = null;
this.Depth = 1;
// Currently selected sample
this.SelectSampleInfo = null;
// Create WebGL sample buffers
this.sampleBuffer = new glDynamicBuffer(gl, gl.FLOAT, 4, 8);
this.colourBuffer = new glDynamicBuffer(gl, gl.FLOAT, 4, 8);
// Create a vertex array for these buffers
this.vertexArrayObject = gl.createVertexArray();
gl.bindVertexArray(this.vertexArrayObject);
this.sampleBuffer.BindAsInstanceAttribute(timeline.Program, "inSample_TextOffset");
this.colourBuffer.BindAsInstanceAttribute(timeline.Program, "inColour_TextLength");
// An initial SetSize call to restore containers to their original size after traces were loaded prior to this
this.SetSize();
}
TimelineRow.prototype.SetSize = function()
{
this.LabelContainerNode.style.height = CANVAS_BORDER + SAMPLE_BORDER + SAMPLE_Y_SPACING * this.Depth;
}
TimelineRow.prototype.SetVisibleFrames = function(time_range)
{
// Clear previous visible list
this.VisibleFrames = [ ];
if (this.FrameHistory.length == 0)
return;
// Store a copy of the visible time range rather than referencing it
// This prevents external modifications to the time range from affecting rendering/selection
time_range = time_range.Clone();
this.VisibleTimeRange = time_range;
// The frame history can be reset outside this class
// This also catches the overflow to the end of the frame list below when a thread stops sending samples
var max_frame = Math.max(this.FrameHistory.length - 1, 0);
var start_frame_index = Math.min(this.StartFrameIndex, max_frame);
// First do a back-track in case the time range moves negatively
while (start_frame_index > 0)
{
var frame = this.FrameHistory[start_frame_index];
if (time_range.Start_us > frame.StartTime_us)
break;
start_frame_index--;
}
// Then search from this point for the first visible frame
while (start_frame_index < this.FrameHistory.length)
{
var frame = this.FrameHistory[start_frame_index];
if (frame.EndTime_us > time_range.Start_us)
break;
start_frame_index++;
}
// Gather all frames up to the end point
this.StartFrameIndex = start_frame_index;
for (var i = start_frame_index; i < this.FrameHistory.length; i++)
{
var frame = this.FrameHistory[i];
if (frame.StartTime_us > time_range.End_us)
break;
this.VisibleFrames.push(frame);
}
}
TimelineRow.prototype.DrawSampleHighlight = function(sample, depth, colour, y_scroll_offset)
{
if (depth <= this.Depth)
{
// Determine pixel range of the sample
var x0 = this.VisibleTimeRange.PixelOffset(sample.us_start);
var x1 = x0 + this.VisibleTimeRange.PixelSize(sample.us_length);
var offset_x = x0;
var offset_y = this.LabelContainerNode.offsetTop + 2 + (depth - 1) * SAMPLE_Y_SPACING + y_scroll_offset;
var size_x = x1 - x0;
var size_y = SAMPLE_HEIGHT;
// Normal rendering
var ctx = this.timeline.drawContext;
ctx.lineWidth = 2;
ctx.strokeStyle = colour;
ctx.strokeRect(offset_x + 2.5, offset_y - 0.5, size_x - 3, size_y + 1);
}
}
TimelineRow.prototype.DisplayHeight = function()
{
return this.LabelContainerNode.clientHeight;
}
TimelineRow.prototype.YOffset = function()
{
return this.LabelContainerNode.offsetTop;
}
TimelineRow.prototype.DrawBackground = function(hover_sample_info, y_scroll_offset)
{
// Fill box that shows the boundary between thread rows
this.timeline.drawContext.fillStyle = "#444"
var b = CANVAS_BORDER;
this.timeline.drawContext.fillRect(b, this.YOffset() + y_scroll_offset + b, this.timeline.drawCanvas.width - b * 2, this.DisplayHeight() - b * 2);
// Draw the selected sample for this row
if (this.SelectSampleInfo != null)
{
const sample = this.SelectSampleInfo[1];
const depth = this.SelectSampleInfo[2];
this.DrawSampleHighlight(sample, depth, "#FF0000", y_scroll_offset);
}
// Draw the current hover sample if it's over this row
if (hover_sample_info != null && hover_sample_info[3] == this)
{
const sample = hover_sample_info[1];
const depth = hover_sample_info[2];
const thread_row = hover_sample_info[3];
this.DrawSampleHighlight(sample, depth, "#FFFFFF", y_scroll_offset);
}
}
TimelineRow.prototype.Draw = function(gl, draw_text, y_scroll_offset)
{
let samples_per_depth = [];
// Gather all root samples in the visible frame set
for (var i in this.VisibleFrames)
{
var frame = this.VisibleFrames[i];
GatherSamples(this, frame.Samples, 1, draw_text, samples_per_depth);
}
// Count number of samples required
let nb_samples = 0;
for (const samples_this_depth of samples_per_depth)
{
nb_samples += samples_this_depth.length;
}
// Resize buffers to match any new count of samples
if (nb_samples > this.sampleBuffer.nbEntries)
{
this.sampleBuffer.ResizeToFitNextPow2(nb_samples);
this.colourBuffer.ResizeToFitNextPow2(nb_samples);
// Have to create a new VAO for these buffers
this.vertexArrayObject = gl.createVertexArray();
gl.bindVertexArray(this.vertexArrayObject);
this.sampleBuffer.BindAsInstanceAttribute(this.timeline.Program, "inSample_TextOffset");
this.colourBuffer.BindAsInstanceAttribute(this.timeline.Program, "inColour_TextLength");
}
// CPU write destination for samples
let cpu_samples = this.sampleBuffer.cpuArray;
let cpu_colours = this.colourBuffer.cpuArray;
let sample_pos = 0;
const empty_text_entry = {
offset: 0,
length: 1,
};
// Copy samples to the CPU buffer
// TODO(don): Use a ring buffer instead and take advantage of timeline scrolling adding new samples at the beginning/end
for (let depth = 0; depth < samples_per_depth.length; depth++)
{
let samples_this_depth = samples_per_depth[depth];
for (const sample of samples_this_depth)
{
const text_entry = sample.name.textEntry != null ? sample.name.textEntry : empty_text_entry;
cpu_samples[sample_pos + 0] = sample.us_start;
cpu_samples[sample_pos + 1] = sample.us_length;
cpu_samples[sample_pos + 2] = depth;
cpu_samples[sample_pos + 3] = text_entry.offset;
cpu_colours[sample_pos + 0] = sample.rgbColour[0];
cpu_colours[sample_pos + 1] = sample.rgbColour[1];
cpu_colours[sample_pos + 2] = sample.rgbColour[2];
cpu_colours[sample_pos + 3] = text_entry.length;
sample_pos += 4;
}
}
// Upload to GPU
this.sampleBuffer.UploadData();
this.colourBuffer.UploadData();
this.timeline.textBuffer.UploadData();
// Set row parameters
glSetUniform(gl, this.timeline.Program, "inRow.yOffset", this.YOffset() + y_scroll_offset);
gl.bindVertexArray(this.vertexArrayObject);
gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, nb_samples);
}
function GatherSamples(self, samples, depth, draw_text, samples_per_depth)
{
// Ensure there's enough entries for each depth
while (depth >= samples_per_depth.length)
{
samples_per_depth.push([]);
}
let samples_this_depth = samples_per_depth[depth];
for (var i in samples)
{
var sample = samples[i];
samples_this_depth.push(sample);
if (depth < self.Depth && sample.children != null)
GatherSamples(self, sample.children, depth + 1, draw_text, samples_per_depth);
}
}
TimelineRow.prototype.SetSelectSample = function(sample_info)
{
this.SelectSampleInfo = sample_info;
}
function ExpandButtonDown(evt)
{
var node = DOM.Event.GetNode(evt);
DOM.Node.AddClass(node, "TimelineRowExpandButtonActive");
}
function ExpandButtonUp(evt)
{
var node = DOM.Event.GetNode(evt);
DOM.Node.RemoveClass(node, "TimelineRowExpandButtonActive");
}
TimelineRow.prototype.IncDepth = function()
{
this.Depth++;
this.SetSize();
this.timeline.DrawAllRows();
}
TimelineRow.prototype.DecDepth = function()
{
if (this.Depth > 1)
{
this.Depth--;
this.SetSize();
// Trigger scroll handling to ensure reducing the depth reduces the display height
this.timeline.ScrollVertically(0);
this.timeline.DrawAllRows();
}
}
TimelineRow.prototype.GetSampleAtPosition = function(time_us, mouse_y)
{
// Calculate depth of the mouse cursor
var depth = Math.min(Math.floor(mouse_y / SAMPLE_Y_SPACING) + 1, this.Depth);
// Search for the first frame to intersect this time
for (var i in this.VisibleFrames)
{
// Use the sample's closed interval to detect hits.
// Rendering of samples ensures a sample is never smaller than one pixel so that all samples always draw, irrespective
// of zoom level. If a half-open interval is used then some visible samples will be unselectable due to them being
// 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.
var frame = this.VisibleFrames[i];
if (time_us >= frame.StartTime_us && time_us <= frame.EndTime_us)
{
var found_sample = FindSample(this, frame.Samples, time_us, depth, 1);
if (found_sample != null)
return [ frame, found_sample[0], found_sample[1], this ];
}
}
return null;
}
function FindSample(self, samples, time_us, target_depth, depth)
{
for (var i in samples)
{
var sample = samples[i];
if (depth == target_depth)
{
if (time_us >= sample.us_start && time_us < sample.us_start + sample.us_length)
return [ sample, depth ];
}
else if (depth < target_depth && sample.children != null)
{
var found_sample = FindSample(self, sample.children, time_us, target_depth, depth + 1);
if (found_sample != null)
return found_sample;
}
}
return null;
}
return TimelineRow;
})();

494
vis/Code/TimelineWindow.js Normal file
View file

@ -0,0 +1,494 @@
// TODO(don): Separate all knowledge of threads from this timeline
TimelineWindow = (function()
{
var BORDER = 10;
function TimelineWindow(wm, name, settings, check_handler)
{
this.Settings = settings;
// Create timeline window
this.Window = wm.AddWindow("Timeline", 10, 20, 100, 100);
this.Window.SetTitle(name);
this.Window.ShowNoAnim();
this.timelineMarkers = new TimelineMarkers(this);
// DO THESE need to be containers... can they just be divs?
// divs need a retrieval function
this.TimelineLabelScrollClipper = this.Window.AddControlNew(new WM.Container(10, 10, 10, 10));
DOM.Node.AddClass(this.TimelineLabelScrollClipper.Node, "TimelineLabelScrollClipper");
this.TimelineLabels = this.TimelineLabelScrollClipper.AddControlNew(new WM.Container(0, 0, 10, 10));
DOM.Node.AddClass(this.TimelineLabels.Node, "TimelineLabels");
// Ordered list of thread rows on the timeline
this.ThreadRows = [ ];
// Create timeline container
this.TimelineContainer = this.Window.AddControlNew(new WM.Container(10, 10, 800, 160));
DOM.Node.AddClass(this.TimelineContainer.Node, "TimelineContainer");
var mouse_wheel_event = (/Firefox/i.test(navigator.userAgent)) ? "DOMMouseScroll" : "mousewheel";
DOM.Event.AddHandler(this.TimelineContainer.Node, mouse_wheel_event, Bind(OnMouseScroll, this));
// Setup timeline manipulation
this.MouseDown = false;
this.LastMouseState = null;
this.TimelineMoved = false;
DOM.Event.AddHandler(this.TimelineContainer.Node, "mousedown", Bind(OnMouseDown, this));
DOM.Event.AddHandler(this.TimelineContainer.Node, "mouseup", Bind(OnMouseUp, this));
DOM.Event.AddHandler(this.TimelineContainer.Node, "mousemove", Bind(OnMouseMove, this));
DOM.Event.AddHandler(this.TimelineContainer.Node, "mouseleave", Bind(OnMouseLeave, this));
// Create a canvas for timeline 2D rendering
// TODO(don): Port this to shaders
this.drawCanvas = document.createElement("canvas");
this.drawCanvas.width = this.TimelineContainer.Node.clientWidth;
this.drawCanvas.height = this.TimelineContainer.Node.clientHeight;
this.TimelineContainer.Node.appendChild(this.drawCanvas);
this.drawContext = this.drawCanvas.getContext("2d");
// Create a canvas for timeline 3D accelerated rendering
this.glCanvas = document.createElement("canvas");
this.glCanvas.width = this.TimelineContainer.Node.clientWidth;
this.glCanvas.height = this.TimelineContainer.Node.clientHeight;
this.TimelineContainer.Node.appendChild(this.glCanvas);
// OVERLAY - add to CSS
this.glCanvas.style.position = "absolute";
this.glCanvas.style.top = 0;
this.glCanvas.style.left = 0;
// For now a gl context per timeline
let gl = this.glCanvas.getContext("webgl2");
this.gl = gl;
const vshader = glCompileShader(gl, gl.VERTEX_SHADER, "TimelineVShader", TimelineVShader);
const fshader = glCompileShader(gl, gl.FRAGMENT_SHADER, "TimelineFShader", TimelineFShader);
this.Program = glCreateProgram(gl, vshader, fshader);
this.font = new glFont(gl);
this.textBuffer = new glTextBuffer(gl, this.font);
this.Window.SetOnResize(Bind(OnUserResize, this));
this.Clear();
this.OnHoverHandler = null;
this.OnSelectedHandler = null;
this.OnMovedHandler = null;
this.CheckHandler = check_handler;
this.yScrollOffset = 0;
this.HoverSampleInfo = null;
}
TimelineWindow.prototype.Clear = function()
{
// Clear out labels
this.TimelineLabels.ClearControls();
this.ThreadRows = [ ];
this.TimeRange = new PixelTimeRange(0, 200 * 1000, this.TimelineContainer.Node.clientWidth);
}
TimelineWindow.prototype.SetOnHover = function(handler)
{
this.OnHoverHandler = handler;
}
TimelineWindow.prototype.SetOnSelected = function(handler)
{
this.OnSelectedHandler = handler;
}
TimelineWindow.prototype.SetOnMoved = function(handler)
{
this.OnMovedHandler = handler;
}
TimelineWindow.prototype.WindowResized = function(x, width, top_window)
{
// Resize window
var top = top_window.Position[1] + top_window.Size[1] + 10;
this.Window.SetPosition(x, top);
this.Window.SetSize(width - 2 * 10, 260);
ResizeInternals(this);
}
TimelineWindow.prototype.OnSamples = function(thread_name, frame_history)
{
// Shift the timeline to the last entry on this thread
// As multiple threads come through here with different end frames, only do this for the latest
var last_frame = frame_history[frame_history.length - 1];
if (last_frame.EndTime_us > this.TimeRange.End_us)
this.TimeRange.SetEnd(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)
{
thread_index = i;
break;
}
}
// If this thread has not been seen before, add a new row to the list and re-sort
if (thread_index == -1)
{
var row = new TimelineRow(this.gl, thread_name, this, frame_history, this.CheckHandler);
this.ThreadRows.push(row);
}
}
TimelineWindow.prototype.DrawBackground = function()
{
// TODO(don): Port all this lot to shader, maybe... it's not performance sensitive
this.drawContext.clearRect(0, 0, this.drawCanvas.width, this.drawCanvas.height);
// Draw thread row backgrounds
for (let thread_row of this.ThreadRows)
{
thread_row.DrawBackground(this.HoverSampleInfo, this.yScrollOffset);
}
this.timelineMarkers.Draw(this.TimeRange);
}
TimelineWindow.prototype.DrawAllRows = function()
{
let gl = this.gl;
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.useProgram(this.Program);
// Set viewport parameters
glSetUniform(gl, this.Program, "inViewport.width", gl.canvas.width);
glSetUniform(gl, this.Program, "inViewport.height", gl.canvas.height);
// Set time range parameters
const time_range = this.TimeRange;
time_range.SetAsUniform(gl, this.Program);
// Set text rendering resources
// Note it might not be loaded yet so we need the null check
if (this.font.atlasTexture != null)
{
glSetUniform(gl, this.Program, "inFontAtlasTextre", this.font.atlasTexture, 0);
this.textBuffer.SetAsUniform(gl, this.Program, "inTextBuffer", 1);
}
const draw_text = this.Settings.IsPaused;
for (let i in this.ThreadRows)
{
var thread_row = this.ThreadRows[i];
thread_row.SetVisibleFrames(time_range);
thread_row.Draw(gl, draw_text, this.yScrollOffset);
}
// Render last so that each thread row uses any new time ranges
this.DrawBackground();
}
function OnUserResize(self, evt)
{
ResizeInternals(self);
}
function ResizeInternals(self)
{
// .TimelineRowLabel
// .TimelineRowExpand
// .TimelineRowExpand
// .TimelineRowCheck
// Window padding
let offset_x = 145+19+19+19+10;
let MarkersHeight = 18;
var parent_size = self.Window.Size;
self.timelineMarkers.Resize(BORDER + offset_x, 10, parent_size[0] - 2* BORDER - offset_x, MarkersHeight);
// Resize controls
self.TimelineContainer.SetPosition(BORDER + offset_x, 10 + MarkersHeight);
self.TimelineContainer.SetSize(parent_size[0] - 2 * BORDER - offset_x, parent_size[1] - MarkersHeight - 40);
self.TimelineLabelScrollClipper.SetPosition(10, 10 + MarkersHeight);
self.TimelineLabelScrollClipper.SetSize(offset_x, parent_size[1] - MarkersHeight - 40);
self.TimelineLabels.SetSize(offset_x, parent_size[1] - MarkersHeight - 40);
// Match canvas size to container
const width = self.TimelineContainer.Node.clientWidth;
const height = self.TimelineContainer.Node.clientHeight;
self.drawCanvas.width = width;
self.drawCanvas.height = height;
self.glCanvas.width = width;
self.glCanvas.height = height;
// Adjust time range to new width
self.TimeRange.SetPixelSpan(width);
self.DrawAllRows();
}
function OnMouseScroll(self, evt)
{
let mouse_state = new Mouse.State(evt);
let scale = 1.11;
if (mouse_state.WheelDelta > 0)
scale = 1 / scale;
// What time is the mouse hovering over?
let mouse_pos = self.TimelineMousePosition(mouse_state);
let time_us = self.TimeRange.TimeAtPosition(mouse_pos[0]);
// Calculate start time relative to the mouse hover position
var time_start_us = self.TimeRange.Start_us - time_us;
// Scale and offset back to the hover time
self.TimeRange.Set(time_start_us * scale + time_us, self.TimeRange.Span_us * scale);
self.DrawAllRows();
if (self.OnMovedHandler)
{
self.OnMovedHandler(self);
}
// Prevent vertical scrolling on mouse-wheel
DOM.Event.StopDefaultAction(evt);
}
TimelineWindow.prototype.SetTimeRange = function(start_us, span_us)
{
this.TimeRange.Set(start_us, span_us);
}
TimelineWindow.prototype.DisplayHeight = function()
{
// Sum height of each thread row
let height = 0;
for (thread_row of this.ThreadRows)
{
height += thread_row.DisplayHeight();
}
return height;
}
TimelineWindow.prototype.ScrollVertically = function(y_scroll)
{
// Calculate the minimum negative value the position of the labels can be to account for scrolling to the bottom
// of the label/depth list
let display_height = this.DisplayHeight();
let container_height = this.TimelineLabelScrollClipper.Node.clientHeight;
let minimum_y = Math.min(container_height - display_height, 0.0);
// Resize the label container to match the display height
this.TimelineLabels.Node.style.height = Math.max(display_height, container_height);
// Increment the y-scroll using just-calculated limits
let old_y_scroll_offset = this.yScrollOffset;
this.yScrollOffset = Math.min(Math.max(this.yScrollOffset + y_scroll, minimum_y), 0);
// Calculate how much the labels should actually scroll after limiting and apply
let y_scroll_px = this.yScrollOffset - old_y_scroll_offset;
this.TimelineLabels.Node.style.top = this.TimelineLabels.Node.offsetTop + y_scroll_px;
}
TimelineWindow.prototype.TimelineMousePosition = function(mouse_state)
{
// Position of the mouse relative to the timeline container
let node_offset = DOM.Node.GetPosition(this.TimelineContainer.Node);
let mouse_x = mouse_state.Position[0] - node_offset[0];
let mouse_y = mouse_state.Position[1] - node_offset[1];
// Offset by the amount of scroll
mouse_y -= this.yScrollOffset;
return [ mouse_x, mouse_y ];
}
TimelineWindow.prototype.GetHoverThreadRow = function(mouse_pos)
{
// Search for the thread row the mouse intersects
let height = 0;
for (let thread_row of this.ThreadRows)
{
let row_height = thread_row.DisplayHeight();
if (mouse_pos[1] >= height && mouse_pos[1] < height + row_height)
{
// Mouse y relative to row start
mouse_pos[1] -= height;
return thread_row;
}
height += row_height;
}
return null;
}
function OnMouseDown(self, evt)
{
// Only manipulate the timeline when paused
if (!self.Settings.IsPaused)
return;
self.MouseDown = true;
self.LastMouseState = new Mouse.State(evt);
self.TimelineMoved = false;
DOM.Event.StopDefaultAction(evt);
}
function OnMouseUp(self, evt)
{
// Only manipulate the timeline when paused
if (!self.Settings.IsPaused)
return;
var mouse_state = new Mouse.State(evt);
self.MouseDown = false;
if (!self.TimelineMoved)
{
// Are we hovering over a thread row?
let mouse_pos = self.TimelineMousePosition(mouse_state);
let hover_thread_row = self.GetHoverThreadRow(mouse_pos);
if (hover_thread_row != null)
{
// Are we hovering over a sample?
let time_us = self.TimeRange.TimeAtPosition(mouse_pos[0]);
let sample_info = hover_thread_row.GetSampleAtPosition(time_us, mouse_pos[1]);
if (sample_info != null)
{
// Redraw with new select sample
hover_thread_row.SetSelectSample(sample_info);
self.DrawBackground();
// Call any selection handlers
if (self.OnSelectedHandler)
{
self.OnSelectedHandler(hover_thread_row.Name, sample_info);
}
}
}
}
}
function OnMouseMove(self, evt)
{
// Only manipulate the timeline when paused
if (!self.Settings.IsPaused)
return;
var mouse_state = new Mouse.State(evt);
if (self.MouseDown)
{
let movement = false;
// Shift the visible time range with mouse movement
let time_offset_us = (mouse_state.Position[0] - self.LastMouseState.Position[0]) / self.TimeRange.usPerPixel;
if (time_offset_us != 0)
{
self.TimeRange.SetStart(self.TimeRange.Start_us - time_offset_us);
movement = true;
}
// Control vertical movement
let y_offset_px = mouse_state.Position[1] - self.LastMouseState.Position[1];
if (y_offset_px != 0)
{
self.ScrollVertically(y_offset_px);
movement = true;
}
// Redraw everything if there is movement
if (movement)
{
self.DrawAllRows();
self.TimelineMoved = true;
if (self.OnMovedHandler)
{
self.OnMovedHandler(self);
}
}
}
else
{
// Are we hovering over a thread row?
let mouse_pos = self.TimelineMousePosition(mouse_state);
let hover_thread_row = self.GetHoverThreadRow(mouse_pos);
if (hover_thread_row != null)
{
// Are we hovering over a sample?
let time_us = self.TimeRange.TimeAtPosition(mouse_pos[0]);
self.HoverSampleInfo = hover_thread_row.GetSampleAtPosition(time_us, mouse_pos[1]);
// Tell listeners which sample we're hovering over
if (self.OnHoverHandler != null)
{
self.OnHoverHandler(hover_thread_row.Name, self.HoverSampleInfo);
}
}
else
{
self.HoverSampleInfo = null;
}
// Redraw to update highlights
self.DrawBackground();
}
self.LastMouseState = mouse_state;
}
function OnMouseLeave(self, evt)
{
// Only manipulate the timeline when paused
if (!self.Settings.IsPaused)
return;
// Cancel scrolling
self.MouseDown = false;
// Cancel hovering
if (self.OnHoverHandler != null)
{
self.OnHoverHandler(null, null);
}
}
return TimelineWindow;
})();

105
vis/Code/TitleWindow.js Normal file
View file

@ -0,0 +1,105 @@
TitleWindow = (function()
{
function TitleWindow(wm, settings, server, connection_address)
{
this.Settings = settings;
this.Window = wm.AddWindow("&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Remotery", 10, 10, 100, 100);
this.Window.ShowNoAnim();
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));
// Setup pause button
this.PauseButton = this.Window.AddControlNew(new WM.Button("Pause", 5, 5, { toggle: true }));
this.PauseButton.SetOnClick(Bind(OnPausePressed, this));
this.SyncButton = this.Window.AddControlNew(new WM.Button("Sync Timelines", 5, 5, { toggle: true}));
this.SyncButton.SetOnClick(Bind(OnSyncPressed, this));
this.SyncButton.SetState(this.Settings.SyncTimelines);
server.AddMessageHandler("PING", Bind(OnPing, this));
this.Window.SetOnResize(Bind(OnUserResize, this));
}
TitleWindow.prototype.SetConnectionAddressChanged = function(handler)
{
this.EditBox.SetChangeHandler(handler);
}
TitleWindow.prototype.WindowResized = function(width, height)
{
this.Window.SetSize(width - 2 * 10, 50);
ResizeInternals(this);
}
TitleWindow.prototype.Pause = function()
{
if (!this.Settings.IsPaused)
{
this.PauseButton.SetText("Paused");
this.PauseButton.SetState(true);
this.Settings.IsPaused = true;
}
}
TitleWindow.prototype.Unpause = function()
{
if (this.Settings.IsPaused)
{
this.PauseButton.SetText("Pause");
this.PauseButton.SetState(false);
this.Settings.IsPaused = false;
}
}
function OnUserResize(self, evt)
{
ResizeInternals(self);
}
function ResizeInternals(self)
{
self.PauseButton.SetPosition(self.Window.Size[0] - 60, 5);
self.SyncButton.SetPosition(self.Window.Size[0] - 155, 5);
}
function OnPausePressed(self)
{
if (self.PauseButton.IsPressed())
{
self.Pause();
}
else
{
self.Unpause();
}
}
function OnSyncPressed(self)
{
self.Settings.SyncTimelines = self.SyncButton.IsPressed();
}
function OnPing(self, server)
{
// Set the ping container as active and take it off half a second later
DOM.Node.AddClass(self.PingContainer.Node, "PingContainerActive");
window.setTimeout(Bind(function(self)
{
DOM.Node.RemoveClass(self.PingContainer.Node, "PingContainerActive");
}, self), 500);
}
return TitleWindow;
})();

134
vis/Code/TraceDrop.js Normal file
View file

@ -0,0 +1,134 @@
class TraceDrop
{
constructor(remotery)
{
this.Remotery = remotery;
// Create a full-page overlay div for dropping files onto
this.DropNode = DOM.Node.CreateHTML("<div id='DropZone' class='DropZone'>Load Remotery Trace</div>");
document.body.appendChild(this.DropNode);
// Attach drop handlers
window.addEventListener("dragenter", () => this.ShowDropZone());
this.DropNode.addEventListener("dragenter", (e) => this.AllowDrag(e));
this.DropNode.addEventListener("dragover", (e) => this.AllowDrag(e));
this.DropNode.addEventListener("dragleave", () => this.HideDropZone());
this.DropNode.addEventListener("drop", (e) => this.OnDrop(e));
}
ShowDropZone()
{
this.DropNode.style.display = "flex";
}
HideDropZone()
{
this.DropNode.style.display = "none";
}
AllowDrag(evt)
{
// Prevent the default drag handler kicking in
evt.preventDefault();
evt.dataTransfer.dropEffect = "copy";
}
OnDrop(evt)
{
// Prevent the default drop handler kicking in
evt.preventDefault();
this.HideDropZone(evt);
// Get the file that was dropped
let files = DOM.Event.GetDropFiles(evt);
if (files.length == 0)
{
alert("No files dropped");
return;
}
if (files.length > 1)
{
alert("Too many files dropped");
return;
}
// Check file type
let file = files[0];
if (!file.name.endsWith(".rbin"))
{
alert("Not the correct .rbin file type");
return;
}
// Background-load the file
var remotery = this.Remotery;
let file_reader = new FileReader();
file_reader.onload = function()
{
// Create the data reader and verify the header
let data_view = new DataView(this.result);
let data_view_reader = new DataViewReader(data_view, 0);
let header = data_view_reader.GetStringOfLength(8);
if (header != "RMTBLOGF")
{
alert("Not a valid Remotery Log File");
return;
}
remotery.Clear();
try
{
// Forward all recorded events to message handlers
while (!data_view_reader.AtEnd())
{
remotery.Server.CallMessageHandlers(data_view_reader);
}
}
catch (e)
{
// The last message may be partially written due to process exit
// Catch this safely as it's a valid state for the file to be in
if (e instanceof RangeError)
{
console.log("Aborted reading last message");
}
}
// After loading completes, populate the UI which wasn't updated during loading
remotery.Console.TriggerUpdate();
// Set frame history for each timeline thread
for (let name in remotery.FrameHistory)
{
let frame_history = remotery.FrameHistory[name];
remotery.SampleTimelineWindow.OnSamples(name, frame_history);
}
remotery.SampleTimelineWindow.DrawAllRows();
for (let name in remotery.ProcessorFrameHistory)
{
let frame_history = remotery.ProcessorFrameHistory[name];
remotery.ProcessorTimelineWindow.OnSamples(name, frame_history);
}
remotery.ProcessorTimelineWindow.DrawAllRows();
// Set the last frame of each thread sample history on the sample windows
for (let name in remotery.SampleWindows)
{
let sample_window = remotery.SampleWindows[name];
let frame_history = remotery.FrameHistory[name];
let frame = frame_history[frame_history.length - 1];
sample_window.OnSamples(frame.NbSamples, frame.SampleDigest, frame.Samples);
}
// Pause for viewing
remotery.TitleWindow.Pause();
};
file_reader.readAsArrayBuffer(file);
}
}

238
vis/Code/WebGL.js Normal file
View file

@ -0,0 +1,238 @@
function assert(condition, message)
{
if (!condition)
{
throw new Error(message || "Assertion failed");
}
}
function glCompileShader(gl, type, name, source)
{
console.log("Compiling " + name);
// Compile the shader
let shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
// Report any errors
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS))
{
console.log("Error compiling " + name);
console.log(gl.getShaderInfoLog(shader));
console.trace();
}
return shader;
}
function glCreateProgram(gl, vshader, fshader)
{
// Attach shaders and link
let program = gl.createProgram();
gl.attachShader(program, vshader);
gl.attachShader(program, fshader);
gl.linkProgram(program);
// Report any errors
if (!gl.getProgramParameter(program, gl.LINK_STATUS))
{
console.log("Failed to link program");
console.trace();
}
return program;
}
function glSetUniform(gl, program, name, value, index)
{
// Get location
const location = gl.getUniformLocation(program, name);
assert(location != null, "Can't find uniform " + name);
// Dispatch to uniform function by type
assert(value != null, "Value is null");
const type = Object.prototype.toString.call(value).slice(8, -1);
switch (type)
{
case "Number":
gl.uniform1f(location, value);
break;
case "WebGLTexture":
gl.activeTexture(gl.TEXTURE0 + index);
gl.bindTexture(gl.TEXTURE_2D, value);
gl.uniform1i(location, index);
break;
default:
assert(false, "Unhandled type " + type);
break;
}
}
function glCreateTexture(gl, width, height, data)
{
const texture = gl.createTexture();
// Set filtering/wrapping to nearest/clamp
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, data);
return texture;
}
const glDynamicBufferType = Object.freeze({
Buffer: 1,
Texture: 2
});
class glDynamicBuffer
{
constructor(gl, element_type, nb_elements, nb_entries, buffer_type)
{
this.gl = gl;
this.elementType = element_type;
this.nbElements = nb_elements;
this.bufferType = buffer_type == undefined ? glDynamicBufferType.Buffer : buffer_type;
this.dirty = false;
this.Resize(nb_entries);
}
BindAsInstanceAttribute(program, attrib_name)
{
assert(this.bufferType == glDynamicBufferType.Buffer, "Can only call BindAsInstanceAttribute with Buffer types");
let gl = this.gl;
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
// The attribute referenced in the program
const attrib_location = gl.getAttribLocation(program, attrib_name);
gl.enableVertexAttribArray(attrib_location);
gl.vertexAttribPointer(attrib_location, this.nbElements, this.elementType, false, 0, 0);
// One per instance
gl.vertexAttribDivisor(attrib_location, 1);
}
UploadData()
{
let gl = this.gl;
switch (this.bufferType)
{
case glDynamicBufferType.Buffer:
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.cpuArray);
break;
case glDynamicBufferType.Texture:
gl.bindTexture(gl.TEXTURE_2D, this.texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.ALPHA, this.nbEntries, 1, 0, gl.ALPHA, gl.UNSIGNED_BYTE, this.cpuArray);
break;
}
}
UploadDirtyData()
{
if (this.dirty)
{
this.UploadData();
this.dirty = false;
}
}
ResizeToFitNextPow2(target_count)
{
let nb_entries = this.nbEntries;
while (target_count > nb_entries)
{
nb_entries <<= 1;
}
if (nb_entries > this.nbEntries)
{
this.Resize(nb_entries);
}
}
Resize(nb_entries)
{
let gl = this.gl;
this.nbEntries = nb_entries;
// Create the CPU array
const old_array = this.cpuArray;
switch (this.elementType)
{
case gl.FLOAT:
this.nbElementBytes = 4;
this.cpuArray = new Float32Array(this.nbElements * this.nbEntries);
break;
case gl.BYTE:
this.nbElementBytes = 1;
this.cpuArray = new Uint8Array(this.nbElements * this.nbEntries);
break;
default:
assert(false, "Unsupported dynamic buffer element type");
}
// Calculate byte size of the buffer
this.nbBytes = this.nbElementBytes * this.nbElements * this.nbEntries;
if (old_array != undefined)
{
// Copy the values of the previous array over
this.cpuArray.set(old_array);
console.log(`glDynamicBuffer: Resizing to ${nb_entries} entries, ${this.nbElements} elements per entry, ${this.nbElementBytes} bytes per element, ${this.nbBytes} bytes total.`);
}
else
{
console.log(`glDynamicBuffer: Creating ${nb_entries} entries, ${this.nbElements} elements per entry, ${this.nbElementBytes} bytes per element, ${this.nbBytes} bytes total.`);
}
// Create the GPU buffer
switch (this.bufferType)
{
case glDynamicBufferType.Buffer:
this.buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
gl.bufferData(gl.ARRAY_BUFFER, this.nbBytes, gl.DYNAMIC_DRAW);
break;
case glDynamicBufferType.Texture:
this.texture = gl.createTexture();
// Point sampling with clamp for indexing
gl.bindTexture(gl.TEXTURE_2D, this.texture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
// Fixed-format for now
assert(this.elementType == gl.BYTE);
assert(this.nbElements == 1);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.ALPHA, this.nbEntries, 1, 0, gl.ALPHA, gl.UNSIGNED_BYTE, this.cpuArray);
break;
default:
assert(false, "Unsupported dynamic buffer type");
}
}
};

119
vis/Code/WebGLFont.js Normal file
View file

@ -0,0 +1,119 @@
class glFont
{
constructor(gl)
{
// Offscreen canvas for rendering individual characters
this.charCanvas = document.createElement("canvas");
this.charContext = this.charCanvas.getContext("2d");
// Describe the font
const font_size = 9;
this.fontWidth = 5;
this.fontHeight = 12;
const font_face = "LocalFiraCode";
const font_desc = font_size + "px " + font_face;
// Ensure the CSS font is loaded before we do any work with it
const self = this;
document.fonts.load(font_desc).then(function (){
// Create a canvas atlas for all characters in the font
const atlas_canvas = document.createElement("canvas");
const atlas_context = atlas_canvas.getContext("2d");
atlas_canvas.width = 16 * self.fontWidth;
atlas_canvas.height = 16 * self.fontHeight;
// Add each character to the atlas
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-+=[]{};\'~#,./<>?!\"£$%%^&*()";
for (let char of chars)
{
// Render this character to the canvas on its own
self.RenderTextToCanvas(char, font_desc, self.fontWidth, self.fontHeight);
// Calculate a location for it in the atlas using its ASCII code
const ascii_code = char.charCodeAt(0);
assert(ascii_code < 256);
const y_index = Math.floor(ascii_code / 16);
const x_index = ascii_code - y_index * 16
assert(x_index < 16);
assert(y_index < 16);
// Copy into the atlas
atlas_context.drawImage(self.charCanvas, x_index * self.fontWidth, y_index * self.fontHeight);
}
// Create the atlas texture and store it in the destination object
self.atlasTexture = glCreateTexture(gl, atlas_canvas.width, atlas_canvas.height, atlas_canvas);
});
}
RenderTextToCanvas(text, font, width, height)
{
// Resize canvas to match
this.charCanvas.width = width;
this.charCanvas.height = height;
// Clear the background
this.charContext.fillStyle = "black";
this.charContext.clearRect(0, 0, width, height);
// Render the text
this.charContext.font = font;
this.charContext.textAlign = "left";
this.charContext.textBaseline = "top";
this.charContext.fillText(text, 0, 1.5);
}
}
class glTextBuffer
{
constructor(gl, font)
{
this.font = font;
this.textMap = {};
this.textBuffer = new glDynamicBuffer(gl, gl.BYTE, 1, 8, glDynamicBufferType.Texture);
this.textBufferPos = 0;
this.textEncoder = new TextEncoder();
}
AddText(text)
{
// Return if it already exists
const existing_entry = this.textMap[text];
if (existing_entry != undefined)
{
return existing_entry;
}
// Add to the map
// Note we're leaving an extra NULL character before every piece of text so that the shader can sample into it on text
// boundaries and sample a zero colour for clamp.
let entry = {
offset: this.textBufferPos + 1,
length: text.length,
};
this.textMap[text] = entry;
// Ensure there's always enough space in the text buffer before adding
this.textBuffer.ResizeToFitNextPow2(entry.offset + entry.length + 1);
this.textBuffer.cpuArray.set(this.textEncoder.encode(text), entry.offset, entry.length);
this.textBuffer.dirty = true;
this.textBufferPos = entry.offset + entry.length;
return entry;
}
UploadData()
{
this.textBuffer.UploadDirtyData();
}
SetAsUniform(gl, program, name, index)
{
glSetUniform(gl, program, name, this.textBuffer.texture, index);
glSetUniform(gl, program, "inTextBufferDesc.fontWidth", this.font.fontWidth);
glSetUniform(gl, program, "inTextBufferDesc.fontHeight", this.font.fontHeight);
glSetUniform(gl, program, "inTextBufferDesc.textBufferLength", this.textBuffer.nbEntries);
}
}

View file

@ -0,0 +1,137 @@
WebSocketConnection = (function()
{
function WebSocketConnection()
{
this.MessageHandlers = { };
this.Socket = null;
this.Console = null;
}
WebSocketConnection.prototype.SetConsole = function(console)
{
this.Console = console;
}
WebSocketConnection.prototype.Connected = function()
{
// Will return true if the socket is also in the process of connecting
return this.Socket != null;
}
WebSocketConnection.prototype.AddConnectHandler = function(handler)
{
this.AddMessageHandler("__OnConnect__", handler);
}
WebSocketConnection.prototype.AddDisconnectHandler = function(handler)
{
this.AddMessageHandler("__OnDisconnect__", handler);
}
WebSocketConnection.prototype.AddMessageHandler = function(message_name, handler)
{
// Create the message handler array on-demand
if (!(message_name in this.MessageHandlers))
this.MessageHandlers[message_name] = [ ];
this.MessageHandlers[message_name].push(handler);
}
WebSocketConnection.prototype.Connect = function(address)
{
// Disconnect if already connected
if (this.Connected())
this.Disconnect();
Log(this, "Connecting to " + address);
this.Socket = new WebSocket(address);
this.Socket.binaryType = "arraybuffer";
this.Socket.onopen = Bind(OnOpen, this);
this.Socket.onmessage = Bind(OnMessage, this);
this.Socket.onclose = Bind(OnClose, this);
this.Socket.onerror = Bind(OnError, this);
}
WebSocketConnection.prototype.Disconnect = function()
{
Log(this, "Disconnecting");
if (this.Connected())
this.Socket.close();
}
WebSocketConnection.prototype.Send = function(msg)
{
if (this.Connected())
this.Socket.send(msg);
}
function Log(self, message)
{
self.Console.Log(message);
}
function CallMessageHandlers(self, message_name, data_view)
{
if (message_name in self.MessageHandlers)
{
var handlers = self.MessageHandlers[message_name];
for (var i in handlers)
handlers[i](self, data_view);
}
}
function OnOpen(self, event)
{
Log(self, "Connected");
CallMessageHandlers(self, "__OnConnect__");
}
function OnClose(self, event)
{
// Clear all references
self.Socket.onopen = null;
self.Socket.onmessage = null;
self.Socket.onclose = null;
self.Socket.onerror = null;
self.Socket = null;
Log(self, "Disconnected");
CallMessageHandlers(self, "__OnDisconnect__");
}
function OnError(self, event)
{
Log(self, "Connection Error ");
}
function OnMessage(self, event)
{
let data_view = new DataView(event.data);
let data_view_reader = new DataViewReader(data_view, 0);
self.CallMessageHandlers(data_view_reader);
}
WebSocketConnection.prototype.CallMessageHandlers = function(data_view_reader)
{
let id = data_view_reader.GetStringOfLength(4);
CallMessageHandlers(this, id, data_view_reader);
}
return WebSocketConnection;
})();