// Copyright (C) 2026 Wildfire Games. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // Profiler2Report module // Create one instance per profiler report you wish to open. // This gives you the interface to access the raw and processed data class Profiler2Report { // Item types returned by the engine ITEM_EVENT = 1; ITEM_ENTER = 2; ITEM_LEAVE = 3; ITEM_ATTRIBUTE = 4; m_used_colours = {}; m_raw_data; m_data; m_data_by_frame; constructor(callback, tryLive, file) { this.refresh(callback, tryLive, file); } data() { return this.m_data; } raw_data() { return this.m_raw_data; } data_by_frame() { return this.m_data_by_frame; } refresh(callback, tryLive, file) { if (tryLive) this.refresh_live(callback, file); else this.refresh_jsonp(callback, file); } refresh_jsonp(callback, source) { const self = this; if (!source) { callback(false); return; } var reader = new FileReader(); reader.onload = function(e) { self.refresh_from_jsonp(callback, e.target.result); }; reader.onerror = function(e) { alert("Failed to load report file"); callback(false); return; }; reader.readAsText(source); } refresh_from_jsonp(callback, content) { const self = this; var script = document.createElement('script'); window.profileDataCB = function(data) { script.parentNode.removeChild(script); var threads = []; data.threads.forEach(function(thread) { var canvas = $(''); threads.push({ 'name': thread.name, 'data': { 'events': concat_events(thread.data) }, 'canvas': canvas.get(0) }); }); self.m_raw_data = { 'threads': threads }; self.compute_data(); callback(true); }; script.innerHTML = content; document.body.appendChild(script); } refresh_live(callback, file) { const self = this; $.ajax({ "url": `http://127.0.0.1:${$("#gameport").val()}/overview`, "dataType": 'json', "success": function(data) { var threads = []; data.threads.forEach(function(thread) { threads.push({ 'name': thread.name }); }); var callback_data = { 'threads': threads, 'completed': 0 }; threads.forEach(function(thread) { self.refresh_thread(callback, thread, callback_data); }); }, "error": function(jqXHR, textStatus, errorThrown) { console.log('Failed to connect to server ("'+textStatus+'")'); callback(false); } }); } refresh_thread(callback, thread, callback_data) { const self = this; $.ajax({ "url": `http://127.0.0.1:${$("#gameport").val()}/query`, "dataType": 'json', "data": { 'thread': thread.name }, "success": function(data) { data.events = concat_events(data); thread.data = data; if (++callback_data.completed == callback_data.threads.length) { self.m_raw_data = { 'threads': callback_data.threads }; self.compute_data(); callback(true); } }, "error": function(jqXHR, textStatus, errorThrown) { alert('Failed to connect to server ("'+textStatus+'")'); } }); } compute_data(range) { this.m_data = { "threads": [] }; this.m_data_by_frame = { "threads": [] }; for (let thread = 0; thread < this.m_raw_data.threads.length; thread++) { const processed_data = this.process_raw_data(this.m_raw_data.threads[thread].data.events, range); if (!processed_data.intervals.length && !processed_data.events.length) continue; this.m_data.threads[thread] = processed_data; this.m_data.threads[thread].intervals_by_type_frame = {}; if (!this.m_data.threads[thread].frames.length) continue; // compute intervals by types and frames if there are frames. for (const type in this.m_data.threads[thread].intervals_by_type) { let current_frame = 0; this.m_data.threads[thread].intervals_by_type_frame[type] = [[]]; for (let i = 0; i < this.m_data.threads[thread].intervals_by_type[type].length; i++) { const event = this.m_data.threads[thread].intervals[this.m_data.threads[thread].intervals_by_type[type][i]]; while (current_frame < this.m_data.threads[thread].frames.length && event.t0 > this.m_data.threads[thread].frames[current_frame].t1) { this.m_data.threads[thread].intervals_by_type_frame[type].push([]); current_frame++; } if (current_frame < this.m_data.threads[thread].frames.length) this.m_data.threads[thread].intervals_by_type_frame[type][current_frame].push(this.m_data.threads[thread].intervals_by_type[type][i]); } } } } process_raw_data(data, range) { if (!data.length) return { 'frames': [], 'events': [], 'intervals': [], 'intervals_by_type': {}, 'tmin': 0, 'tmax': 0 }; var start, end; var tmin, tmax; var frames = []; var last_frame_time_start; var stack = []; for (let i = 0; i < data.length; ++i) { if (data[i][0] == this.ITEM_EVENT && data[i][2] == '__framestart') { if (last_frame_time_start) frames.push({ 't0': last_frame_time_start, 't1': data[i][1] }); last_frame_time_start = data[i][1]; } if (data[i][0] == this.ITEM_ENTER) stack.push(data[i][2]); if (data[i][0] == this.ITEM_LEAVE) stack.pop(); } if (!range) { range = { "tmin": data[0][1], "tmax": data[data.length-1][1] }; } if (range.numframes) { for (let i = data.length - 1; i > 0; --i) { if (data[i][0] == this.ITEM_EVENT && data[i][2] == '__framestart') { end = i; break; } } var framesfound = 0; for (let i = end - 1; i > 0; --i) { if (data[i][0] == this.ITEM_EVENT && data[i][2] == '__framestart') { start = i; if (++framesfound == range.numframes) break; } } tmin = data[start][1]; tmax = data[end][1]; } else if (range.seconds) { end = data.length - 1; for (let i = end; i > 0; --i) { const type = data[i][0]; if (type == this.ITEM_EVENT || type == this.ITEM_ENTER || type == this.ITEM_LEAVE) { tmax = data[i][1]; break; } } tmin = tmax - range.seconds; for (let i = end; i > 0; --i) { const type = data[i][0]; if ((type == this.ITEM_EVENT || type == this.ITEM_ENTER || type == this.ITEM_LEAVE) && data[i][1] < tmin) break; start = i; } } else { start = 0; end = data.length - 1; tmin = range.tmin; tmax = range.tmax; for (let i = data.length-1; i > 0; --i) { const type = data[i][0]; if ((type == this.ITEM_EVENT || type == this.ITEM_ENTER || type == this.ITEM_LEAVE) && data[i][1] < tmax) { end = i; break; } } for (let i = end; i > 0; --i) { const type = data[i][0]; if ((type == this.ITEM_EVENT || type == this.ITEM_ENTER || type == this.ITEM_LEAVE) && data[i][1] < tmin) break; start = i; } // Move the start/end outwards by another frame, so we don't lose data at the edges while (start > 0) { --start; if (data[start][0] == this.ITEM_EVENT && data[start][2] == '__framestart') break; } while (end < data.length-1) { ++end; if (data[end][0] == this.ITEM_EVENT && data[end][2] == '__framestart') break; } } var num_colours = 0; var events = []; // Read events for the entire data period (not just start..end) { let lastWasEvent = false; for (let i = 0; i < data.length; ++i) { if (data[i][0] == this.ITEM_EVENT) { events.push({ 't': data[i][1], 'id': data[i][2] }); lastWasEvent = true; } else if (data[i][0] == this.ITEM_ATTRIBUTE) { if (lastWasEvent) { if (!events[events.length-1].attrs) events[events.length-1].attrs = []; events[events.length-1].attrs.push(data[i][1]); } } else { lastWasEvent = false; } } } var intervals = []; var intervals_by_type = {}; // Read intervals from the focused data period (start..end) stack = []; var lastT = 0; var lastWasEvent = false; for (let i = start; i <= end; ++i) { if (data[i][0] == this.ITEM_EVENT) { // if (data[i][1] < lastT) // console.log('Time went backwards: ' + (data[i][1] - lastT)); lastT = data[i][1]; lastWasEvent = true; } else if (data[i][0] == this.ITEM_ENTER) { // if (data[i][1] < lastT) // console.log('Time - ENTER went backwards: ' + (data[i][1] - lastT) + " - " + JSON.stringify(data[i])); stack.push({ 't0': data[i][1], 'id': data[i][2] }); lastT = data[i][1]; lastWasEvent = false; } else if (data[i][0] == this.ITEM_LEAVE) { // if (data[i][1] < lastT) // console.log('Time - LEAVE went backwards: ' + (data[i][1] - lastT) + " - " + JSON.stringify(data[i])); lastT = data[i][1]; lastWasEvent = false; if (!stack.length) continue; var interval = stack.pop(); if (!this.m_used_colours[interval.id]) this.m_used_colours[interval.id] = new_colour(num_colours++); interval.colour = this.m_used_colours[interval.id]; interval.t1 = data[i][1]; interval.duration = interval.t1 - interval.t0; interval.depth = stack.length; // console.log(JSON.stringify(interval)); intervals.push(interval); if (interval.id in intervals_by_type) intervals_by_type[interval.id].push(intervals.length-1); else intervals_by_type[interval.id] = [intervals.length-1]; if (interval.id == "Script" && interval.attrs && interval.attrs.length) { let curT = interval.t0; for (const subItem in interval.attrs) { const sub = interval.attrs[subItem]; if (sub.search("buffer") != -1) continue; const newInterv = {}; newInterv.t0 = curT; newInterv.duration = +sub.replace(/.+? ([.0-9]+)us/, "$1")/1000000; if (!newInterv.duration) continue; newInterv.t1 = curT + newInterv.duration; curT += newInterv.duration; newInterv.id = "Script:" + sub.replace(/(.+?) ([.0-9]+)us/, "$1"); newInterv.colour = this.m_used_colours[interval.id]; newInterv.depth = interval.depth+1; intervals.push(newInterv); if (newInterv.id in intervals_by_type) intervals_by_type[newInterv.id].push(intervals.length-1); else intervals_by_type[newInterv.id] = [intervals.length-1]; } } } else if (data[i][0] == this.ITEM_ATTRIBUTE) { if (!lastWasEvent && stack.length) { if (!stack[stack.length-1].attrs) stack[stack.length-1].attrs = []; stack[stack.length-1].attrs.push(data[i][1]); } } } return { 'frames': frames, 'events': events, 'intervals': intervals, 'intervals_by_type': intervals_by_type, 'tmin': tmin, 'tmax': tmax }; } }