[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