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(