From 6bfc81978c48848a89818feb88b1c3774ad4ab11 Mon Sep 17 00:00:00 2001
From: David Taylor <david@taylorhq.com>
Date: Thu, 2 May 2024 23:07:36 +0100
Subject: [PATCH] DEV: Improve built-in browser performance marks/measurements
 (#26758)

- Rename `discourse-booted` to 'discourse-init' (because 'booted' makes it sound like boot was finished. When in fact, it was just starting)

- Introduce `discourse-paint`, which is fired after the Ember application has been painted to the screen by the browser. This happens slightly after DOMContentLoaded

- Add a `performance.measure` call to link those two marks, so they're easily visible in performance traces

Also removes an ember boot-order workaround which is no longer required.
---
 .../discourse/app/controllers/application.js  | 22 +++++++++++++++++++
 .../discourse/app/lib/after-frame-paint.js    | 18 +++++++++++++++
 .../discourse/app/templates/application.hbs   |  2 +-
 .../public/assets/scripts/discourse-boot.js   |  9 +-------
 .../public/assets/scripts/start-app.js        |  4 ++--
 .../discourse/tests/test-boot-ember-cli.js    |  2 +-
 6 files changed, 45 insertions(+), 12 deletions(-)
 create mode 100644 app/assets/javascripts/discourse/app/lib/after-frame-paint.js

diff --git a/app/assets/javascripts/discourse/app/controllers/application.js b/app/assets/javascripts/discourse/app/controllers/application.js
index 2a3807c257e..88f60f2e295 100644
--- a/app/assets/javascripts/discourse/app/controllers/application.js
+++ b/app/assets/javascripts/discourse/app/controllers/application.js
@@ -1,6 +1,8 @@
 import Controller from "@ember/controller";
 import { action } from "@ember/object";
 import { service } from "@ember/service";
+import runAfterFramePaint from "discourse/lib/after-frame-paint";
+import { isTesting } from "discourse-common/config/environment";
 import discourseDebounce from "discourse-common/lib/debounce";
 import deprecated from "discourse-common/lib/deprecated";
 import discourseComputed from "discourse-common/utils/decorators";
@@ -123,4 +125,24 @@ export default Controller.extend({
       }
     }
   },
+
+  @action
+  trackDiscoursePainted() {
+    if (isTesting()) {
+      return;
+    }
+    runAfterFramePaint(() => {
+      performance.mark("discourse-paint");
+      try {
+        performance.measure(
+          "discourse-init-to-paint",
+          "discourse-init",
+          "discourse-paint"
+        );
+      } catch (e) {
+        // eslint-disable-next-line no-console
+        console.warn("Failed to measure init-to-paint", e);
+      }
+    });
+  },
 });
diff --git a/app/assets/javascripts/discourse/app/lib/after-frame-paint.js b/app/assets/javascripts/discourse/app/lib/after-frame-paint.js
new file mode 100644
index 00000000000..9105167eaad
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/lib/after-frame-paint.js
@@ -0,0 +1,18 @@
+/**
+ * Runs `callback` shortly after the next browser Frame is produced.
+ * ref: https://webperf.tips/tip/measuring-paint-time
+ */
+export default function runAfterFramePaint(callback) {
+  // Queue a "before Render Steps" callback via requestAnimationFrame.
+  requestAnimationFrame(() => {
+    // MessageChannel is one of the highest priority task queues
+    // which will be executed after the frame has painted.
+    const messageChannel = new MessageChannel();
+
+    // Setup the callback to run in a Task
+    messageChannel.port1.onmessage = callback;
+
+    // Queue the Task on the Task Queue
+    messageChannel.port2.postMessage(undefined);
+  });
+}
diff --git a/app/assets/javascripts/discourse/app/templates/application.hbs b/app/assets/javascripts/discourse/app/templates/application.hbs
index ca18fd22e3a..38e2792b95f 100644
--- a/app/assets/javascripts/discourse/app/templates/application.hbs
+++ b/app/assets/javascripts/discourse/app/templates/application.hbs
@@ -1,7 +1,7 @@
 <DStyles />
 <DVirtualHeight />
 
-<DiscourseRoot>
+<DiscourseRoot {{did-insert this.trackDiscoursePainted}}>
   <a href="#main-container" id="skip-link">{{i18n "skip_to_main_content"}}</a>
   <DDocument />
   <PageLoadingSlider />
diff --git a/app/assets/javascripts/discourse/public/assets/scripts/discourse-boot.js b/app/assets/javascripts/discourse/public/assets/scripts/discourse-boot.js
index c9601aa0075..6849dbdc81e 100644
--- a/app/assets/javascripts/discourse/public/assets/scripts/discourse-boot.js
+++ b/app/assets/javascripts/discourse/public/assets/scripts/discourse-boot.js
@@ -3,19 +3,12 @@
     throw "Unsupported browser detected";
   }
 
-  // In Ember 3.28, the `ember` package is responsible for configuring `Helper.helper`,
-  // so we need to require('ember') before setting up any helpers.
-  // https://github.com/emberjs/ember.js/blob/744e536d37/packages/ember/index.js#L493-L493
-  // In modern Ember, the Helper.helper definition has moved to the helper module itself
-  // https://github.com/emberjs/ember.js/blob/0c5518ea7b/packages/%40ember/-internals/glimmer/lib/helper.ts#L134-L138
-  require("ember");
-
   let element = document.querySelector(
     `meta[name="discourse/config/environment"]`
   );
   const config = JSON.parse(
     decodeURIComponent(element.getAttribute("content"))
   );
-  const event = new CustomEvent("discourse-booted", { detail: config });
+  const event = new CustomEvent("discourse-init", { detail: config });
   document.dispatchEvent(event);
 })();
diff --git a/app/assets/javascripts/discourse/public/assets/scripts/start-app.js b/app/assets/javascripts/discourse/public/assets/scripts/start-app.js
index 3383ca92932..602f945ab3a 100644
--- a/app/assets/javascripts/discourse/public/assets/scripts/start-app.js
+++ b/app/assets/javascripts/discourse/public/assets/scripts/start-app.js
@@ -1,5 +1,5 @@
-document.addEventListener("discourse-booted", (e) => {
-  performance.mark("discourse-booted");
+document.addEventListener("discourse-init", (e) => {
+  performance.mark("discourse-init");
   const config = e.detail;
   const app = require(`${config.modulePrefix}/app`)["default"].create(config);
   app.start();
diff --git a/app/assets/javascripts/discourse/tests/test-boot-ember-cli.js b/app/assets/javascripts/discourse/tests/test-boot-ember-cli.js
index c4f129b6560..4847caafa63 100644
--- a/app/assets/javascripts/discourse/tests/test-boot-ember-cli.js
+++ b/app/assets/javascripts/discourse/tests/test-boot-ember-cli.js
@@ -6,7 +6,7 @@ import { setup } from "qunit-dom";
 import setupTests from "discourse/tests/setup-tests";
 import config from "../config/environment";
 
-document.addEventListener("discourse-booted", () => {
+document.addEventListener("discourse-init", () => {
   // eslint-disable-next-line no-undef
   if (!EmberENV.TESTS_FILE_LOADED) {
     throw new Error(