Repository: electron/electron
Pull request: feat: add support for `long-animation-frame` script attribution

Backport of #49706

See that PR for details.


Notes: Added support for `long-animation-frame` script attribution (via `--enable-features=AlwaysLogLOAFURL`).


Changed files:
filename: docs/api/command-line-switches.md
patch: @@ -370,6 +370,13 @@ Keep in mind that standalone switches can sometimes be split into individual fea
 
 Finally, you'll need to ensure that the version of Chromium in Electron matches the version of the browser you're using to cross-reference the switches.
 
+### Chromium features relevant to Electron apps
+
+* `AlwaysLogLOAFURL`: enables script attribution for
+  [`long-animation-frame`](https://developer.mozilla.org/en-US/docs/Web/API/Performance_API/Long_animation_frame_timing)
+  `PerformanceObserver` events for non-http(s), non-data, non-blob URLs (such as `file:` or custom
+  protocol URLs).
+
 [app]: app.md
 [append-switch]: command-line.md#commandlineappendswitchswitch-value
 [debugging-main-process]: ../tutorial/debugging-main-process.md

filename: patches/chromium/.patches
patch: @@ -144,3 +144,4 @@ fix_linux_tray_id.patch
 expose_gtk_ui_platform_field.patch
 fix_os_crypt_async_cookie_encryption.patch
 cherry-pick-e045399a1ecb.patch
+loaf_add_feature_to_enable_sourceurl_for_all_protocols.patch

filename: patches/chromium/loaf_add_feature_to_enable_sourceurl_for_all_protocols.patch
patch: @@ -0,0 +1,52 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Niklas Wenzel <dev@nikwen.de>
+Date: Wed, 4 Feb 2026 06:02:40 -0800
+Subject: LoAF: Add feature to enable sourceURL for all protocols
+
+Backports https://crrev.com/c/7510894 (minus the test changes).
+
+Can be removed when that CL is included via a Chromium roll.
+
+Change-Id: Id5e58a151b13cc0ac054f4ec237b038255d683fd
+Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7510894
+Commit-Queue: Noam Rosenthal <nrosenthal@google.com>
+Reviewed-by: Dave Tapuska <dtapuska@chromium.org>
+Reviewed-by: Noam Rosenthal <nrosenthal@google.com>
+Cr-Commit-Position: refs/heads/main@{#1579397}
+
+diff --git a/third_party/blink/renderer/core/frame/animation_frame_timing_monitor.cc b/third_party/blink/renderer/core/frame/animation_frame_timing_monitor.cc
+index 20eb792c45d23874fb6a3982a335a140820a94d0..1e8c8c6f73bf576f10cb32beb80cadcaa9516b71 100644
+--- a/third_party/blink/renderer/core/frame/animation_frame_timing_monitor.cc
++++ b/third_party/blink/renderer/core/frame/animation_frame_timing_monitor.cc
+@@ -524,8 +524,15 @@ void AnimationFrameTimingMonitor::Trace(Visitor* visitor) const {
+   visitor->Trace(frame_handling_input_);
+ }
+ 
++BASE_FEATURE(kAlwaysLogLOAFURL, base::FEATURE_DISABLED_BY_DEFAULT);
++
+ namespace {
++
+ bool ShouldAllowScriptURL(const String& url) {
++  if (base::FeatureList::IsEnabled(kAlwaysLogLOAFURL)) {
++    return true;
++  }
++
+   KURL kurl(url);
+   return kurl.ProtocolIsData() || kurl.ProtocolIsInHTTPFamily() ||
+          kurl.ProtocolIs("blob") || kurl.IsEmpty();
+diff --git a/third_party/blink/renderer/core/frame/animation_frame_timing_monitor.h b/third_party/blink/renderer/core/frame/animation_frame_timing_monitor.h
+index 60a891c17f34ec617c86d0fcbfcec0f2469bdd07..91475dfc3175cf49b8396c9f029cb25bf3ec1e75 100644
+--- a/third_party/blink/renderer/core/frame/animation_frame_timing_monitor.h
++++ b/third_party/blink/renderer/core/frame/animation_frame_timing_monitor.h
+@@ -22,6 +22,11 @@ class TimeTicks;
+ 
+ namespace blink {
+ 
++// When enabled, long-animation-frame events will always include the sourceURL,
++// regardless of protocol. This is useful during development when using `file:`
++// URLs or custom protocols defined by embedders.
++CORE_EXPORT BASE_DECLARE_FEATURE(kAlwaysLogLOAFURL);
++
+ class LocalFrame;
+ 
+ // Monitors long-animation-frame timing (LoAF).

filename: spec/chromium-spec.ts
patch: @@ -3280,6 +3280,29 @@ describe('chromium features', () => {
       expect(rgb).to.equal('');
     });
   });
+
+  describe('long-animation-frame', () => {
+    it('should include script attribution on custom protocols if AlwaysLogLOAFURL is enabled', async () => {
+      const rc = await startRemoteControlApp(['--enable-features=AlwaysLogLOAFURL']);
+      const hasAttribution = await rc.remotely(async (fixture: string) => {
+        const { BrowserWindow, protocol, net } = require('electron/main');
+        const { pathToFileURL } = require('node:url');
+
+        protocol.handle('custom', () => net.fetch(pathToFileURL(fixture).toString()));
+
+        // `show: true` is necessary on Windows and Linux due to https://github.com/electron/electron/issues/32001
+        const w = new BrowserWindow({ show: true });
+        await w.loadURL('custom://my-url');
+
+        const hasAttribution = await w.webContents.executeJavaScript('hasAttributionPromise');
+
+        global.setTimeout(() => require('electron').app.quit());
+
+        return hasAttribution;
+      }, path.join(fixturesPath, 'chromium', 'long-animation-frame.html'));
+      expect(hasAttribution).to.be.true();
+    });
+  });
 });
 
 describe('font fallback', () => {

filename: spec/fixtures/chromium/long-animation-frame.html
patch: @@ -0,0 +1,29 @@
+<!doctype html>
+<html>
+  <body>
+    <div id="update"></div>
+    <script>
+      var hasAttributionPromise = new Promise((resolve) => {
+        new PerformanceObserver((list) => {
+          const entries = list.getEntries();
+          const has_attribution =
+            entries.length > 0 &&
+            entries.every(
+              ({ scripts }) =>
+                scripts.length > 0 &&
+                scripts.every(({ sourceURL }) => sourceURL === location.href),
+            );
+          resolve(has_attribution);
+        }).observe({ entryTypes: ["long-animation-frame"] });
+
+        // Produce a long animation frame
+        requestAnimationFrame(() => {
+          const before = performance.now();
+          document.querySelector("#update").innerHTML = "Update";
+          while (performance.now() < before + 60) {}
+          requestAnimationFrame(() => {});
+        });
+      });
+    </script>
+  </body>
+</html>