← reports

#4515 · Continuous working-indicator mask

BugMedium priorityMedium effortui · perfopen issue2026-09-29 · base b14b3b100f2f

Verdict: PARTIALLY REPRODUCED · Root-cause confidence: low for the reported WindowServer CPU difference; high for the continuous animation mechanism.

1. TL;DR

Visible working indicators still use a continuously changing image mask. The current trusted source contains 30 distinct mask frames per second and requests a compositing layer for each affected element. A second clean checkout confirms those exact asset and style properties. This Linux investigation could not repeat the macOS WindowServer measurement or establish how many frames reached a display. The animation is a plausible source of compositor work, but its measured share of the reported CPU difference is unverified.

2. Claims vs findings

ClaimStatusEvidence
A working cue has an ongoing image animation.VerifiedThe checked-in APNG has 30 distinct frames, 1/30-second delays, and an infinite loop. Both visible cue classes use it as a mask.
The animation continues without main-thread painting in the reported desktop setup.UnverifiedNo macOS desktop trace was available in this investigation. Static asset inspection cannot prove Chromium scheduling or presentation frequency.
Removing the mask lowers WindowServer CPU by the reported amount.UnverifiedThe available host is Linux. No comparable macOS windows, displays, or WindowServer process were available.
Reduced-motion mode removes the mask.Verified in sourceThe media query removes both mask properties and resets will-change.

3. Environment

4. Minimal reproduction

  1. Check out the trusted base and install/build it with the commands above.
  2. Run node check-shimmer.mjs <checkout-path> using the saved reproduction script. It reads the checked-in PNG chunks and the corresponding CSS; it does not launch a browser.

Expected for a static working cue: no indefinitely cycling mask frames. Actual:

APNG: 30 distinct frames per second; infinite loop; both visible shimmer classes use the mask and request a layer

The script is included here verbatim:

import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';

const root = process.argv[2];
assert.ok(root, 'pass the trusted repository checkout path');
const png = readFileSync(join(root, 'apps/app/src/components/ui/shine-sweep.png'));
const css = readFileSync(join(root, 'apps/app/src/components/ui/theme.css'), 'utf8');
assert.equal(png.subarray(0, 8).toString('hex'), '89504e470d0a1a0a');
const chunks = [];
for (let offset = 8; offset < png.length;) {
  const length = png.readUInt32BE(offset);
  const type = png.toString('ascii', offset + 4, offset + 8);
  chunks.push({ type, data: png.subarray(offset + 8, offset + 8 + length) });
  offset += length + 12;
}
const animation = chunks.find((chunk) => chunk.type === 'acTL');
assert.ok(animation, 'mask must be an APNG');
const frames = chunks.filter((chunk) => chunk.type === 'fcTL');
assert.equal(animation.data.readUInt32BE(0), frames.length);
assert.equal(animation.data.readUInt32BE(4), 0, 'animation loops indefinitely');
assert.equal(frames.length, 30);
assert.ok(frames.every((frame) => frame.data.readUInt16BE(20) === 1));
assert.ok(frames.every((frame) => frame.data.readUInt16BE(22) === 30));
const framePayloads = chunks.filter((chunk) => chunk.type === 'IDAT' || chunk.type === 'fdAT')
  .map((chunk) => (chunk.type === 'fdAT' ? chunk.data.subarray(4) : chunk.data).toString('hex'));
assert.equal(new Set(framePayloads).size, 30, 'all frames contain distinct pixels');
assert.match(css, /\.animate-shine,\s*\.animate-shine-icon\s*\{[^}]*mask-image:\s*url\("\.\/shine-sweep\.png"\)/);
assert.match(css, /\.animate-shine,\s*\.animate-shine-icon\s*\{[^}]*will-change:\s*transform/);
console.log(`APNG: ${frames.length} distinct frames per second; infinite loop; both visible shimmer classes use the mask and request a layer`);

5. Root cause

The generator constants set 30 frames over one second. Its APNG control chunks specify an unlimited loop and 1/30-second frame delays. The checked-in image matches those settings. The shared cue rule applies that image as a mask and sets will-change: transform. The active row styles assign those cue classes to the icon and label. Thus visible active cues can request successive frame updates while the page is otherwise idle. The hidden-host rule and reduced-motion rule remove the mask in their respective cases. The exact compositor and WindowServer cost depends on the browser, visible cue count, window layout, and displays; it was not measured here.

6. Proposed fix

First measure frame activity and WindowServer or GPU CPU on comparable macOS desktop windows with a visible working cue, then compare a lower cadence with the current mask. Keep a legible active indicator and check visual smoothness before selecting a cadence. A source-only reduction has no verified CPU benefit yet.

7. Related work

PR #3906 is merged; its GitHub metadata describes replacing a main-thread CSS sweep with this image mask and reported remaining compositor draws. No open PR links to this issue. The separate #3921 report concerns a different status spinner.

8. Verification

The same agent created a second clean detached checkout at b14b3b100f2fd7772c801710bc1f1ebcf089f3de, installed and built it, and ran the saved script against that checkout. It printed the identical line above and exited 0. The report was limited to partial reproduction because neither run measured macOS WindowServer CPU or display presentation. No correction to the asset finding was needed.

9. Appendix

First checkout: pnpm install --frozen-lockfile --prefer-offline (exit 0), pnpm exec turbo run build (exit 0), node check-shimmer.mjs <checkout-path> (exit 0). Second clean checkout: same commands and results. Source checks: git rev-parse origin/main, rg for cue classes, and line-numbered reads of the CSS and generator. No untrusted issue command, attachment, linked branch, or external issue URL was executed. Issue prose was treated only as a claim to test.