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

#### Description of Change

Fixes https://github.com/electron/electron/issues/47630.

Backports https://crrev.com/c/7510894 (minus the test changes) and adds an Electron-specific test.

The change is guarded behind the `AlwaysLogLOAFURL` Chromium feature (`--enable-features=AlwaysLogLOAFURL`), which is disabled by default. Thus, the change is very low risk and safe to backport.

The backported CL makes it possible to get script attribution for [`long-animation-frame`](https://developer.mozilla.org/en-US/docs/Web/API/Performance_API/Long_animation_frame_timing) `PerformanceObserver` events in Electron apps.

`long-animation-frame` events can be used to debug UI freezes. Script attribution via the `sourceURL` attribute lets the application developer figure out which script is slow.

By default, script attribution is only available for http(s), blob, and data URLs. Electron apps use `file://` or custom protocols. Hence, this feature is needed to get script attribution working in Electron apps.

#### Checklist
<!-- Remove items that do not apply. For completed items, change [ ] to [x]. -->

- [x] PR description included
- [x] I have built and tested this PR
- [x] `npm test` passes
- [x] [PR release notes](https://github.com/electron/clerk/blob/main/README.md) describe the change in a way relevant to app developers, and are [capitalized, punctuated, and past tense](https://github.com/electron/clerk/blob/main/README.md#examples).

#### Release Notes

Notes: Added support for `long-animation-frame` script attribution (via `--enable-features=AlwaysLogLOAFURL`).

----

<img width="1892" height="1492" alt="Image" src="https://github.com/user-attachments/assets/7790d0b8-15f9-4a35-ae88-2e981a2d7594" />

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: @@ -145,3 +145,4 @@ expose_gtk_ui_platform_field.patch
 fix_os_crypt_async_cookie_encryption.patch
 patch_osr_control_screen_info.patch
 graphite_handle_out_of_order_recording_errors.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 24e58da8eaa9319e2bb626c69d5ad23de720e108..0ffea9dafe3e3dffcbaf9031082aa88dccb46267 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
+@@ -516,8 +516,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 f2fe39be2db525d89fcd9787c2ae9285babab26d..c395cf39d404f6c4f6f6e23c9fb8dfe92151a7d2 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>