[Scummvm-git-logs] scummvm master -> 820051314a7d9650fb5e1b94bb418871fd5cc0d3
sev-
noreply at scummvm.org
Tue Aug 25 23:15:31 UTC 2026
This automated email contains information about 3 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
575564dd77 DIRECTOR: DT: Add Lingo execution profiler panel
a50b853d88 DIRECTOR: DT: Improve the Lingo profiler timing, stats and theming
820051314a DIRECTOR: DT: Split the profiler panel into helpers
Commit: 575564dd776eaf47f0a4e51500230a389f156c5a
https://github.com/scummvm/scummvm/commit/575564dd776eaf47f0a4e51500230a389f156c5a
Author: ramyak-sharma (ramyaksharma1 at gmail.com)
Date: 2026-08-26T01:15:25+02:00
Commit Message:
DIRECTOR: DT: Add Lingo execution profiler panel
Add a DT panel that records Lingo handler enter/exit, score-frame
boundaries and LingoState freeze/thaw as an event stream, and renders it
as a Tracy-style flamegraph with frame and freeze/thaw markers along the
top ruler. Capture is off by default, costing only a bool check on the
existing context hooks. Includes a Chrome/Perfetto JSON trace export.
Capture layer: engines/director/lingo/lingo-profiler.{h,cpp}
UI panel: engines/director/debugger/dt-profiler.cpp
Changed paths:
A engines/director/debugger/dt-profiler.cpp
A engines/director/lingo/lingo-profiler.cpp
A engines/director/lingo/lingo-profiler.h
engines/director/debugger.cpp
engines/director/debugger/debugtools.cpp
engines/director/debugger/dt-internal.h
engines/director/debugger/dt-save-state.cpp
engines/director/director.cpp
engines/director/director.h
engines/director/module.mk
engines/director/window.cpp
diff --git a/engines/director/debugger.cpp b/engines/director/debugger.cpp
index 3cda88d8f52..98464a8a932 100644
--- a/engines/director/debugger.cpp
+++ b/engines/director/debugger.cpp
@@ -33,6 +33,7 @@
#include "director/util.h"
#include "director/window.h"
#include "director/lingo/lingo.h"
+#include "director/lingo/lingo-profiler.h"
#include "director/lingo/lingo-code.h"
#include "director/lingo/lingo-codegen.h"
#include "director/lingo/lingo-object.h"
@@ -1350,6 +1351,8 @@ void Debugger::eventHook(LEvent eventId) {
}
void Debugger::pushContextHook() {
+ if (g_director->_lingoProfiler)
+ g_director->_lingoProfiler->onPushContext();
if (_next)
_nextCounter++;
if (_finish)
@@ -1358,6 +1361,8 @@ void Debugger::pushContextHook() {
}
void Debugger::popContextHook() {
+ if (g_director->_lingoProfiler)
+ g_director->_lingoProfiler->onPopContext();
if (_next && _nextCounter > 0)
_nextCounter--;
if (_finish)
diff --git a/engines/director/debugger/debugtools.cpp b/engines/director/debugger/debugtools.cpp
index 2714d38f8f7..8165c008757 100644
--- a/engines/director/debugger/debugtools.cpp
+++ b/engines/director/debugger/debugtools.cpp
@@ -1154,6 +1154,7 @@ void onImGuiRender() {
ImGui::MenuItem("Archive", NULL, &_state->_w.archive);
ImGui::MenuItem("Windows", NULL, &_state->_w.windows);
ImGui::MenuItem("Execution Context", NULL, &_state->_w.executionContext);
+ ImGui::MenuItem("Profiler", NULL, &_state->_w.profiler);
ImGui::Separator();
if (ImGui::MenuItem("Pick from stage", NULL, _state->_pickMode)) {
@@ -1200,6 +1201,7 @@ void onImGuiRender() {
showArchive();
showWindows();
showWatchedVars();
+ showProfiler();
_state->_logger->draw("Logger", &_state->_w.logger);
}
diff --git a/engines/director/debugger/dt-internal.h b/engines/director/debugger/dt-internal.h
index 26354aee31d..8bbbe58acfe 100644
--- a/engines/director/debugger/dt-internal.h
+++ b/engines/director/debugger/dt-internal.h
@@ -104,6 +104,7 @@ typedef struct ImGuiWindows {
bool imageViewer = false;
bool windows = false;
bool help = false;
+ bool profiler = false;
} ImGuiWindows;
// Rebindable debugger actions. Keep in sync with kShortcutDefs (dt-help.cpp).
@@ -444,6 +445,7 @@ void showImageViewer(); // dt-castdetails.cpp
void showCastDetails(); // dt-castdetails.cpp
void showControlPanel();// dt-controlpanel.cpp
void handleDebuggerShortcuts(); // dt-controlpanel.cpp
+void showProfiler(); // dt-profiler.cpp
// dt-help.cpp
extern const ShortcutDef kShortcutDefs[kActCount];
diff --git a/engines/director/debugger/dt-profiler.cpp b/engines/director/debugger/dt-profiler.cpp
new file mode 100644
index 00000000000..bd6bec99a79
--- /dev/null
+++ b/engines/director/debugger/dt-profiler.cpp
@@ -0,0 +1,491 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/algorithm.h"
+#include "common/hashmap.h"
+#include "common/path.h"
+
+#include "director/director.h"
+#include "director/archive.h"
+#include "director/movie.h"
+#include "director/debugger/dt-internal.h"
+#include "director/lingo/lingo-profiler.h"
+
+namespace Director {
+namespace DT {
+
+static bool openHandlerScript(const Common::String &handlerName) {
+ Movie *movie = g_director ? g_director->getCurrentMovie() : nullptr;
+ if (!movie || handlerName.empty())
+ return false;
+
+ ScriptContext *ctx = nullptr;
+ for (auto &it : *movie->getCasts()) {
+ ctx = resolveHandlerContext(-1, CastMemberID(0, it._key), handlerName);
+ if (ctx)
+ break;
+ }
+ if (!ctx)
+ ctx = resolveHandlerContext(-1, CastMemberID(0, SHARED_CAST_LIB), handlerName);
+ if (!ctx)
+ return false;
+
+ int castLibID = getCastLibIDForContext(ctx);
+ Common::String moviePath = movie->getArchive()->getPathName().toString();
+ addToOpenHandlers(buildImGuiHandlerScript(ctx, castLibID, handlerName, moviePath));
+ return true;
+}
+
+struct ProfZone {
+ uint32 startSeq;
+ uint32 endSeq;
+ uint32 nameId;
+ uint32 movieId;
+ uint32 startFrame;
+ uint32 endFrame;
+ uint16 depth;
+};
+
+struct StatRow {
+ uint32 nameId;
+ uint32 count;
+ uint64 totalSpan;
+};
+
+struct DrawnRect {
+ float x0, y0, x1, y1;
+ uint32 nameId;
+};
+
+static bool statGreater(const StatRow &a, const StatRow &b) {
+ return a.count > b.count;
+}
+
+static ImU32 nameColor(uint32 id) {
+ uint32 h = (id + 1) * 2654435761u;
+ int r = 130 + (int)((h >> 1) & 0x4F);
+ int g = 130 + (int)((h >> 9) & 0x4F);
+ int b = 130 + (int)((h >> 17) & 0x4F);
+ return IM_COL32(r, g, b, 255);
+}
+
+static inline float fabsff(float f) { return f < 0.0f ? -f : f; }
+
+void showProfiler() {
+ if (!_state->_w.profiler)
+ return;
+
+ if (!ImGui::Begin("Profiler", &_state->_w.profiler)) {
+ ImGui::End();
+ return;
+ }
+
+ LingoProfiler *prof = g_director ? g_director->_lingoProfiler : nullptr;
+ if (!prof) {
+ ImGui::TextUnformatted("Profiler unavailable");
+ ImGui::End();
+ return;
+ }
+
+ static Common::Array<ProfZone> zones;
+ static uint builtCount = 0xFFFFFFFFu;
+ static uint16 maxRow = 0;
+ static uint32 maxSeq = 0;
+ static float viewStart = 0.0f;
+ static float viewSpan = 200.0f;
+ static bool follow = true;
+ static int selected = -1;
+
+ static bool animActive = false;
+ static float animT = 0.0f;
+ static float aFromStart = 0.0f, aToStart = 0.0f, aFromSpan = 0.0f, aToSpan = 0.0f;
+
+ bool capturing = prof->isEnabled();
+ if (ImGui::Checkbox("Capture", &capturing))
+ prof->setEnabled(capturing);
+ if (ImGui::IsItemHovered())
+ ImGui::SetTooltip("Record Lingo handler calls, freeze/thaw and frames.\nOff by default so it costs nothing.");
+
+ ImGui::SameLine();
+ if (ImGui::Button("Clear"))
+ prof->clear();
+
+ ImGui::SameLine();
+ static Common::String status;
+ if (ImGui::Button("Export JSON")) {
+ Common::Path path("lingo-trace.json");
+ status = prof->exportChromeTrace(path) ? Common::String("wrote lingo-trace.json") : Common::String("export failed");
+ }
+ if (ImGui::IsItemHovered())
+ ImGui::SetTooltip("Write a Chrome/Perfetto trace to lingo-trace.json.");
+
+ ImGui::SameLine();
+ ImGui::Checkbox("Follow live", &follow);
+ if (ImGui::IsItemHovered())
+ ImGui::SetTooltip("Keep the newest event (red line) at the right edge.\nAny zoom/pan turns this off.");
+
+ const Common::Array<ProfilerEvent> &events = prof->events();
+ if (builtCount != events.size()) {
+ builtCount = events.size();
+ zones.clear();
+ maxRow = 0;
+
+ Common::Array<uint> openStack;
+ Common::Array<uint16> offStack;
+ uint16 depthOff = 0;
+
+ for (uint i = 0; i < events.size(); i++) {
+ const ProfilerEvent &e = events[i];
+ switch (e.type) {
+ case kProfBegin: {
+ ProfZone z;
+ z.startSeq = e.seq;
+ z.endSeq = e.seq;
+ z.nameId = e.nameId;
+ z.movieId = e.movieId;
+ z.startFrame = e.frame;
+ z.endFrame = e.frame;
+ z.depth = e.depth + depthOff;
+ if (z.depth > maxRow)
+ maxRow = z.depth;
+ zones.push_back(z);
+ openStack.push_back(zones.size() - 1);
+ break;
+ }
+ case kProfEnd:
+ if (!openStack.empty()) {
+ uint idx = openStack.back();
+ openStack.pop_back();
+ zones[idx].endSeq = e.seq;
+ zones[idx].endFrame = e.frame;
+ }
+ break;
+ case kProfFreeze:
+ offStack.push_back(depthOff);
+ depthOff += e.depth;
+ break;
+ case kProfThaw:
+ if (!offStack.empty()) {
+ depthOff = offStack.back();
+ offStack.pop_back();
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ maxSeq = events.empty() ? 0 : events.back().seq;
+ for (uint i = 0; i < openStack.size(); i++)
+ zones[openStack[i]].endSeq = maxSeq + 1;
+
+ if (selected >= (int)zones.size())
+ selected = -1;
+
+ if (viewSpan < 4.0f)
+ viewSpan = 200.0f;
+ }
+
+ ImGui::SameLine();
+ if (ImGui::Button("Fit")) {
+ aFromStart = viewStart;
+ aFromSpan = viewSpan;
+ aToStart = 0.0f;
+ aToSpan = (maxSeq > 10) ? (float)maxSeq : 200.0f;
+ animT = 0.0f;
+ animActive = true;
+ follow = false;
+ }
+ if (ImGui::IsItemHovered())
+ ImGui::SetTooltip("Zoom out to the whole trace.");
+
+ ImGui::SameLine();
+ ImGui::Text("%u events%s | wheel: zoom drag: pan dbl-click: zoom to call",
+ (unsigned)events.size(), prof->isFull() ? " (full)" : "");
+
+ if (selected >= 0 && (uint)selected < zones.size()) {
+ const ProfZone &z = zones[selected];
+ if (ImGui::SmallButton("Open script"))
+ openHandlerScript(prof->internedName(z.nameId));
+ if (ImGui::IsItemHovered())
+ ImGui::SetTooltip("Open this handler's script in the Scripts window.");
+ ImGui::SameLine();
+ ImGui::Text("selected: %s frames %u-%u span %u depth %u movie %s",
+ prof->internedName(z.nameId).c_str(), z.startFrame, z.endFrame,
+ z.endSeq - z.startSeq, z.depth, prof->internedName(z.movieId).c_str());
+ } else {
+ ImGui::TextUnformatted("selected: (none) -- click a call to select, double-click to zoom, Open script for its code");
+ }
+
+ if (ImGui::CollapsingHeader("Statistics")) {
+ Common::HashMap<uint32, uint> idxByName;
+ Common::Array<StatRow> rows;
+ for (uint i = 0; i < zones.size(); i++) {
+ const ProfZone &z = zones[i];
+ uint32 span = z.endSeq > z.startSeq ? z.endSeq - z.startSeq : 0;
+ Common::HashMap<uint32, uint>::iterator it = idxByName.find(z.nameId);
+ if (it == idxByName.end()) {
+ StatRow r;
+ r.nameId = z.nameId;
+ r.count = 1;
+ r.totalSpan = span;
+ idxByName[z.nameId] = rows.size();
+ rows.push_back(r);
+ } else {
+ rows[it->_value].count++;
+ rows[it->_value].totalSpan += span;
+ }
+ }
+ Common::sort(rows.begin(), rows.end(), statGreater);
+
+ ImGui::BeginChild("##stats", ImVec2(0, 160.0f), ImGuiChildFlags_Borders);
+ ImGui::Text("%8s %10s %s", "calls", "total(seq)", "handler (click to open)");
+ uint shown = rows.size() < 50 ? rows.size() : 50;
+ for (uint i = 0; i < shown; i++) {
+ Common::String row = Common::String::format("%8u %10.0f %s", rows[i].count,
+ (double)rows[i].totalSpan, prof->internedName(rows[i].nameId).c_str());
+ ImGui::PushID((int)i);
+ if (ImGui::Selectable(row.c_str()))
+ openHandlerScript(prof->internedName(rows[i].nameId));
+ ImGui::PopID();
+ }
+ ImGui::EndChild();
+ }
+
+ if (animActive) {
+ animT += ImGui::GetIO().DeltaTime * 4.0f;
+ float t = animT >= 1.0f ? 1.0f : animT;
+ float v = t * t * (3.0f - 2.0f * t);
+ viewStart = aFromStart + (aToStart - aFromStart) * v;
+ viewSpan = aFromSpan + (aToSpan - aFromSpan) * v;
+ if (animT >= 1.0f)
+ animActive = false;
+ }
+
+ const float rowH = 18.0f;
+ const float rulerH = 22.0f;
+ ImGui::BeginChild("##timeline", ImVec2(0, 0), ImGuiChildFlags_Borders);
+
+ const float viewW = ImGui::GetContentRegionAvail().x;
+ float availH = ImGui::GetContentRegionAvail().y;
+
+ if (viewSpan < 4.0f)
+ viewSpan = 4.0f;
+ if (follow && !animActive)
+ viewStart = (float)maxSeq - viewSpan * 0.5f;
+
+ const ImVec2 origin = ImGui::GetCursorScreenPos();
+ float canvasH = (float)(maxRow + 3) * rowH + 20.0f;
+ if (canvasH < availH)
+ canvasH = availH;
+
+ ImGui::InvisibleButton("##canvas", ImVec2(viewW, canvasH));
+ const bool itemHovered = ImGui::IsItemHovered();
+ const bool itemActive = ImGui::IsItemActive();
+ const ImVec2 mouse = ImGui::GetIO().MousePos;
+
+ float pxPerUnit = viewW / viewSpan;
+
+ if (itemHovered) {
+ float wheel = ImGui::GetIO().MouseWheel;
+ if (wheel != 0.0f) {
+ float cursorSeq = viewStart + (mouse.x - origin.x) / pxPerUnit;
+ float factor = 1.0f - wheel * 0.15f;
+ if (factor < 0.2f)
+ factor = 0.2f;
+ viewSpan *= factor;
+ if (viewSpan < 4.0f)
+ viewSpan = 4.0f;
+ if (viewSpan > (float)(maxSeq + 50))
+ viewSpan = (float)(maxSeq + 50);
+ pxPerUnit = viewW / viewSpan;
+ viewStart = cursorSeq - (mouse.x - origin.x) / pxPerUnit;
+ follow = false;
+ animActive = false;
+ }
+ }
+ if (itemActive && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
+ viewStart -= ImGui::GetIO().MouseDelta.x / pxPerUnit;
+ follow = false;
+ animActive = false;
+ }
+
+ const ImVec2 dragDelta = ImGui::GetMouseDragDelta(ImGuiMouseButton_Left);
+ const bool clicked = itemHovered && ImGui::IsMouseReleased(ImGuiMouseButton_Left) &&
+ fabsff(dragDelta.x) < 3.0f && fabsff(dragDelta.y) < 3.0f;
+ const bool dblClicked = itemHovered && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left);
+
+ ImDrawList *dl = ImGui::GetWindowDrawList();
+ const float baseY = origin.y + rulerH;
+ const ImU32 frameLineCol = ImGui::GetColorU32(_state->theme->line_color);
+
+ const float visStartSeq = viewStart - 2.0f;
+ const float visEndSeq = viewStart + viewSpan + 2.0f;
+ const uint kMaxSlices = 6000;
+ uint drawn = 0;
+ int hoveredZone = -1;
+
+ Common::Array<float> rowRight;
+ rowRight.resize((uint)maxRow + 2);
+ for (uint i = 0; i < rowRight.size(); i++)
+ rowRight[i] = -1.0e9f;
+
+ dl->AddRectFilled(ImVec2(origin.x, origin.y), ImVec2(origin.x + viewW, origin.y + rulerH), IM_COL32(0, 0, 0, 40));
+ dl->AddLine(ImVec2(origin.x, origin.y + rulerH), ImVec2(origin.x + viewW, origin.y + rulerH), frameLineCol, 1.0f);
+
+ float lastFrameLabelX = -1.0e9f;
+ float lastFreezeX = -1.0e9f;
+ float lastThawX = -1.0e9f;
+ for (uint i = 0; i < events.size(); i++) {
+ const ProfilerEvent &e = events[i];
+ if (e.type != kProfFrame && e.type != kProfFreeze && e.type != kProfThaw)
+ continue;
+ if ((float)e.seq < visStartSeq || (float)e.seq > visEndSeq)
+ continue;
+ const float x = origin.x + ((float)e.seq - viewStart) * pxPerUnit;
+ if (e.type == kProfFrame) {
+ if (x - lastFrameLabelX < 44.0f)
+ continue;
+ lastFrameLabelX = x;
+ dl->AddLine(ImVec2(x, origin.y + rulerH - 6.0f), ImVec2(x, origin.y + rulerH), frameLineCol, 1.0f);
+ dl->AddLine(ImVec2(x, origin.y + rulerH), ImVec2(x, origin.y + canvasH), IM_COL32(140, 140, 140, 22), 1.0f);
+ Common::String lbl = Common::String::format("f%u", e.frame);
+ dl->AddText(ImVec2(x + 2.0f, origin.y + 3.0f), _state->theme->gridTextColor, lbl.c_str());
+ } else if (e.type == kProfFreeze) {
+ if (x - lastFreezeX < 4.0f)
+ continue;
+ lastFreezeX = x;
+ dl->AddTriangleFilled(ImVec2(x - 3.0f, origin.y + 2.0f), ImVec2(x + 3.0f, origin.y + 2.0f),
+ ImVec2(x, origin.y + 8.0f), IM_COL32(230, 120, 60, 255));
+ } else {
+ if (x - lastThawX < 4.0f)
+ continue;
+ lastThawX = x;
+ dl->AddTriangleFilled(ImVec2(x - 3.0f, origin.y + rulerH - 2.0f), ImVec2(x + 3.0f, origin.y + rulerH - 2.0f),
+ ImVec2(x, origin.y + rulerH - 8.0f), IM_COL32(90, 200, 120, 255));
+ }
+ }
+
+ Common::Array<DrawnRect> drawnRects;
+ for (uint i = 0; i < zones.size(); i++) {
+ const ProfZone &z = zones[i];
+ if ((float)z.endSeq < visStartSeq || (float)z.startSeq > visEndSeq)
+ continue;
+
+ float x0 = origin.x + ((float)z.startSeq - viewStart) * pxPerUnit;
+ float x1 = origin.x + ((float)z.endSeq - viewStart) * pxPerUnit;
+ if (x1 < x0 + 1.0f)
+ x1 = x0 + 1.0f;
+
+ const uint rr = z.depth < rowRight.size() ? z.depth : rowRight.size() - 1;
+ if (x1 - x0 < 1.5f && x0 < rowRight[rr] + 1.0f)
+ continue;
+ rowRight[rr] = x1;
+
+ if (drawn >= kMaxSlices)
+ break;
+ drawn++;
+
+ const float y0 = baseY + (float)z.depth * rowH;
+ const float y1 = y0 + rowH - 1.0f;
+
+ dl->AddRectFilled(ImVec2(x0, y0), ImVec2(x1, y1), nameColor(z.nameId));
+ if (x1 - x0 > 3.0f)
+ dl->AddRect(ImVec2(x0, y0), ImVec2(x1, y1), IM_COL32(0, 0, 0, 100));
+
+ if (x1 - x0 > 24.0f) {
+ const Common::String &nm = prof->internedName(z.nameId);
+ dl->PushClipRect(ImVec2(x0 + 2.0f, y0), ImVec2(x1 - 2.0f, y1), true);
+ dl->AddText(ImVec2(x0 + 3.0f, y0 + 2.0f), IM_COL32(20, 20, 20, 255), nm.c_str());
+ dl->PopClipRect();
+ }
+
+ DrawnRect dr;
+ dr.x0 = x0; dr.y0 = y0; dr.x1 = x1; dr.y1 = y1; dr.nameId = z.nameId;
+ drawnRects.push_back(dr);
+
+ if (itemHovered && mouse.x >= x0 && mouse.x <= x1 && mouse.y >= y0 && mouse.y <= y1)
+ hoveredZone = (int)i;
+
+ if ((int)i == selected)
+ dl->AddRect(ImVec2(x0 - 1.0f, y0 - 1.0f), ImVec2(x1 + 1.0f, y1 + 1.0f), IM_COL32(255, 220, 40, 255), 0.0f, 0, 2.0f);
+ }
+
+ if (hoveredZone >= 0 && (uint)hoveredZone < zones.size()) {
+ uint32 hn = zones[hoveredZone].nameId;
+ for (uint i = 0; i < drawnRects.size(); i++) {
+ const DrawnRect &dr = drawnRects[i];
+ if (dr.nameId == hn)
+ dl->AddRect(ImVec2(dr.x0, dr.y0), ImVec2(dr.x1, dr.y1), IM_COL32(255, 255, 180, 200), 0.0f, 0, 1.5f);
+ }
+ }
+
+ {
+ const float px = origin.x + ((float)maxSeq - viewStart) * pxPerUnit;
+ if (px >= origin.x && px <= origin.x + viewW)
+ dl->AddLine(ImVec2(px, origin.y), ImVec2(px, origin.y + canvasH), IM_COL32(230, 40, 40, 255), 2.0f);
+ }
+
+ if (itemHovered)
+ dl->AddLine(ImVec2(mouse.x, origin.y), ImVec2(mouse.x, origin.y + canvasH), IM_COL32(150, 150, 150, 90), 1.0f);
+
+ if (zones.empty())
+ dl->AddText(ImVec2(origin.x + 12.0f, origin.y + 12.0f), _state->theme->gridTextColor,
+ "Tick Capture, then interact with the game to record Lingo execution.");
+
+ if (dblClicked && hoveredZone >= 0 && (uint)hoveredZone < zones.size()) {
+ const ProfZone &z = zones[hoveredZone];
+ float span = (float)(z.endSeq - z.startSeq);
+ if (span < 1.0f)
+ span = 1.0f;
+ float pad = span * 0.15f + 1.0f;
+ aFromStart = viewStart;
+ aFromSpan = viewSpan;
+ aToStart = (float)z.startSeq - pad;
+ aToSpan = span + 2.0f * pad;
+ animT = 0.0f;
+ animActive = true;
+ follow = false;
+ selected = hoveredZone;
+ } else if (clicked) {
+ selected = hoveredZone;
+ }
+
+ ImGui::EndChild();
+
+ if (hoveredZone >= 0 && (uint)hoveredZone < zones.size()) {
+ const ProfZone &z = zones[hoveredZone];
+ ImGui::BeginTooltip();
+ ImGui::Text("%s", prof->internedName(z.nameId).c_str());
+ ImGui::Text("movie: %s", prof->internedName(z.movieId).c_str());
+ if (z.startFrame == z.endFrame)
+ ImGui::Text("frame %u", z.startFrame);
+ else
+ ImGui::Text("frames %u - %u", z.startFrame, z.endFrame);
+ ImGui::Text("span: %u depth: %u", z.endSeq - z.startSeq, z.depth);
+ ImGui::EndTooltip();
+ }
+
+ ImGui::End();
+}
+
+} // End of namespace DT
+} // End of namespace Director
diff --git a/engines/director/debugger/dt-save-state.cpp b/engines/director/debugger/dt-save-state.cpp
index fb3b17842d6..0633804ca5e 100644
--- a/engines/director/debugger/dt-save-state.cpp
+++ b/engines/director/debugger/dt-save-state.cpp
@@ -45,6 +45,7 @@ Common::Array<WindowFlag> getWindowFlags() {
{ "Execution Context", &_state->_w.executionContext },
{ "Functions", &_state->_w.funcList },
{ "Log", &_state->_w.logger },
+ { "Profiler", &_state->_w.profiler },
{ "Score", &_state->_w.score },
{ "Settings", &_state->_w.settings },
{ "Vars", &_state->_w.vars },
diff --git a/engines/director/director.cpp b/engines/director/director.cpp
index 845b1727c90..7857289f157 100644
--- a/engines/director/director.cpp
+++ b/engines/director/director.cpp
@@ -38,6 +38,7 @@
#include "director/score.h"
#include "director/sound.h"
#include "director/window.h"
+#include "director/lingo/lingo-profiler.h"
#include "director/debugger/debugtools.h"
/**
@@ -56,6 +57,7 @@ DirectorEngine::DirectorEngine(OSystem *syst, const DirectorGameDescription *gam
g_director = this;
g_debugger = new Debugger();
setDebugger(g_debugger);
+ _lingoProfiler = new LingoProfiler();
// parseOptions depends on the _dirSeparator
_version = getDescriptionVersion();
@@ -161,6 +163,7 @@ DirectorEngine::DirectorEngine(OSystem *syst, const DirectorGameDescription *gam
DirectorEngine::~DirectorEngine() {
delete _lingo;
+ delete _lingoProfiler;
clearPalettes();
diff --git a/engines/director/director.h b/engines/director/director.h
index a454b81dcb4..82b393d4d0d 100644
--- a/engines/director/director.h
+++ b/engines/director/director.h
@@ -58,6 +58,7 @@ class Cast;
class Debugger;
class DirectorSound;
class Lingo;
+class LingoProfiler;
class Movie;
class Window;
struct Picture;
@@ -314,6 +315,8 @@ public:
int _keyCode;
byte _keyFlags;
+ LingoProfiler *_lingoProfiler = nullptr;
+
private:
byte _currentPalette[768];
uint16 _currentPaletteLength;
diff --git a/engines/director/lingo/lingo-profiler.cpp b/engines/director/lingo/lingo-profiler.cpp
new file mode 100644
index 00000000000..130442a065d
--- /dev/null
+++ b/engines/director/lingo/lingo-profiler.cpp
@@ -0,0 +1,246 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/file.h"
+#include "common/path.h"
+
+#include "director/director.h"
+#include "director/movie.h"
+#include "director/score.h"
+#include "director/window.h"
+#include "director/lingo/lingo.h"
+#include "director/lingo/lingo-profiler.h"
+
+namespace Director {
+
+LingoProfiler::LingoProfiler() {
+ _enabled = false;
+ _full = false;
+ _seq = 0;
+ _maxEvents = 500000;
+ _haveLast = false;
+ _lastFrame = 0;
+ _lastMovieId = 0;
+
+ _strings.push_back(Common::String());
+ _intern[Common::String()] = 0;
+}
+
+uint32 LingoProfiler::intern(const Common::String &s) {
+ Common::HashMap<Common::String, uint32>::iterator it = _intern.find(s);
+ if (it != _intern.end())
+ return it->_value;
+
+ uint32 id = _strings.size();
+ _strings.push_back(s);
+ _intern[s] = id;
+ return id;
+}
+
+const Common::String &LingoProfiler::internedName(uint32 id) const {
+ if (id >= _strings.size())
+ return _strings[0];
+ return _strings[id];
+}
+
+uint32 LingoProfiler::currentFrame() const {
+ if (!g_director)
+ return 0;
+ Window *window = g_director->getCurrentWindow();
+ if (!window)
+ return 0;
+ Movie *movie = window->getCurrentMovie();
+ if (!movie || !movie->getScore())
+ return 0;
+ return movie->getScore()->getCurrentFrameNum();
+}
+
+Common::String LingoProfiler::currentMovieName() const {
+ if (!g_director)
+ return Common::String("?");
+ Window *window = g_director->getCurrentWindow();
+ if (!window)
+ return Common::String("?");
+ Movie *movie = window->getCurrentMovie();
+ if (!movie)
+ return Common::String("?");
+ Common::String name = movie->getMacName();
+ return name.empty() ? Common::String("movie") : name;
+}
+
+void LingoProfiler::record(uint8 type, uint32 nameId) {
+ if (_full)
+ return;
+ if (_events.size() >= _maxEvents) {
+ _full = true;
+ return;
+ }
+
+ uint32 frame = currentFrame();
+ uint32 movieId = intern(currentMovieName());
+
+ if (!_haveLast || frame != _lastFrame || movieId != _lastMovieId) {
+ ProfilerEvent fe;
+ fe.type = kProfFrame;
+ fe.seq = _seq++;
+ fe.frame = frame;
+ fe.depth = 0;
+ fe.nameId = 0;
+ fe.movieId = movieId;
+ _events.push_back(fe);
+
+ _haveLast = true;
+ _lastFrame = frame;
+ _lastMovieId = movieId;
+ }
+
+ uint16 depth = 0;
+ if (g_lingo && g_lingo->_state)
+ depth = (uint16)g_lingo->_state->callstack.size();
+
+ ProfilerEvent e;
+ e.type = type;
+ e.seq = _seq++;
+ e.frame = frame;
+ e.depth = depth;
+ e.nameId = nameId;
+ e.movieId = movieId;
+ _events.push_back(e);
+}
+
+void LingoProfiler::onPushContext() {
+ if (!_enabled)
+ return;
+
+ Common::String name("<anon>");
+ if (g_lingo && g_lingo->_state && !g_lingo->_state->callstack.empty()) {
+ Symbol &sym = g_lingo->_state->callstack.back()->sp;
+ if (sym.name)
+ name = *sym.name;
+ }
+ record(kProfBegin, intern(name));
+}
+
+void LingoProfiler::onPopContext() {
+ if (!_enabled)
+ return;
+ record(kProfEnd, 0);
+}
+
+void LingoProfiler::onFreeze() {
+ if (!_enabled)
+ return;
+ record(kProfFreeze, 0);
+}
+
+void LingoProfiler::onThaw() {
+ if (!_enabled)
+ return;
+ record(kProfThaw, 0);
+}
+
+void LingoProfiler::clear() {
+ _events.clear();
+ _strings.clear();
+ _intern.clear();
+ _strings.push_back(Common::String());
+ _intern[Common::String()] = 0;
+ _seq = 0;
+ _full = false;
+ _haveLast = false;
+ _lastFrame = 0;
+ _lastMovieId = 0;
+}
+
+static Common::String jsonEscape(const Common::String &s) {
+ Common::String out;
+ for (uint i = 0; i < s.size(); i++) {
+ char c = s[i];
+ if (c == '"' || c == '\\') {
+ out += '\\';
+ out += c;
+ } else if (c == '\n') {
+ out += "\\n";
+ } else if (c == '\t') {
+ out += "\\t";
+ } else if ((byte)c < 0x20) {
+ } else {
+ out += c;
+ }
+ }
+ return out;
+}
+
+bool LingoProfiler::exportChromeTrace(const Common::Path &path) {
+ Common::DumpFile out;
+ if (!out.open(path))
+ return false;
+
+ out.writeString("{\"traceEvents\":[\n");
+
+ for (uint32 id = 1; id < _strings.size(); id++) {
+ Common::String line = Common::String::format(
+ "{\"name\":\"thread_name\",\"ph\":\"M\",\"pid\":1,\"tid\":%u,\"args\":{\"name\":\"%s\"}},\n",
+ id, jsonEscape(_strings[id]).c_str());
+ out.writeString(line);
+ }
+
+ for (uint i = 0; i < _events.size(); i++) {
+ const ProfilerEvent &e = _events[i];
+ Common::String line;
+ switch (e.type) {
+ case kProfBegin:
+ line = Common::String::format(
+ "{\"name\":\"%s\",\"ph\":\"B\",\"ts\":%u,\"pid\":1,\"tid\":%u},\n",
+ jsonEscape(internedName(e.nameId)).c_str(), e.seq, e.movieId);
+ break;
+ case kProfEnd:
+ line = Common::String::format(
+ "{\"ph\":\"E\",\"ts\":%u,\"pid\":1,\"tid\":%u},\n", e.seq, e.movieId);
+ break;
+ case kProfFrame:
+ line = Common::String::format(
+ "{\"name\":\"frame %u\",\"ph\":\"i\",\"ts\":%u,\"pid\":1,\"tid\":%u,\"s\":\"g\"},\n",
+ e.frame, e.seq, e.movieId);
+ break;
+ case kProfFreeze:
+ line = Common::String::format(
+ "{\"name\":\"freeze\",\"ph\":\"i\",\"ts\":%u,\"pid\":1,\"tid\":%u,\"s\":\"t\"},\n",
+ e.seq, e.movieId);
+ break;
+ case kProfThaw:
+ line = Common::String::format(
+ "{\"name\":\"thaw\",\"ph\":\"i\",\"ts\":%u,\"pid\":1,\"tid\":%u,\"s\":\"t\"},\n",
+ e.seq, e.movieId);
+ break;
+ default:
+ break;
+ }
+ out.writeString(line);
+ }
+
+ out.writeString("{\"name\":\"process_name\",\"ph\":\"M\",\"pid\":1,\"args\":{\"name\":\"Lingo\"}}\n");
+ out.writeString("]}\n");
+ out.close();
+ return true;
+}
+
+} // End of namespace Director
diff --git a/engines/director/lingo/lingo-profiler.h b/engines/director/lingo/lingo-profiler.h
new file mode 100644
index 00000000000..c21e9764407
--- /dev/null
+++ b/engines/director/lingo/lingo-profiler.h
@@ -0,0 +1,96 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef DIRECTOR_LINGO_LINGO_PROFILER_H
+#define DIRECTOR_LINGO_LINGO_PROFILER_H
+
+#include "common/array.h"
+#include "common/hashmap.h"
+#include "common/hash-str.h"
+#include "common/str.h"
+#include "common/str-array.h"
+
+namespace Common {
+class Path;
+}
+
+namespace Director {
+
+enum ProfilerEventType {
+ kProfBegin = 0,
+ kProfEnd,
+ kProfFrame,
+ kProfFreeze,
+ kProfThaw
+};
+
+struct ProfilerEvent {
+ uint8 type;
+ uint32 seq;
+ uint32 frame;
+ uint16 depth;
+ uint32 nameId;
+ uint32 movieId;
+};
+
+class LingoProfiler {
+public:
+ LingoProfiler();
+
+ bool isEnabled() const { return _enabled; }
+ void setEnabled(bool enabled) { _enabled = enabled; }
+
+ void onPushContext();
+ void onPopContext();
+ void onFreeze();
+ void onThaw();
+
+ void clear();
+ bool exportChromeTrace(const Common::Path &path);
+
+ const Common::Array<ProfilerEvent> &events() const { return _events; }
+ const Common::String &internedName(uint32 id) const;
+ bool isFull() const { return _full; }
+ uint32 maxEvents() const { return _maxEvents; }
+
+private:
+ uint32 intern(const Common::String &s);
+ void record(uint8 type, uint32 nameId);
+ uint32 currentFrame() const;
+ Common::String currentMovieName() const;
+
+ bool _enabled;
+ bool _full;
+ uint32 _seq;
+ uint32 _maxEvents;
+
+ bool _haveLast;
+ uint32 _lastFrame;
+ uint32 _lastMovieId;
+
+ Common::Array<ProfilerEvent> _events;
+ Common::StringArray _strings;
+ Common::HashMap<Common::String, uint32> _intern;
+};
+
+} // End of namespace Director
+
+#endif
diff --git a/engines/director/module.mk b/engines/director/module.mk
index f6d5a211e2d..39148d79e11 100644
--- a/engines/director/module.mk
+++ b/engines/director/module.mk
@@ -54,6 +54,7 @@ MODULE_OBJS = \
lingo/lingo-object.o \
lingo/lingo-patcher.o \
lingo/lingo-preprocessor.o \
+ lingo/lingo-profiler.o \
lingo/lingo-the.o \
lingo/lingo-utils.o \
lingo/lingodec/ast.o \
@@ -235,6 +236,7 @@ MODULE_OBJS += \
debugger/dt-controlpanel.o \
debugger/dt-help.o \
debugger/dt-lists.o \
+ debugger/dt-profiler.o \
debugger/dt-save-state.o \
debugger/dt-score.o \
debugger/dt-script-d2.o \
diff --git a/engines/director/window.cpp b/engines/director/window.cpp
index 76fc57aa2e2..4939cb02348 100644
--- a/engines/director/window.cpp
+++ b/engines/director/window.cpp
@@ -31,6 +31,7 @@
#include "director/cast.h"
#include "director/debugger.h"
#include "director/lingo/lingo.h"
+#include "director/lingo/lingo-profiler.h"
#include "director/movie.h"
#include "director/window.h"
#include "director/score.h"
@@ -820,6 +821,8 @@ Common::Path Window::getSharedCastPath() {
}
void Window::freezeLingoState() {
+ if (g_director->_lingoProfiler)
+ g_director->_lingoProfiler->onFreeze();
_frozenLingoStates.push_back(_lingoState);
_lingoState = new LingoState;
debugC(3, kDebugLingoExec, "Freezing Lingo state, depth %d", _frozenLingoStates.size());
@@ -834,6 +837,8 @@ void Window::thawLingoState() {
warning("Can't thaw a Lingo state in mid-execution, ignoring");
return;
}
+ if (g_director->_lingoProfiler)
+ g_director->_lingoProfiler->onThaw();
delete _lingoState;
debugC(3, kDebugLingoExec, "Thawing Lingo state, depth %d", _frozenLingoStates.size());
_lingoState = _frozenLingoStates.back();
Commit: a50b853d889faedc6fc11d373c802c9e78e1c0f5
https://github.com/scummvm/scummvm/commit/a50b853d889faedc6fc11d373c802c9e78e1c0f5
Author: ramyak-sharma (ramyaksharma1 at gmail.com)
Date: 2026-08-26T01:15:25+02:00
Commit Message:
DIRECTOR: DT: Improve the Lingo profiler timing, stats and theming
Record a getMillis timestamp on every event and report per-handler total
and self time in ms, replacing the old event-count span. Cache the
interned track id per movie to keep the record hot path cheap. Move the
panel's fixed colors into DebuggerTheme, open the window larger, drop the
Chrome/Perfetto JSON export so the panel is self-contained, and simplify
the panel comments.
Changed paths:
engines/director/debugger/debugtools.cpp
engines/director/debugger/dt-internal.h
engines/director/debugger/dt-profiler.cpp
engines/director/lingo/lingo-profiler.cpp
engines/director/lingo/lingo-profiler.h
diff --git a/engines/director/debugger/debugtools.cpp b/engines/director/debugger/debugtools.cpp
index 8165c008757..3d4264d68b7 100644
--- a/engines/director/debugger/debugtools.cpp
+++ b/engines/director/debugger/debugtools.cpp
@@ -834,7 +834,18 @@ static const DebuggerTheme themes[kThemeCount] = {
ImVec4(1.0f, 0.4f, 0.4f, 1.0f), // logger_error
ImVec4(1.0f, 1.0f, 0.4f, 1.0f), // logger_warning
ImVec4(1.0f, 0.8f, 0.6f, 1.0f), // logger_info
- ImVec4(0.8f, 0.8f, 0.8f, 1.0f) // logger_debug
+ ImVec4(0.8f, 0.8f, 0.8f, 1.0f), // logger_debug
+
+ ImVec4(0.00f, 0.00f, 0.00f, 0.157f), // prof_ruler_bg
+ ImVec4(0.55f, 0.55f, 0.55f, 0.086f), // prof_grid_line
+ ImVec4(0.90f, 0.47f, 0.24f, 1.0f), // prof_freeze
+ ImVec4(0.35f, 0.78f, 0.47f, 1.0f), // prof_thaw
+ ImVec4(0.00f, 0.00f, 0.00f, 0.392f), // prof_zone_border
+ ImVec4(0.08f, 0.08f, 0.08f, 1.0f), // prof_zone_text
+ ImVec4(1.00f, 0.86f, 0.16f, 1.0f), // prof_selected
+ ImVec4(1.00f, 1.00f, 0.71f, 0.784f), // prof_highlight
+ ImVec4(0.90f, 0.16f, 0.16f, 1.0f), // prof_live_edge
+ ImVec4(0.59f, 0.59f, 0.59f, 0.353f) // prof_crosshair
},
// [kThemeLight]
{
@@ -892,7 +903,18 @@ static const DebuggerTheme themes[kThemeCount] = {
ImVec4(0.7f, 0.0f, 0.0f, 1.0f), // logger_error
ImVec4(0.7f, 0.4f, 0.0f, 1.0f), // logger_warning
ImVec4(0.15f, 0.15f, 0.15f, 1.0f), // logger_info
- ImVec4(0.4f, 0.4f, 0.4f, 1.0f) // logger_debug
+ ImVec4(0.4f, 0.4f, 0.4f, 1.0f), // logger_debug
+
+ ImVec4(0.00f, 0.00f, 0.00f, 0.157f), // prof_ruler_bg
+ ImVec4(0.55f, 0.55f, 0.55f, 0.188f), // prof_grid_line
+ ImVec4(0.90f, 0.47f, 0.24f, 1.0f), // prof_freeze
+ ImVec4(0.35f, 0.78f, 0.47f, 1.0f), // prof_thaw
+ ImVec4(0.00f, 0.00f, 0.00f, 0.392f), // prof_zone_border
+ ImVec4(0.08f, 0.08f, 0.08f, 1.0f), // prof_zone_text
+ ImVec4(0.82f, 0.56f, 0.00f, 1.0f), // prof_selected
+ ImVec4(0.78f, 0.51f, 0.00f, 0.784f), // prof_highlight
+ ImVec4(0.90f, 0.16f, 0.16f, 1.0f), // prof_live_edge
+ ImVec4(0.39f, 0.39f, 0.39f, 0.353f) // prof_crosshair
}
};
diff --git a/engines/director/debugger/dt-internal.h b/engines/director/debugger/dt-internal.h
index 8bbbe58acfe..1f3ff3cf788 100644
--- a/engines/director/debugger/dt-internal.h
+++ b/engines/director/debugger/dt-internal.h
@@ -210,6 +210,18 @@ struct DebuggerTheme {
ImVec4 logger_warning;
ImVec4 logger_info;
ImVec4 logger_debug;
+
+ // Profiler
+ ImVec4 prof_ruler_bg;
+ ImVec4 prof_grid_line;
+ ImVec4 prof_freeze;
+ ImVec4 prof_thaw;
+ ImVec4 prof_zone_border;
+ ImVec4 prof_zone_text;
+ ImVec4 prof_selected;
+ ImVec4 prof_highlight;
+ ImVec4 prof_live_edge;
+ ImVec4 prof_crosshair;
};
struct QuickOpenItem {
diff --git a/engines/director/debugger/dt-profiler.cpp b/engines/director/debugger/dt-profiler.cpp
index bd6bec99a79..7f23074521b 100644
--- a/engines/director/debugger/dt-profiler.cpp
+++ b/engines/director/debugger/dt-profiler.cpp
@@ -21,7 +21,6 @@
#include "common/algorithm.h"
#include "common/hashmap.h"
-#include "common/path.h"
#include "director/director.h"
#include "director/archive.h"
@@ -54,9 +53,13 @@ static bool openHandlerScript(const Common::String &handlerName) {
return true;
}
+// A resolved begin/end pair with its nested render depth.
struct ProfZone {
uint32 startSeq;
uint32 endSeq;
+ uint32 startTs;
+ uint32 endTs;
+ uint32 childTs; // ms spent in nested calls, for self-time
uint32 nameId;
uint32 movieId;
uint32 startFrame;
@@ -67,7 +70,8 @@ struct ProfZone {
struct StatRow {
uint32 nameId;
uint32 count;
- uint64 totalSpan;
+ uint64 totalTs; // inclusive ms across all calls
+ uint64 selfTs; // ms excluding nested calls
};
struct DrawnRect {
@@ -76,9 +80,12 @@ struct DrawnRect {
};
static bool statGreater(const StatRow &a, const StatRow &b) {
+ if (a.totalTs != b.totalTs)
+ return a.totalTs > b.totalTs;
return a.count > b.count;
}
+// Deterministic pastel keyed by handler name, so a handler keeps one color.
static ImU32 nameColor(uint32 id) {
uint32 h = (id + 1) * 2654435761u;
int r = 130 + (int)((h >> 1) & 0x4F);
@@ -93,6 +100,7 @@ void showProfiler() {
if (!_state->_w.profiler)
return;
+ ImGui::SetNextWindowSize(ImVec2(1000, 640), ImGuiCond_FirstUseEver);
if (!ImGui::Begin("Profiler", &_state->_w.profiler)) {
ImGui::End();
return;
@@ -105,15 +113,19 @@ void showProfiler() {
return;
}
+ const DebuggerTheme &theme = *_state->theme;
+
static Common::Array<ProfZone> zones;
static uint builtCount = 0xFFFFFFFFu;
static uint16 maxRow = 0;
static uint32 maxSeq = 0;
+ static uint32 maxTs = 0;
static float viewStart = 0.0f;
static float viewSpan = 200.0f;
static bool follow = true;
static int selected = -1;
+ // Eased zoom animation (Fit / double-click a zone).
static bool animActive = false;
static float animT = 0.0f;
static float aFromStart = 0.0f, aToStart = 0.0f, aFromSpan = 0.0f, aToSpan = 0.0f;
@@ -128,20 +140,12 @@ void showProfiler() {
if (ImGui::Button("Clear"))
prof->clear();
- ImGui::SameLine();
- static Common::String status;
- if (ImGui::Button("Export JSON")) {
- Common::Path path("lingo-trace.json");
- status = prof->exportChromeTrace(path) ? Common::String("wrote lingo-trace.json") : Common::String("export failed");
- }
- if (ImGui::IsItemHovered())
- ImGui::SetTooltip("Write a Chrome/Perfetto trace to lingo-trace.json.");
-
ImGui::SameLine();
ImGui::Checkbox("Follow live", &follow);
if (ImGui::IsItemHovered())
ImGui::SetTooltip("Keep the newest event (red line) at the right edge.\nAny zoom/pan turns this off.");
+ // Rebuild the zone cache when the trace changed.
const Common::Array<ProfilerEvent> &events = prof->events();
if (builtCount != events.size()) {
builtCount = events.size();
@@ -159,6 +163,9 @@ void showProfiler() {
ProfZone z;
z.startSeq = e.seq;
z.endSeq = e.seq;
+ z.startTs = e.ts;
+ z.endTs = e.ts;
+ z.childTs = 0;
z.nameId = e.nameId;
z.movieId = e.movieId;
z.startFrame = e.frame;
@@ -175,7 +182,11 @@ void showProfiler() {
uint idx = openStack.back();
openStack.pop_back();
zones[idx].endSeq = e.seq;
+ zones[idx].endTs = e.ts;
zones[idx].endFrame = e.frame;
+ // Credit inclusive time to the parent for its self-time.
+ if (!openStack.empty())
+ zones[openStack.back()].childTs += zones[idx].endTs - zones[idx].startTs;
}
break;
case kProfFreeze:
@@ -194,8 +205,11 @@ void showProfiler() {
}
maxSeq = events.empty() ? 0 : events.back().seq;
- for (uint i = 0; i < openStack.size(); i++)
+ maxTs = events.empty() ? 0 : events.back().ts;
+ for (uint i = 0; i < openStack.size(); i++) {
zones[openStack[i]].endSeq = maxSeq + 1;
+ zones[openStack[i]].endTs = maxTs;
+ }
if (selected >= (int)zones.size())
selected = -1;
@@ -228,40 +242,46 @@ void showProfiler() {
if (ImGui::IsItemHovered())
ImGui::SetTooltip("Open this handler's script in the Scripts window.");
ImGui::SameLine();
- ImGui::Text("selected: %s frames %u-%u span %u depth %u movie %s",
+ uint32 total = z.endTs > z.startTs ? z.endTs - z.startTs : 0;
+ uint32 self = total > z.childTs ? total - z.childTs : 0;
+ ImGui::Text("selected: %s frames %u-%u time %ums (self %ums) depth %u movie %s",
prof->internedName(z.nameId).c_str(), z.startFrame, z.endFrame,
- z.endSeq - z.startSeq, z.depth, prof->internedName(z.movieId).c_str());
+ total, self, z.depth, prof->internedName(z.movieId).c_str());
} else {
ImGui::TextUnformatted("selected: (none) -- click a call to select, double-click to zoom, Open script for its code");
}
+ // Per-handler aggregate stats.
if (ImGui::CollapsingHeader("Statistics")) {
Common::HashMap<uint32, uint> idxByName;
Common::Array<StatRow> rows;
for (uint i = 0; i < zones.size(); i++) {
const ProfZone &z = zones[i];
- uint32 span = z.endSeq > z.startSeq ? z.endSeq - z.startSeq : 0;
+ uint32 total = z.endTs > z.startTs ? z.endTs - z.startTs : 0;
+ uint32 self = total > z.childTs ? total - z.childTs : 0;
Common::HashMap<uint32, uint>::iterator it = idxByName.find(z.nameId);
if (it == idxByName.end()) {
StatRow r;
r.nameId = z.nameId;
r.count = 1;
- r.totalSpan = span;
+ r.totalTs = total;
+ r.selfTs = self;
idxByName[z.nameId] = rows.size();
rows.push_back(r);
} else {
rows[it->_value].count++;
- rows[it->_value].totalSpan += span;
+ rows[it->_value].totalTs += total;
+ rows[it->_value].selfTs += self;
}
}
Common::sort(rows.begin(), rows.end(), statGreater);
ImGui::BeginChild("##stats", ImVec2(0, 160.0f), ImGuiChildFlags_Borders);
- ImGui::Text("%8s %10s %s", "calls", "total(seq)", "handler (click to open)");
+ ImGui::Text("%8s %10s %10s %s", "calls", "total(ms)", "self(ms)", "handler (click to open)");
uint shown = rows.size() < 50 ? rows.size() : 50;
for (uint i = 0; i < shown; i++) {
- Common::String row = Common::String::format("%8u %10.0f %s", rows[i].count,
- (double)rows[i].totalSpan, prof->internedName(rows[i].nameId).c_str());
+ Common::String row = Common::String::format("%8u %10.0f %10.0f %s", rows[i].count,
+ (double)rows[i].totalTs, (double)rows[i].selfTs, prof->internedName(rows[i].nameId).c_str());
ImGui::PushID((int)i);
if (ImGui::Selectable(row.c_str()))
openHandlerScript(prof->internedName(rows[i].nameId));
@@ -304,6 +324,7 @@ void showProfiler() {
float pxPerUnit = viewW / viewSpan;
+ // Wheel zooms around the cursor, drag pans (both cancel follow/anim).
if (itemHovered) {
float wheel = ImGui::GetIO().MouseWheel;
if (wheel != 0.0f) {
@@ -335,7 +356,17 @@ void showProfiler() {
ImDrawList *dl = ImGui::GetWindowDrawList();
const float baseY = origin.y + rulerH;
- const ImU32 frameLineCol = ImGui::GetColorU32(_state->theme->line_color);
+ const ImU32 frameLineCol = ImGui::GetColorU32(theme.line_color);
+ const ImU32 rulerBgCol = ImGui::GetColorU32(theme.prof_ruler_bg);
+ const ImU32 gridLineCol = ImGui::GetColorU32(theme.prof_grid_line);
+ const ImU32 freezeCol = ImGui::GetColorU32(theme.prof_freeze);
+ const ImU32 thawCol = ImGui::GetColorU32(theme.prof_thaw);
+ const ImU32 zoneBorderCol = ImGui::GetColorU32(theme.prof_zone_border);
+ const ImU32 zoneTextCol = ImGui::GetColorU32(theme.prof_zone_text);
+ const ImU32 selectedCol = ImGui::GetColorU32(theme.prof_selected);
+ const ImU32 highlightCol = ImGui::GetColorU32(theme.prof_highlight);
+ const ImU32 liveEdgeCol = ImGui::GetColorU32(theme.prof_live_edge);
+ const ImU32 crosshairCol = ImGui::GetColorU32(theme.prof_crosshair);
const float visStartSeq = viewStart - 2.0f;
const float visEndSeq = viewStart + viewSpan + 2.0f;
@@ -348,7 +379,8 @@ void showProfiler() {
for (uint i = 0; i < rowRight.size(); i++)
rowRight[i] = -1.0e9f;
- dl->AddRectFilled(ImVec2(origin.x, origin.y), ImVec2(origin.x + viewW, origin.y + rulerH), IM_COL32(0, 0, 0, 40));
+ // Frames and freeze/thaw live in a thin ruler strip at the top.
+ dl->AddRectFilled(ImVec2(origin.x, origin.y), ImVec2(origin.x + viewW, origin.y + rulerH), rulerBgCol);
dl->AddLine(ImVec2(origin.x, origin.y + rulerH), ImVec2(origin.x + viewW, origin.y + rulerH), frameLineCol, 1.0f);
float lastFrameLabelX = -1.0e9f;
@@ -366,24 +398,25 @@ void showProfiler() {
continue;
lastFrameLabelX = x;
dl->AddLine(ImVec2(x, origin.y + rulerH - 6.0f), ImVec2(x, origin.y + rulerH), frameLineCol, 1.0f);
- dl->AddLine(ImVec2(x, origin.y + rulerH), ImVec2(x, origin.y + canvasH), IM_COL32(140, 140, 140, 22), 1.0f);
+ dl->AddLine(ImVec2(x, origin.y + rulerH), ImVec2(x, origin.y + canvasH), gridLineCol, 1.0f);
Common::String lbl = Common::String::format("f%u", e.frame);
- dl->AddText(ImVec2(x + 2.0f, origin.y + 3.0f), _state->theme->gridTextColor, lbl.c_str());
+ dl->AddText(ImVec2(x + 2.0f, origin.y + 3.0f), theme.gridTextColor, lbl.c_str());
} else if (e.type == kProfFreeze) {
if (x - lastFreezeX < 4.0f)
continue;
lastFreezeX = x;
dl->AddTriangleFilled(ImVec2(x - 3.0f, origin.y + 2.0f), ImVec2(x + 3.0f, origin.y + 2.0f),
- ImVec2(x, origin.y + 8.0f), IM_COL32(230, 120, 60, 255));
- } else {
+ ImVec2(x, origin.y + 8.0f), freezeCol);
+ } else { // kProfThaw
if (x - lastThawX < 4.0f)
continue;
lastThawX = x;
dl->AddTriangleFilled(ImVec2(x - 3.0f, origin.y + rulerH - 2.0f), ImVec2(x + 3.0f, origin.y + rulerH - 2.0f),
- ImVec2(x, origin.y + rulerH - 8.0f), IM_COL32(90, 200, 120, 255));
+ ImVec2(x, origin.y + rulerH - 8.0f), thawCol);
}
}
+ // Zones. Cull to the visible window and coalesce sub-pixel slices.
Common::Array<DrawnRect> drawnRects;
for (uint i = 0; i < zones.size(); i++) {
const ProfZone &z = zones[i];
@@ -409,12 +442,12 @@ void showProfiler() {
dl->AddRectFilled(ImVec2(x0, y0), ImVec2(x1, y1), nameColor(z.nameId));
if (x1 - x0 > 3.0f)
- dl->AddRect(ImVec2(x0, y0), ImVec2(x1, y1), IM_COL32(0, 0, 0, 100));
+ dl->AddRect(ImVec2(x0, y0), ImVec2(x1, y1), zoneBorderCol);
if (x1 - x0 > 24.0f) {
const Common::String &nm = prof->internedName(z.nameId);
dl->PushClipRect(ImVec2(x0 + 2.0f, y0), ImVec2(x1 - 2.0f, y1), true);
- dl->AddText(ImVec2(x0 + 3.0f, y0 + 2.0f), IM_COL32(20, 20, 20, 255), nm.c_str());
+ dl->AddText(ImVec2(x0 + 3.0f, y0 + 2.0f), zoneTextCol, nm.c_str());
dl->PopClipRect();
}
@@ -426,29 +459,31 @@ void showProfiler() {
hoveredZone = (int)i;
if ((int)i == selected)
- dl->AddRect(ImVec2(x0 - 1.0f, y0 - 1.0f), ImVec2(x1 + 1.0f, y1 + 1.0f), IM_COL32(255, 220, 40, 255), 0.0f, 0, 2.0f);
+ dl->AddRect(ImVec2(x0 - 1.0f, y0 - 1.0f), ImVec2(x1 + 1.0f, y1 + 1.0f), selectedCol, 0.0f, 0, 2.0f);
}
+ // Highlight every visible call sharing the hovered call's name.
if (hoveredZone >= 0 && (uint)hoveredZone < zones.size()) {
uint32 hn = zones[hoveredZone].nameId;
for (uint i = 0; i < drawnRects.size(); i++) {
const DrawnRect &dr = drawnRects[i];
if (dr.nameId == hn)
- dl->AddRect(ImVec2(dr.x0, dr.y0), ImVec2(dr.x1, dr.y1), IM_COL32(255, 255, 180, 200), 0.0f, 0, 1.5f);
+ dl->AddRect(ImVec2(dr.x0, dr.y0), ImVec2(dr.x1, dr.y1), highlightCol, 0.0f, 0, 1.5f);
}
}
+ // Live edge: the newest recorded event ("now").
{
const float px = origin.x + ((float)maxSeq - viewStart) * pxPerUnit;
if (px >= origin.x && px <= origin.x + viewW)
- dl->AddLine(ImVec2(px, origin.y), ImVec2(px, origin.y + canvasH), IM_COL32(230, 40, 40, 255), 2.0f);
+ dl->AddLine(ImVec2(px, origin.y), ImVec2(px, origin.y + canvasH), liveEdgeCol, 2.0f);
}
if (itemHovered)
- dl->AddLine(ImVec2(mouse.x, origin.y), ImVec2(mouse.x, origin.y + canvasH), IM_COL32(150, 150, 150, 90), 1.0f);
+ dl->AddLine(ImVec2(mouse.x, origin.y), ImVec2(mouse.x, origin.y + canvasH), crosshairCol, 1.0f);
if (zones.empty())
- dl->AddText(ImVec2(origin.x + 12.0f, origin.y + 12.0f), _state->theme->gridTextColor,
+ dl->AddText(ImVec2(origin.x + 12.0f, origin.y + 12.0f), theme.gridTextColor,
"Tick Capture, then interact with the game to record Lingo execution.");
if (dblClicked && hoveredZone >= 0 && (uint)hoveredZone < zones.size()) {
@@ -480,7 +515,9 @@ void showProfiler() {
ImGui::Text("frame %u", z.startFrame);
else
ImGui::Text("frames %u - %u", z.startFrame, z.endFrame);
- ImGui::Text("span: %u depth: %u", z.endSeq - z.startSeq, z.depth);
+ uint32 total = z.endTs > z.startTs ? z.endTs - z.startTs : 0;
+ uint32 self = total > z.childTs ? total - z.childTs : 0;
+ ImGui::Text("time: %ums (self %ums) depth: %u", total, self, z.depth);
ImGui::EndTooltip();
}
diff --git a/engines/director/lingo/lingo-profiler.cpp b/engines/director/lingo/lingo-profiler.cpp
index 130442a065d..523581f220c 100644
--- a/engines/director/lingo/lingo-profiler.cpp
+++ b/engines/director/lingo/lingo-profiler.cpp
@@ -19,8 +19,7 @@
*
*/
-#include "common/file.h"
-#include "common/path.h"
+#include "common/system.h"
#include "director/director.h"
#include "director/movie.h"
@@ -39,6 +38,8 @@ LingoProfiler::LingoProfiler() {
_haveLast = false;
_lastFrame = 0;
_lastMovieId = 0;
+ _lastMoviePtr = nullptr;
+ _curMovieId = 0;
_strings.push_back(Common::String());
_intern[Common::String()] = 0;
@@ -61,31 +62,6 @@ const Common::String &LingoProfiler::internedName(uint32 id) const {
return _strings[id];
}
-uint32 LingoProfiler::currentFrame() const {
- if (!g_director)
- return 0;
- Window *window = g_director->getCurrentWindow();
- if (!window)
- return 0;
- Movie *movie = window->getCurrentMovie();
- if (!movie || !movie->getScore())
- return 0;
- return movie->getScore()->getCurrentFrameNum();
-}
-
-Common::String LingoProfiler::currentMovieName() const {
- if (!g_director)
- return Common::String("?");
- Window *window = g_director->getCurrentWindow();
- if (!window)
- return Common::String("?");
- Movie *movie = window->getCurrentMovie();
- if (!movie)
- return Common::String("?");
- Common::String name = movie->getMacName();
- return name.empty() ? Common::String("movie") : name;
-}
-
void LingoProfiler::record(uint8 type, uint32 nameId) {
if (_full)
return;
@@ -94,13 +70,30 @@ void LingoProfiler::record(uint8 type, uint32 nameId) {
return;
}
- uint32 frame = currentFrame();
- uint32 movieId = intern(currentMovieName());
+ // Walk to the current movie once and derive frame, movie and timestamp
+ // from it, so the hot path avoids repeated pointer chases.
+ Window *window = g_director ? g_director->getCurrentWindow() : nullptr;
+ Movie *movie = window ? window->getCurrentMovie() : nullptr;
+ uint32 frame = (movie && movie->getScore()) ? movie->getScore()->getCurrentFrameNum() : 0;
+
+ // The interned movie id only changes when the movie does; recompute (which
+ // copies the name and hits the hashmap) just on a movie switch.
+ if ((const void *)movie != _lastMoviePtr) {
+ _lastMoviePtr = movie;
+ Common::String name = movie ? movie->getMacName() : Common::String();
+ if (name.empty())
+ name = movie ? "movie" : "?";
+ _curMovieId = intern(name);
+ }
+ uint32 movieId = _curMovieId;
+
+ uint32 ts = g_system->getMillis();
if (!_haveLast || frame != _lastFrame || movieId != _lastMovieId) {
ProfilerEvent fe;
fe.type = kProfFrame;
fe.seq = _seq++;
+ fe.ts = ts;
fe.frame = frame;
fe.depth = 0;
fe.nameId = 0;
@@ -119,6 +112,7 @@ void LingoProfiler::record(uint8 type, uint32 nameId) {
ProfilerEvent e;
e.type = type;
e.seq = _seq++;
+ e.ts = ts;
e.frame = frame;
e.depth = depth;
e.nameId = nameId;
@@ -168,79 +162,8 @@ void LingoProfiler::clear() {
_haveLast = false;
_lastFrame = 0;
_lastMovieId = 0;
-}
-
-static Common::String jsonEscape(const Common::String &s) {
- Common::String out;
- for (uint i = 0; i < s.size(); i++) {
- char c = s[i];
- if (c == '"' || c == '\\') {
- out += '\\';
- out += c;
- } else if (c == '\n') {
- out += "\\n";
- } else if (c == '\t') {
- out += "\\t";
- } else if ((byte)c < 0x20) {
- } else {
- out += c;
- }
- }
- return out;
-}
-
-bool LingoProfiler::exportChromeTrace(const Common::Path &path) {
- Common::DumpFile out;
- if (!out.open(path))
- return false;
-
- out.writeString("{\"traceEvents\":[\n");
-
- for (uint32 id = 1; id < _strings.size(); id++) {
- Common::String line = Common::String::format(
- "{\"name\":\"thread_name\",\"ph\":\"M\",\"pid\":1,\"tid\":%u,\"args\":{\"name\":\"%s\"}},\n",
- id, jsonEscape(_strings[id]).c_str());
- out.writeString(line);
- }
-
- for (uint i = 0; i < _events.size(); i++) {
- const ProfilerEvent &e = _events[i];
- Common::String line;
- switch (e.type) {
- case kProfBegin:
- line = Common::String::format(
- "{\"name\":\"%s\",\"ph\":\"B\",\"ts\":%u,\"pid\":1,\"tid\":%u},\n",
- jsonEscape(internedName(e.nameId)).c_str(), e.seq, e.movieId);
- break;
- case kProfEnd:
- line = Common::String::format(
- "{\"ph\":\"E\",\"ts\":%u,\"pid\":1,\"tid\":%u},\n", e.seq, e.movieId);
- break;
- case kProfFrame:
- line = Common::String::format(
- "{\"name\":\"frame %u\",\"ph\":\"i\",\"ts\":%u,\"pid\":1,\"tid\":%u,\"s\":\"g\"},\n",
- e.frame, e.seq, e.movieId);
- break;
- case kProfFreeze:
- line = Common::String::format(
- "{\"name\":\"freeze\",\"ph\":\"i\",\"ts\":%u,\"pid\":1,\"tid\":%u,\"s\":\"t\"},\n",
- e.seq, e.movieId);
- break;
- case kProfThaw:
- line = Common::String::format(
- "{\"name\":\"thaw\",\"ph\":\"i\",\"ts\":%u,\"pid\":1,\"tid\":%u,\"s\":\"t\"},\n",
- e.seq, e.movieId);
- break;
- default:
- break;
- }
- out.writeString(line);
- }
-
- out.writeString("{\"name\":\"process_name\",\"ph\":\"M\",\"pid\":1,\"args\":{\"name\":\"Lingo\"}}\n");
- out.writeString("]}\n");
- out.close();
- return true;
+ _lastMoviePtr = nullptr;
+ _curMovieId = 0;
}
} // End of namespace Director
diff --git a/engines/director/lingo/lingo-profiler.h b/engines/director/lingo/lingo-profiler.h
index c21e9764407..69a4636a191 100644
--- a/engines/director/lingo/lingo-profiler.h
+++ b/engines/director/lingo/lingo-profiler.h
@@ -28,10 +28,6 @@
#include "common/str.h"
#include "common/str-array.h"
-namespace Common {
-class Path;
-}
-
namespace Director {
enum ProfilerEventType {
@@ -43,12 +39,13 @@ enum ProfilerEventType {
};
struct ProfilerEvent {
- uint8 type;
- uint32 seq;
- uint32 frame;
- uint16 depth;
- uint32 nameId;
- uint32 movieId;
+ uint8 type; // ProfilerEventType
+ uint32 seq; // monotonic event order, used for stable ordering
+ uint32 ts; // wall-clock milliseconds at emit time (getMillis)
+ uint32 frame; // score frame number at emit time
+ uint16 depth; // Lingo callstack depth at emit time
+ uint32 nameId; // interned handler name (kProfBegin), else 0
+ uint32 movieId; // interned movie name (owning window's movie)
};
class LingoProfiler {
@@ -64,7 +61,6 @@ public:
void onThaw();
void clear();
- bool exportChromeTrace(const Common::Path &path);
const Common::Array<ProfilerEvent> &events() const { return _events; }
const Common::String &internedName(uint32 id) const;
@@ -74,8 +70,6 @@ public:
private:
uint32 intern(const Common::String &s);
void record(uint8 type, uint32 nameId);
- uint32 currentFrame() const;
- Common::String currentMovieName() const;
bool _enabled;
bool _full;
@@ -86,6 +80,11 @@ private:
uint32 _lastFrame;
uint32 _lastMovieId;
+ // Cache the interned movie id per movie, so the hot path skips the
+ // name copy + hashmap lookup on every event.
+ const void *_lastMoviePtr;
+ uint32 _curMovieId;
+
Common::Array<ProfilerEvent> _events;
Common::StringArray _strings;
Common::HashMap<Common::String, uint32> _intern;
Commit: 820051314a7d9650fb5e1b94bb418871fd5cc0d3
https://github.com/scummvm/scummvm/commit/820051314a7d9650fb5e1b94bb418871fd5cc0d3
Author: ramyak-sharma (ramyaksharma1 at gmail.com)
Date: 2026-08-26T01:15:25+02:00
Commit Message:
DIRECTOR: DT: Split the profiler panel into helpers
Pull the zone-model build and the statistics table out of showProfiler
into rebuildZones() and drawStatistics(), matching the drawX() helpers in
dt-score.cpp, so each can be changed on its own.
Changed paths:
engines/director/debugger/dt-profiler.cpp
diff --git a/engines/director/debugger/dt-profiler.cpp b/engines/director/debugger/dt-profiler.cpp
index 7f23074521b..9a661c87215 100644
--- a/engines/director/debugger/dt-profiler.cpp
+++ b/engines/director/debugger/dt-profiler.cpp
@@ -96,6 +96,114 @@ static ImU32 nameColor(uint32 id) {
static inline float fabsff(float f) { return f < 0.0f ? -f : f; }
+// Resolve the raw event stream into completed begin/end zones with nesting.
+static void rebuildZones(const Common::Array<ProfilerEvent> &events, Common::Array<ProfZone> &zones,
+ uint16 &maxRow, uint32 &maxSeq, uint32 &maxTs) {
+ zones.clear();
+ maxRow = 0;
+
+ Common::Array<uint> openStack;
+ Common::Array<uint16> offStack;
+ uint16 depthOff = 0;
+
+ for (uint i = 0; i < events.size(); i++) {
+ const ProfilerEvent &e = events[i];
+ switch (e.type) {
+ case kProfBegin: {
+ ProfZone z;
+ z.startSeq = e.seq;
+ z.endSeq = e.seq;
+ z.startTs = e.ts;
+ z.endTs = e.ts;
+ z.childTs = 0;
+ z.nameId = e.nameId;
+ z.movieId = e.movieId;
+ z.startFrame = e.frame;
+ z.endFrame = e.frame;
+ z.depth = e.depth + depthOff;
+ if (z.depth > maxRow)
+ maxRow = z.depth;
+ zones.push_back(z);
+ openStack.push_back(zones.size() - 1);
+ break;
+ }
+ case kProfEnd:
+ if (!openStack.empty()) {
+ uint idx = openStack.back();
+ openStack.pop_back();
+ zones[idx].endSeq = e.seq;
+ zones[idx].endTs = e.ts;
+ zones[idx].endFrame = e.frame;
+ // Credit inclusive time to the parent for its self-time.
+ if (!openStack.empty())
+ zones[openStack.back()].childTs += zones[idx].endTs - zones[idx].startTs;
+ }
+ break;
+ case kProfFreeze:
+ offStack.push_back(depthOff);
+ depthOff += e.depth;
+ break;
+ case kProfThaw:
+ if (!offStack.empty()) {
+ depthOff = offStack.back();
+ offStack.pop_back();
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ maxSeq = events.empty() ? 0 : events.back().seq;
+ maxTs = events.empty() ? 0 : events.back().ts;
+ for (uint i = 0; i < openStack.size(); i++) {
+ zones[openStack[i]].endSeq = maxSeq + 1;
+ zones[openStack[i]].endTs = maxTs;
+ }
+}
+
+// Per-handler aggregate table (inclusive and self time), most costly first.
+static void drawStatistics(LingoProfiler *prof, const Common::Array<ProfZone> &zones) {
+ if (!ImGui::CollapsingHeader("Statistics"))
+ return;
+
+ Common::HashMap<uint32, uint> idxByName;
+ Common::Array<StatRow> rows;
+ for (uint i = 0; i < zones.size(); i++) {
+ const ProfZone &z = zones[i];
+ uint32 total = z.endTs > z.startTs ? z.endTs - z.startTs : 0;
+ uint32 self = total > z.childTs ? total - z.childTs : 0;
+ Common::HashMap<uint32, uint>::iterator it = idxByName.find(z.nameId);
+ if (it == idxByName.end()) {
+ StatRow r;
+ r.nameId = z.nameId;
+ r.count = 1;
+ r.totalTs = total;
+ r.selfTs = self;
+ idxByName[z.nameId] = rows.size();
+ rows.push_back(r);
+ } else {
+ rows[it->_value].count++;
+ rows[it->_value].totalTs += total;
+ rows[it->_value].selfTs += self;
+ }
+ }
+ Common::sort(rows.begin(), rows.end(), statGreater);
+
+ ImGui::BeginChild("##stats", ImVec2(0, 160.0f), ImGuiChildFlags_Borders);
+ ImGui::Text("%8s %10s %10s %s", "calls", "total(ms)", "self(ms)", "handler (click to open)");
+ uint shown = rows.size() < 50 ? rows.size() : 50;
+ for (uint i = 0; i < shown; i++) {
+ Common::String row = Common::String::format("%8u %10.0f %10.0f %s", rows[i].count,
+ (double)rows[i].totalTs, (double)rows[i].selfTs, prof->internedName(rows[i].nameId).c_str());
+ ImGui::PushID((int)i);
+ if (ImGui::Selectable(row.c_str()))
+ openHandlerScript(prof->internedName(rows[i].nameId));
+ ImGui::PopID();
+ }
+ ImGui::EndChild();
+}
+
void showProfiler() {
if (!_state->_w.profiler)
return;
@@ -145,75 +253,14 @@ void showProfiler() {
if (ImGui::IsItemHovered())
ImGui::SetTooltip("Keep the newest event (red line) at the right edge.\nAny zoom/pan turns this off.");
- // Rebuild the zone cache when the trace changed.
+ // Rebuild the zone cache when the trace changed. Zones keep their indices
+ // across rebuilds (events only ever append), so the selection survives.
const Common::Array<ProfilerEvent> &events = prof->events();
if (builtCount != events.size()) {
builtCount = events.size();
- zones.clear();
- maxRow = 0;
-
- Common::Array<uint> openStack;
- Common::Array<uint16> offStack;
- uint16 depthOff = 0;
-
- for (uint i = 0; i < events.size(); i++) {
- const ProfilerEvent &e = events[i];
- switch (e.type) {
- case kProfBegin: {
- ProfZone z;
- z.startSeq = e.seq;
- z.endSeq = e.seq;
- z.startTs = e.ts;
- z.endTs = e.ts;
- z.childTs = 0;
- z.nameId = e.nameId;
- z.movieId = e.movieId;
- z.startFrame = e.frame;
- z.endFrame = e.frame;
- z.depth = e.depth + depthOff;
- if (z.depth > maxRow)
- maxRow = z.depth;
- zones.push_back(z);
- openStack.push_back(zones.size() - 1);
- break;
- }
- case kProfEnd:
- if (!openStack.empty()) {
- uint idx = openStack.back();
- openStack.pop_back();
- zones[idx].endSeq = e.seq;
- zones[idx].endTs = e.ts;
- zones[idx].endFrame = e.frame;
- // Credit inclusive time to the parent for its self-time.
- if (!openStack.empty())
- zones[openStack.back()].childTs += zones[idx].endTs - zones[idx].startTs;
- }
- break;
- case kProfFreeze:
- offStack.push_back(depthOff);
- depthOff += e.depth;
- break;
- case kProfThaw:
- if (!offStack.empty()) {
- depthOff = offStack.back();
- offStack.pop_back();
- }
- break;
- default:
- break;
- }
- }
-
- maxSeq = events.empty() ? 0 : events.back().seq;
- maxTs = events.empty() ? 0 : events.back().ts;
- for (uint i = 0; i < openStack.size(); i++) {
- zones[openStack[i]].endSeq = maxSeq + 1;
- zones[openStack[i]].endTs = maxTs;
- }
-
+ rebuildZones(events, zones, maxRow, maxSeq, maxTs);
if (selected >= (int)zones.size())
selected = -1;
-
if (viewSpan < 4.0f)
viewSpan = 200.0f;
}
@@ -251,44 +298,7 @@ void showProfiler() {
ImGui::TextUnformatted("selected: (none) -- click a call to select, double-click to zoom, Open script for its code");
}
- // Per-handler aggregate stats.
- if (ImGui::CollapsingHeader("Statistics")) {
- Common::HashMap<uint32, uint> idxByName;
- Common::Array<StatRow> rows;
- for (uint i = 0; i < zones.size(); i++) {
- const ProfZone &z = zones[i];
- uint32 total = z.endTs > z.startTs ? z.endTs - z.startTs : 0;
- uint32 self = total > z.childTs ? total - z.childTs : 0;
- Common::HashMap<uint32, uint>::iterator it = idxByName.find(z.nameId);
- if (it == idxByName.end()) {
- StatRow r;
- r.nameId = z.nameId;
- r.count = 1;
- r.totalTs = total;
- r.selfTs = self;
- idxByName[z.nameId] = rows.size();
- rows.push_back(r);
- } else {
- rows[it->_value].count++;
- rows[it->_value].totalTs += total;
- rows[it->_value].selfTs += self;
- }
- }
- Common::sort(rows.begin(), rows.end(), statGreater);
-
- ImGui::BeginChild("##stats", ImVec2(0, 160.0f), ImGuiChildFlags_Borders);
- ImGui::Text("%8s %10s %10s %s", "calls", "total(ms)", "self(ms)", "handler (click to open)");
- uint shown = rows.size() < 50 ? rows.size() : 50;
- for (uint i = 0; i < shown; i++) {
- Common::String row = Common::String::format("%8u %10.0f %10.0f %s", rows[i].count,
- (double)rows[i].totalTs, (double)rows[i].selfTs, prof->internedName(rows[i].nameId).c_str());
- ImGui::PushID((int)i);
- if (ImGui::Selectable(row.c_str()))
- openHandlerScript(prof->internedName(rows[i].nameId));
- ImGui::PopID();
- }
- ImGui::EndChild();
- }
+ drawStatistics(prof, zones);
if (animActive) {
animT += ImGui::GetIO().DeltaTime * 4.0f;
More information about the Scummvm-git-logs
mailing list