diff --git a/css/debugging/profileEvents.css b/css/debugging/profileEvents.css
new file mode 100644
index 0000000000000000000000000000000000000000..eb3d430426918cd4ba64fe71ac8744ae92a86f79
--- /dev/null
+++ b/css/debugging/profileEvents.css
@@ -0,0 +1,5 @@
+.profile-events {
+    display: grid;
+    grid-template-columns: max-content max-content max-content auto;
+    grid-column-gap: 1em;
+}
diff --git a/src/debugging/profileEvents.js b/src/debugging/profileEvents.js
new file mode 100644
index 0000000000000000000000000000000000000000..cce2b14802b8481f16ea0dc76897df89dd3b4f6c
--- /dev/null
+++ b/src/debugging/profileEvents.js
@@ -0,0 +1,60 @@
+globalThis.profileEvents = (function() {
+	let passageinit = 0;
+	let passagestart = 0;
+	let passagerender = 0;
+	let passagedisplay = 0;
+	let passageend = 0;
+
+	/**
+	 * @param {HTMLElement} container
+	 */
+	function render(container) {
+		App.UI.DOM.appendNewElement("h2", container, "Passage Events Profiler");
+		const p = document.createElement("p");
+		p.classList.add("profile-events");
+		row(p, ":passageinit", ":passagestart", "Copy State", passagestart - passageinit);
+		row(p, ":passagestart", ":passagerender", "Render", passagerender - passagestart);
+		row(p, ":passagerender", ":passagedisplay", "Display", passagedisplay - passagerender);
+		row(p, ":passagedisplay", ":passageend", "Cleanup / Auto Save", passageend - passagedisplay);
+		container.append(p);
+	}
+
+	function row(container, start, stop, desc, value) {
+		App.UI.DOM.appendNewElement("div", container, `From ${start}`);
+		App.UI.DOM.appendNewElement("div", container, `To ${stop}`);
+		App.UI.DOM.appendNewElement("div", container, `(${desc})`);
+		App.UI.DOM.appendNewElement("div", container, `${value}ms`);
+	}
+
+	return {
+		passageinit: () => {
+			if (V.profiler) {
+				passageinit = performance.now();
+			}
+		},
+		passagestart: () => {
+			if (V.profiler) {
+				passagestart = performance.now();
+			}
+		},
+		passagerender: () => {
+			if (V.profiler) {
+				passagerender = performance.now();
+			}
+		},
+		passagedisplay: () => {
+			if (V.profiler) {
+				passagedisplay = performance.now();
+			}
+		},
+		/**
+		 * @param {HTMLElement} content
+		 */
+		passageend: (content) => {
+			if (V.profiler) {
+				passageend = performance.now();
+				render(content);
+			}
+		},
+	};
+})();
diff --git a/src/zz1-last/setupEventHandlers.js b/src/zz1-last/setupEventHandlers.js
index bbe69053495bd2b344be8b4e7d5aae5a2de385a0..8ad72f2310c94cbd84210b3e47fc61fb59283a27 100644
--- a/src/zz1-last/setupEventHandlers.js
+++ b/src/zz1-last/setupEventHandlers.js
@@ -8,19 +8,33 @@ $(document).on(":storyready", () => {
 	App.EventHandlers.storyReady();
 });
 
+$(document).on(":passageinit", () => {
+	if (V.passageSwitchHandler) {
+		V.passageSwitchHandler();
+		delete V.passageSwitchHandler;
+	}
+	profileEvents.passageinit();
+});
+
 $(document).on(":passagestart", event => {
 	App.Debug.slavesConsistency(event);
 	Object.defineProperty(State.temporary, "S", {
 		get: () => S,
 		enumerable: true
 	});
+	profileEvents.passagestart();
 });
 
-$(document).on(":passageinit", () => {
-	if (V.passageSwitchHandler) {
-		V.passageSwitchHandler();
-		delete V.passageSwitchHandler;
-	}
+$(document).on(":passagerender", () => {
+	profileEvents.passagerender();
+});
+
+$(document).on(":passagedisplay", () => {
+	profileEvents.passagedisplay();
+});
+
+$(document).on(":passageend", ev => {
+	profileEvents.passageend(ev.content);
 });
 
 /* ### One-time listeners ### */