756 lines
28 KiB
JavaScript
756 lines
28 KiB
JavaScript
|
|
||
|
//
|
||
|
// 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));
|
||
|
|
||
|
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);
|
||
|
|
||
|
// 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.glCanvas);
|
||
|
this.SampleTimelineWindow.SetOnHover(Bind(OnSampleHover, this));
|
||
|
this.SampleTimelineWindow.SetOnSelected(Bind(OnSampleSelected, this));
|
||
|
this.ProcessorTimelineWindow = new TimelineWindow(this.WindowManager, "Processor Timeline", this.Settings, null, this.glCanvas);
|
||
|
|
||
|
this.SampleTimelineWindow.SetOnMoved(Bind(OnTimelineMoved, this));
|
||
|
this.ProcessorTimelineWindow.SetOnMoved(Bind(OnTimelineMoved, this));
|
||
|
|
||
|
this.TraceDrop = new TraceDrop(this);
|
||
|
|
||
|
this.nbGridWindows = 0;
|
||
|
this.gridWindows = { };
|
||
|
this.FrameHistory = { };
|
||
|
this.ProcessorFrameHistory = { };
|
||
|
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);
|
||
|
|
||
|
// Hook up resize event handler
|
||
|
DOM.Event.AddHandler(window, "resize", Bind(OnResizeWindow, this));
|
||
|
OnResizeWindow(this);
|
||
|
}
|
||
|
|
||
|
|
||
|
Remotery.prototype.Clear = function()
|
||
|
{
|
||
|
// Clear timelines
|
||
|
this.SampleTimelineWindow.Clear();
|
||
|
this.ProcessorTimelineWindow.Clear();
|
||
|
|
||
|
// Close and clear all sample windows
|
||
|
for (var i in this.gridWindows)
|
||
|
{
|
||
|
const grid_window = this.gridWindows[i];
|
||
|
grid_window.Close();
|
||
|
}
|
||
|
this.nbGridWindows = 0;
|
||
|
this.gridWindows = { };
|
||
|
|
||
|
this.propertyGridWindow = this.AddGridWindow("__rmt__global__properties__", "Global Properties", new GridConfigProperties());
|
||
|
|
||
|
// Clear runtime data
|
||
|
this.FrameHistory = { };
|
||
|
this.ProcessorFrameHistory = { };
|
||
|
this.PropertyFrameHistory = [ ];
|
||
|
this.SelectedFrames = { };
|
||
|
this.glCanvas.ClearTextResources();
|
||
|
|
||
|
// Resize everything to fit new layout
|
||
|
OnResizeWindow(this);
|
||
|
}
|
||
|
|
||
|
function DrawWindowMask(gl, program, window_node)
|
||
|
{
|
||
|
gl.useProgram(program);
|
||
|
|
||
|
// Using depth as a mask
|
||
|
gl.enable(gl.DEPTH_TEST);
|
||
|
gl.depthFunc(gl.LEQUAL);
|
||
|
|
||
|
// Viewport constants
|
||
|
glSetUniform(gl, program, "inViewport.width", gl.canvas.width);
|
||
|
glSetUniform(gl, program, "inViewport.height", gl.canvas.height);
|
||
|
|
||
|
// Window dimensions
|
||
|
const rect = window_node.getBoundingClientRect();
|
||
|
glSetUniform(gl, program, "minX", rect.left);
|
||
|
glSetUniform(gl, program, "minY", rect.top);
|
||
|
glSetUniform(gl, program, "maxX", rect.left + rect.width);
|
||
|
glSetUniform(gl, program, "maxY", rect.top + rect.height);
|
||
|
|
||
|
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
||
|
}
|
||
|
|
||
|
Remotery.prototype.OnGLCanvasDraw = function(gl, seconds)
|
||
|
{
|
||
|
this.glCanvas.textBuffer.UploadData();
|
||
|
|
||
|
// Draw windows in their z-order, front-to-back
|
||
|
// Depth test is enabled, rejecting equal z, and windows render transparent
|
||
|
// Draw window content first, then draw the invisible window mask
|
||
|
// Any windows that come after another window will not draw where the previous window already masked out depth
|
||
|
for (let window of this.WindowManager.Windows)
|
||
|
{
|
||
|
// Some windows might not have WebGL drawing on them; they need to occlude as well
|
||
|
DrawWindowMask(gl, this.glCanvas.windowProgram, window.Node);
|
||
|
|
||
|
if (window.userData != null)
|
||
|
{
|
||
|
window.userData.Draw();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
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);
|
||
|
}
|
||
|
|
||
|
// 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;
|
||
|
}
|
||
|
|
||
|
|
||
|
Remotery.prototype.AddGridWindow = function(name, display_name, config)
|
||
|
{
|
||
|
const grid_window = new GridWindow(this.WindowManager, display_name, this.nbGridWindows, this.glCanvas, config);
|
||
|
this.gridWindows[name] = grid_window;
|
||
|
this.gridWindows[name].WindowResized(this.SampleTimelineWindow.Window, this.Console.Window);
|
||
|
this.nbGridWindows++;
|
||
|
MoveGridWindows(this);
|
||
|
return grid_window;
|
||
|
}
|
||
|
|
||
|
|
||
|
function DecodeSampleHeader(self, data_view_reader, length)
|
||
|
{
|
||
|
// Message-specific header
|
||
|
let message = { };
|
||
|
message.messageStart = data_view_reader.Offset;
|
||
|
message.thread_name = data_view_reader.GetString();
|
||
|
message.nb_samples = data_view_reader.GetUInt32();
|
||
|
message.partial_tree = data_view_reader.GetUInt32();
|
||
|
|
||
|
// Align sample reading to 32-bit boundary
|
||
|
const align = ((4 - (data_view_reader.Offset & 3)) & 3);
|
||
|
data_view_reader.Offset += align;
|
||
|
message.samplesStart = data_view_reader.Offset;
|
||
|
message.samplesLength = length - (message.samplesStart - message.messageStart);
|
||
|
|
||
|
return message;
|
||
|
}
|
||
|
|
||
|
|
||
|
function SetNanosecondsAsMilliseconds(samples_view, offset)
|
||
|
{
|
||
|
samples_view.setFloat32(offset, samples_view.getFloat64(offset, true) / 1000.0, true);
|
||
|
}
|
||
|
|
||
|
|
||
|
function SetUint32AsFloat32(samples_view, offset)
|
||
|
{
|
||
|
samples_view.setFloat32(offset, samples_view.getUint32(offset, true), true);
|
||
|
}
|
||
|
|
||
|
|
||
|
function ProcessSampleTree(self, sample_data, message)
|
||
|
{
|
||
|
const empty_text_entry = {
|
||
|
offset: 0,
|
||
|
length: 1,
|
||
|
};
|
||
|
|
||
|
const samples_length = message.nb_samples * g_nbBytesPerSample;
|
||
|
const samples_view = new DataView(sample_data, message.samplesStart, samples_length);
|
||
|
message.sampleDataView = samples_view;
|
||
|
|
||
|
for (let offset = 0; offset < samples_length; offset += g_nbBytesPerSample)
|
||
|
{
|
||
|
// 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);
|
||
|
|
||
|
// 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" + name_hash);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Add sample name text buffer location
|
||
|
const text_entry = name.textEntry != null ? name.textEntry : empty_text_entry;
|
||
|
samples_view.setFloat32(offset + g_sampleOffsetBytes_NameOffset, text_entry.offset, true);
|
||
|
samples_view.setFloat32(offset + g_sampleOffsetBytes_NameLength, text_entry.length, true);
|
||
|
|
||
|
// Time in milliseconds
|
||
|
SetNanosecondsAsMilliseconds(samples_view, offset + g_sampleOffsetBytes_Start);
|
||
|
SetNanosecondsAsMilliseconds(samples_view, offset + g_sampleOffsetBytes_Length);
|
||
|
SetNanosecondsAsMilliseconds(samples_view, offset + g_sampleOffsetBytes_Self);
|
||
|
SetNanosecondsAsMilliseconds(samples_view, offset + g_sampleOffsetBytes_GpuToCpu);
|
||
|
|
||
|
// Convert call count/recursion integers to float
|
||
|
SetUint32AsFloat32(samples_view, offset + g_sampleOffsetBytes_Calls);
|
||
|
SetUint32AsFloat32(samples_view, offset + g_sampleOffsetBytes_Recurse);
|
||
|
}
|
||
|
|
||
|
// Convert to floats for GPU
|
||
|
message.sampleFloats = new Float32Array(sample_data, message.samplesStart, message.nb_samples * g_nbFloatsPerSample);
|
||
|
}
|
||
|
|
||
|
function OnSamples(self, 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())
|
||
|
return;
|
||
|
|
||
|
// Binary decode incoming sample data
|
||
|
var message = DecodeSampleHeader(self, data_view_reader, length);
|
||
|
if (message.nb_samples == 0)
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
var name = message.thread_name;
|
||
|
ProcessSampleTree(self, data_view_reader.DataView.buffer, message);
|
||
|
|
||
|
// 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.gridWindows))
|
||
|
{
|
||
|
self.AddGridWindow(name, name, new GridConfigSamples());
|
||
|
}
|
||
|
|
||
|
// 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.gridWindows[name].UpdateEntries(message.nb_samples, message.sampleFloats);
|
||
|
|
||
|
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.glCanvas.nameMap.Set(name_hash, name_string);
|
||
|
}
|
||
|
|
||
|
|
||
|
function OnProcessorThreads(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;
|
||
|
|
||
|
let nb_processors = data_view_reader.GetUInt32();
|
||
|
let message_index = data_view_reader.GetUInt64();
|
||
|
|
||
|
const empty_text_entry = {
|
||
|
offset: 0,
|
||
|
length: 1,
|
||
|
};
|
||
|
|
||
|
// 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
|
||
|
const us_length = sample_time - last_thread_frame.usLastStart;
|
||
|
last_thread_frame.usLastStart = sample_time;
|
||
|
last_thread_frame.EndTime_us += us_length;
|
||
|
const last_length = last_thread_frame.sampleDataView.getFloat32(g_sampleOffsetBytes_Length, true);
|
||
|
last_thread_frame.sampleDataView.setFloat32(g_sampleOffsetBytes_Length, last_length + us_length / 1000.0, true);
|
||
|
|
||
|
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 [ name_exists, thread_name ] = self.glCanvas.nameMap.Get(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())
|
||
|
{
|
||
|
self.Server.Send("GSMP" + thread_name_hash);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 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 = { };
|
||
|
thread_message.nb_samples = 1;
|
||
|
thread_message.sampleData = new ArrayBuffer(g_nbBytesPerSample);
|
||
|
thread_message.sampleDataView = new DataView(thread_message.sampleData);
|
||
|
const sample_data_view = thread_message.sampleDataView;
|
||
|
|
||
|
// Set the name
|
||
|
const text_entry = thread_name.textEntry != null ? thread_name.textEntry : empty_text_entry;
|
||
|
sample_data_view.setFloat32(g_sampleOffsetBytes_NameOffset, text_entry.offset, true);
|
||
|
sample_data_view.setFloat32(g_sampleOffsetBytes_NameLength, text_entry.length, true);
|
||
|
|
||
|
// Make a pastel-y colour from the thread name hash
|
||
|
const hash = thread_name.hash;
|
||
|
sample_data_view.setUint8(g_sampleOffsetBytes_Colour + 0, 127 + (hash & 255) / 2);
|
||
|
sample_data_view.setUint8(g_sampleOffsetBytes_Colour + 1, 127 + ((hash >> 4) & 255) / 2);
|
||
|
sample_data_view.setUint8(g_sampleOffsetBytes_Colour + 2, 127 + ((hash >> 8) & 255) / 2);
|
||
|
|
||
|
// Set the time
|
||
|
sample_data_view.setFloat32(g_sampleOffsetBytes_Start, sample_time / 1000.0, true);
|
||
|
sample_data_view.setFloat32(g_sampleOffsetBytes_Length, 0.25, true);
|
||
|
|
||
|
thread_message.sampleFloats = new Float32Array(thread_message.sampleData, 0, thread_message.nb_samples * g_nbFloatsPerSample);
|
||
|
|
||
|
// 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 UInt64ToFloat32(view, offset)
|
||
|
{
|
||
|
// Unpack as two 32-bit integers so we have a vague attempt at reconstructing the value
|
||
|
const a = view.getUint32(offset + 0, true);
|
||
|
const b = view.getUint32(offset + 4, true);
|
||
|
|
||
|
// Can't do bit arithmetic above 32-bits in JS so combine using power-of-two math
|
||
|
const v = a + (b * Math.pow(2, 32));
|
||
|
|
||
|
// TODO(don): Potentially massive data loss!
|
||
|
snapshots_view.setFloat32(offset, v);
|
||
|
}
|
||
|
|
||
|
|
||
|
function SInt64ToFloat32(view, offset)
|
||
|
{
|
||
|
// Unpack as two 32-bit integers so we have a vague attempt at reconstructing the value
|
||
|
const a = view.getUint32(offset + 0, true);
|
||
|
const b = view.getUint32(offset + 4, true);
|
||
|
|
||
|
// Is this negative?
|
||
|
if (b & 0x80000000)
|
||
|
{
|
||
|
// Can only convert from twos-complement with 32-bit arithmetic so shave off the upper 32-bits
|
||
|
// TODO(don): Crazy data loss here
|
||
|
const v = -(~(a - 1));
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
// Can't do bit arithmetic above 32-bits in JS so combine using power-of-two math
|
||
|
const v = a + (b * Math.pow(2, 32));
|
||
|
}
|
||
|
|
||
|
// TODO(don): Potentially massive data loss!
|
||
|
snapshots_view.setFloat32(offset, v);
|
||
|
}
|
||
|
|
||
|
|
||
|
function DecodeSnapshotHeader(self, data_view_reader, length)
|
||
|
{
|
||
|
// Message-specific header
|
||
|
let message = { };
|
||
|
message.messageStart = data_view_reader.Offset;
|
||
|
message.nbSnapshots = data_view_reader.GetUInt32();
|
||
|
message.propertyFrame = data_view_reader.GetUInt32();
|
||
|
message.snapshotsStart = data_view_reader.Offset;
|
||
|
message.snapshotsLength = length - (message.snapshotsStart - message.messageStart);
|
||
|
return message;
|
||
|
}
|
||
|
|
||
|
|
||
|
function ProcessSnapshots(self, snapshot_data, message)
|
||
|
{
|
||
|
if (self.Settings.IsPaused)
|
||
|
{
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
const empty_text_entry = {
|
||
|
offset: 0,
|
||
|
length: 1,
|
||
|
};
|
||
|
|
||
|
const snapshots_length = message.nbSnapshots * g_nbBytesPerSnapshot;
|
||
|
const snapshots_view = new DataView(snapshot_data, message.snapshotsStart, snapshots_length);
|
||
|
|
||
|
for (let offset = 0; offset < snapshots_length; offset += g_nbBytesPerSnapshot)
|
||
|
{
|
||
|
// 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);
|
||
|
|
||
|
// 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" + name_hash);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Add snapshot name text buffer location
|
||
|
const text_entry = name.textEntry != null ? name.textEntry : empty_text_entry;
|
||
|
snapshots_view.setFloat32(offset + 0, text_entry.offset, true);
|
||
|
snapshots_view.setFloat32(offset + 4, text_entry.length, true);
|
||
|
|
||
|
// Heat colour style falloff to quickly identify modified properties
|
||
|
let r = 255, g = 255, b = 255;
|
||
|
const prev_value_frame = snapshots_view.getUint32(offset + 32, true);
|
||
|
const frame_delta = message.propertyFrame - prev_value_frame;
|
||
|
if (frame_delta < 64)
|
||
|
{
|
||
|
g = Math.min(Math.min(frame_delta, 32) * 8, 255);
|
||
|
b = Math.min(frame_delta * 4, 255);
|
||
|
}
|
||
|
snapshots_view.setUint8(offset + 8, r);
|
||
|
snapshots_view.setUint8(offset + 9, g);
|
||
|
snapshots_view.setUint8(offset + 10, b);
|
||
|
|
||
|
const snapshot_type = snapshots_view.getUint32(offset + 12, true);
|
||
|
switch (snapshot_type)
|
||
|
{
|
||
|
case 1:
|
||
|
case 2:
|
||
|
case 3:
|
||
|
case 4:
|
||
|
case 7:
|
||
|
snapshots_view.setFloat32(offset + 16, snapshots_view.getFloat64(offset + 16, true), true);
|
||
|
snapshots_view.setFloat32(offset + 24, snapshots_view.getFloat64(offset + 24, true), true);
|
||
|
break;
|
||
|
|
||
|
// Unpack 64-bit integers stored full precision in the logs and view them to the best of our current abilities
|
||
|
case 5:
|
||
|
SInt64ToFloat32(snapshots_view, offset + 16);
|
||
|
SInt64ToFloat32(snapshots_view, offset + 24);
|
||
|
case 6:
|
||
|
UInt64ToFloat32(snapshots_view, offset + 16);
|
||
|
UInt64ToFloat32(snapshots_view, offset + 24);
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Convert to floats for GPU
|
||
|
return new Float32Array(snapshot_data, message.snapshotsStart, message.nbSnapshots * g_nbFloatsPerSnapshot);
|
||
|
}
|
||
|
|
||
|
|
||
|
function OnPropertySnapshots(self, 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())
|
||
|
return;
|
||
|
|
||
|
// Binary decode incoming snapshot data
|
||
|
const message = DecodeSnapshotHeader(self, data_view_reader, length);
|
||
|
message.snapshotFloats = ProcessSnapshots(self, data_view_reader.DataView.buffer, message);
|
||
|
|
||
|
// Add to frame history
|
||
|
const thread_frame = new PropertySnapshotFrame(message);
|
||
|
const frame_history = self.PropertyFrameHistory;
|
||
|
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);
|
||
|
|
||
|
// Set on the window if connected as this implies a trace is being loaded, which we want to speed up
|
||
|
if (self.Server.Connected())
|
||
|
{
|
||
|
self.propertyGridWindow.UpdateEntries(message.nbSnapshots, message.snapshotFloats);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
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.gridWindows[name].SetVisible(target.checked);
|
||
|
MoveGridWindows(self);
|
||
|
}
|
||
|
|
||
|
|
||
|
function MoveGridWindows(self)
|
||
|
{
|
||
|
// Stack all windows next to each other
|
||
|
let xpos = 0;
|
||
|
for (let i in self.gridWindows)
|
||
|
{
|
||
|
const grid_window = self.gridWindows[i];
|
||
|
if (grid_window.visible)
|
||
|
{
|
||
|
grid_window.SetXPos(xpos++, self.SampleTimelineWindow.Window, self.Console.Window);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
function OnSampleHover(self, thread_name, hover)
|
||
|
{
|
||
|
if (!self.Settings.IsPaused)
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Search for the grid window for the thread being hovered over
|
||
|
for (let window_thread_name in self.gridWindows)
|
||
|
{
|
||
|
if (window_thread_name == thread_name)
|
||
|
{
|
||
|
const grid_window = self.gridWindows[thread_name];
|
||
|
|
||
|
// Populate with the sample under hover
|
||
|
if (hover != null)
|
||
|
{
|
||
|
const frame = hover[0];
|
||
|
grid_window.UpdateEntries(frame.NbSamples, frame.sampleFloats);
|
||
|
}
|
||
|
|
||
|
// When there's no hover, go back to the selected frame
|
||
|
else if (self.SelectedFrames[thread_name])
|
||
|
{
|
||
|
const frame = self.SelectedFrames[thread_name];
|
||
|
grid_window.UpdateEntries(frame.NbSamples, frame.sampleFloats);
|
||
|
}
|
||
|
|
||
|
// Otherwise display the last sample in the frame
|
||
|
else
|
||
|
{
|
||
|
const frames = self.FrameHistory[thread_name];
|
||
|
const frame = frames[frames.length - 1];
|
||
|
grid_window.UpdateEntries(frame.NbSamples, frame.sampleFloats);
|
||
|
}
|
||
|
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
function OnSampleSelected(self, thread_name, select)
|
||
|
{
|
||
|
// Lookup sample window
|
||
|
if (thread_name in self.gridWindows)
|
||
|
{
|
||
|
const grid_window = self.gridWindows[thread_name];
|
||
|
|
||
|
// Set the grid window to the selected frame if valid
|
||
|
if (select)
|
||
|
{
|
||
|
const frame = select[0];
|
||
|
self.SelectedFrames[thread_name] = frame;
|
||
|
grid_window.UpdateEntries(frame.NbSamples, frame.sampleFloats);
|
||
|
}
|
||
|
|
||
|
// Otherwise deselect
|
||
|
else
|
||
|
{
|
||
|
const frames = self.FrameHistory[thread_name];
|
||
|
const frame = frames[frames.length - 1];
|
||
|
self.SelectedFrames[thread_name] = null;
|
||
|
grid_window.UpdateEntries(frame.NbSamples, frame.sampleFloats);
|
||
|
self.SampleTimelineWindow.Deselect(thread_name);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
function OnResizeWindow(self)
|
||
|
{
|
||
|
var w = window.innerWidth;
|
||
|
var h = window.innerHeight;
|
||
|
|
||
|
// Resize windows
|
||
|
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.gridWindows)
|
||
|
{
|
||
|
self.gridWindows[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);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return Remotery;
|
||
|
})();
|