CSS filter() with invert() and hue-rotate() results in sluggish scrolling on Wikipedia
Categories
(Core :: Graphics: WebRender, defect)
Tracking
()
Performance Impact | pending-needinfo |
Tracking | Status | |
---|---|---|
firefox113 | --- | affected |
People
(Reporter: denschub, Unassigned, NeedInfo)
References
(Blocks 3 open bugs, )
Details
(Keywords: webcompat:platform-bug)
### Basic information
Scrolling the Wikipedia on Fenix is slow if the native dark mode is enabled.
How to enable dark mode:
- Go to https://en.m.wikipedia.org
- Log into your account
- Go to settings/user preferences (Open preferences).
- Scroll down and access "Gadgets".
- Enable "dark mode toggle" from "Appearance" section.
- Enable "Core styling for dark mode..." from "Utility modules" section.
Performance recording (profile)
Profile URL: https://share.firefox.dev/3zx0ncq
System configuration:
OS version: Android 13
GPU model: Adreno 506
Number of cores: 4+4 (Qualcomm SDM632 Snapdragon 632)
Amount of memory (RAM): 4 GiB
More information
Instead of re-defining colors for their dark mode, Wikimedia is using a global filter. The problematic rule is
html,
html img,
html video,
html ogvjs,
html svg,
html iframe,
html .mw-no-invert,
html td .diffchange,
html .mwe-math-element,
html .wvui-typeahead-suggestion__thumbnail,
html .skin-minerva .mw-notification-visible .mw-notification-content,
html .cdx-menu-item__thumbnail,
html .cx-slitem__image,
html .mw-mmv-overlay,
html .mw-mmv-pre-image,
html .media-viewer .image img,
html .media-viewer .mw-file-description img,
html .mw-kartographer-map,
html .mw-kartographer-mapDialog-map,
html .list-thumb,
html .ext-related-articles-card-list .ext-related-articles-card-thumb {
filter: invert(1) hue-rotate(180deg);
}
Having this filter on the entire page makes scrolling very sluggish.
This might just be a duplicate of bug 1193882 or other bugs, but since this applies nested filters to multiple elements, it could also be similar to bug 1600296 or something else, so I figured filing a new bug is a good idea.
Thanks so much for your help.
Comment 1•2 years ago
|
||
Seems like a CSS parsing issue.
The Performance Impact Calculator has determined this bug's performance impact to be high. If you'd like to request re-triage, you can reset the Performance Impact flag to "?" or needinfo the triage sheriff.
Platforms: Android
Impact on site: Causes noticeable jank
Configuration: Specific but common
Page load impact: Some
Websites affected: Major
[x] Able to reproduce locally
Comment 2•2 years ago
|
||
The severity field is not set for this bug.
:jfkthame, could you have a look please?
For more information, please visit BugBot documentation.
Comment 3•2 years ago
|
||
Bas, I'm curious why you see this as a CSS parsing issue? In the profile in comment 0, it looks to me like it's primarily the Renderer thread in the GPU process that's the issue, with its composites often taking over 20ms; that'll definitely prevent us getting anywhere near 60fps, won't it?
Comment 4•2 years ago
|
||
(In reply to Jonathan Kew [:jfkthame] from comment #3)
Bas, I'm curious why you see this as a CSS parsing issue? In the profile in comment 0, it looks to me like it's primarily the Renderer thread in the GPU process that's the issue, with its composites often taking over 20ms; that'll definitely prevent us getting anywhere near 60fps, won't it?
You are correct. I missed the profile. Sorry about that! I should have realized this before. Moving components. I'm guessing the inversion filter causes WebRender to hit some kind of slow path, that probably isn't exclusive to Android but may affect it disproportionately. I'll let Jeff & Jamie comment on that.
Reporter | ||
Comment 6•1 years ago
|
||
We got a report about another site doing the same, https://ponepaste.org/8805.
Comment 7•1 years ago
|
||
This seems very much an @ahale thing!
In particular, what do you think the severity should be? I feel like an estimated S2.8 rounds to S3, especially if you're working on it already?
Comment 8•1 year ago
|
||
The profile shows some really slow weird stuff in the Renderer thread, in SwapBuffers and QuerySurface, so this feels very Jamie-targetted. :)
Comment 9•1 year ago
|
||
I'm working on making all filter graphs native in WebRender https://bugzilla.mozilla.org/show_bug.cgi?id=1409486 which would avoid any possible blob fallback happening here, however this bug may be something else and I'll let Jamie assess that.
If this filter chain is not being accelerated properly in WebRender we can fix that, but it looks like something else may be going on in the profile.
As for the severity, I can imagine an argument that this is S2 because accessibility features are being used on a popular website (Wikipedia), there is a mechanistic quirk here - on desktop people who need this type of dark mode experience would tend to use an extension (and the extension may be doing this slightly differently), but on mobile it's common to use site features such as this to get a dark mode experience, so the scope of impact may be largely specific to mobile users of wikipedia. It's hard to say whether S3 or S2 is more appropriate in this case.
Comment 10•1 year ago
•
|
||
I don't think we have any blob fallback: all the worker threads are completely idle.
The reporter had an Adreno 506 which is on the less-powerful end of the spectrum. I can reproduce on Samsung A51 (Mali G72), but not a Pixel 5 (Adreno 620) or Pixel 7 (Mali G710). So this isn't specific to a model of GPU, but rather occurs on less powerful devices. On the more powerful devices I do see the frame time increase by a couple of milliseconds, but we're still comfortably in budget.
The provided profile is consistent with being GPU or perhaps more specifically memory bandwidth bound. Given the page is quite simple I would lean towards the latter. Here is a profile on the A51 which is much more concentrated on glClear()
clearing picture cache targets, which would validate that theory.
Enabling dark mode on both wikipedia and ponepaste.org results in picture caching no longer working. Instead of a single scrollable slice we appear to get 2 non-scrolling slices, meaning the entire page is invalidated when we scroll. On less powerful devices we cannot afford to do that. This is also the case on desktop. The question for those who understand picture caching slightly better is why we fail to create a scrollable slice, and can we fix that?
Comment 11•1 year ago
|
||
The severity field for this bug is set to S3. However, the Performance Impact
field flags this bug as having a high impact on the performance.
:gw, could you consider increasing the severity of this performance-impacting bug? Alternatively, if you think the performance impact is lower than previously assessed, could you request a re-triage from the performance team by setting the Performance Impact
flag to ?
?
For more information, please visit BugBot documentation.
Comment 12•1 year ago
|
||
The picture-cache issue sounds quite similar to bug 1836063 where CSS filters (opacity in that case) cause unnecessary picture-cache invalidation and slow scrolling with weak GPUs. A fix for that should be landing imminently
Excellent, hopefully it's the same issue! Thanks!
FWIW I don't think the performance impact flag here is accurate. Wikipedia is a major website but it only occurs if the user is signed in and enables dark mode. But I'll wait and see if bug 1836063 fixes this before requesting retriage.
Comment 14•1 year ago
|
||
I can replicate this issue on Raspberry Pi 4. However, I've just tested and sadly my fix for 1836063 doesn't resolve this.
Updated•1 year ago
|
Comment 15•9 months ago
|
||
Is this possibly the same cause as the abominable performance cost of the Dark Reader extension on desktop?
Updated•8 months ago
|
Comment 16•8 months ago
|
||
I did a bit of investigation in to this. Here's a simple testcase: https://codepen.io/jamienicol/pen/gOyvopr
To render this webrender first renders the text in to a render target, then renders that with a filter in to another render target, then renders that (I assume with the second filter) in to the picture cache tiles. The key thing is that as the filter is on the root html node, the spatial node used for these offscreen pictures is the root reference frame rather than the scroll frame. Meaning when we build the tile cache we see a prim list containing a single picture with the root reference frame as its spatial node, and therefore choose the root reference frame as the scroll root.
Here's an alternative implementation that attaches the filter to the body
node: https://codepen.io/jamienicol/pen/QWPmZwx . In this case the offscreen pictures have the scroll frame as their spatial node, meaning we choose that as the scroll root when building the tile cache.
In terms of the display list, the broken case (filter on html root) looks like this:
PushStackingContext(PushStackingContextDisplayItem { origin: (0.0, 0.0), spatial_id: SpatialId(1, PipelineId(1, 3)), prim_flags: IS_BACKFACE_VISIBLE, stacking_context: StackingContext { transform_style: Flat, mix_blend_mode: Normal, clip_chain_id: None, raster_space: Screen, flags: 0x0 } })
SolidColor p=0x7f5a45b42600 f=0x7f5a461d9020(Viewport(-1)) key=50 bounds(0,0,61440,40440) componentAlpha(0,0,0,0) clip() asr() clipChain() uniform (opaque 0,0,61440,40440) reuse-state(None) (rgba 255,255,255,255)
Rectangle(RectangleDisplayItem { common: CommonItemProperties { clip_rect: Box2D((0.0, 0.0), (1024.0, 674.0)), clip_chain_id: ClipChainId(18446744073709551615, PipelineId(4294967295, 4294967295)), spatial_id: SpatialId(1, PipelineId(1, 3)), flags: IS_BACKFACE_VISIBLE | CHECKERBOARD_BACKGROUND }, bounds: Box2D((0.0, 0.0), (1024.0, 674.0)), color: Value(ColorF { r: 1.0, g: 1.0, b: 1.0, a: 1.0 }) })
CompositorHitTestInfo p=0x7f5a45b3d020 f=0x7f5a461d9198(HTMLScroll(html)(-1)) key=22 bounds(0,0,0,0) componentAlpha(0,0,0,0) clip() asr() clipChain() hitTestInfo(0x1) hitTestArea(0,0,61440,40440) reuse-state(None)
HitTest(HitTestDisplayItem { rect: Box2D((0.0, 0.0), (1024.0, 674.0)), clip_chain_id: ClipChainId(18446744073709551615, PipelineId(4294967295, 4294967295)), spatial_id: SpatialId(1, PipelineId(1, 3)), flags: IS_BACKFACE_VISIBLE, tag: (0, 1) })
AsyncZoom p=0x7f5a45b41d08 f=0x7f5a461d9198(HTMLScroll(html)(-1)) key=2 bounds(0,0,61440,40440) componentAlpha(420,900,60559,107520) clip(0,0,61440,40440) asr() clipChain(0x7f5a45b3d170 <0,0,61440,40440> [root asr]) reuse-state(None) (flags 0x0) (scrolltarget 0)
RectClip(RectClipDisplayItem { id: ClipId(1, PipelineId(1, 3)), spatial_id: SpatialId(1, PipelineId(1, 3)), clip_rect: Box2D((0.0, 0.0), (1024.0, 674.0)) })
ClipChain(ClipChainItem { id: ClipChainId(0, PipelineId(1, 3)), parent: None })
PushReferenceFrame(ReferenceFrameDisplayListItem)
PushStackingContext(PushStackingContextDisplayItem { origin: (0.0, 0.0), spatial_id: SpatialId(2, PipelineId(1, 3)), prim_flags: IS_BACKFACE_VISIBLE, stacking_context: StackingContext { transform_style: Flat, mix_blend_mode: Normal, clip_chain_id: Some(ClipChainId(0, PipelineId(1, 3))), raster_space: Screen, flags: 0x0 } })
Filter p=0x7f5a45b41b98 f=0x7f5a461d9198(HTMLScroll(html)(-1)) key=27 bounds(0,0,61440,40440) componentAlpha(420,900,60559,107520) clip() asr() clipChain() reuse-state(None) effects=(filter)
RectClip(RectClipDisplayItem { id: ClipId(2, PipelineId(1, 3)), spatial_id: SpatialId(2, PipelineId(1, 3)), clip_rect: Box2D((0.0, 0.0), (1024.0, 674.0)) })
ClipChain(ClipChainItem { id: ClipChainId(1, PipelineId(1, 3)), parent: None })
ClipChain(ClipChainItem { id: ClipChainId(2, PipelineId(1, 3)), parent: None })
SetFilterOps
PushStackingContext(PushStackingContextDisplayItem { origin: (0.0, 0.0), spatial_id: SpatialId(2, PipelineId(1, 3)), prim_flags: IS_BACKFACE_VISIBLE, stacking_context: StackingContext { transform_style: Flat, mix_blend_mode: Normal, clip_chain_id: None, raster_space: Screen, flags: 0x0 } })
CompositorHitTestInfo p=0x7f5a45b3d0d0 f=0x7f5a461d90c8(Canvas(html)(-1)) key=278 bounds(0,0,0,0) componentAlpha(0,0,0,0) clip() asr(<0x7f5a461d9238>) clipChain(0x7f5a45b3d170 <0,0,61440,40440> [root asr]) hitTestInfo(0x1) hitTestArea(0,0,61440,175680) reuse-state(None)
ClipChain(ClipChainItem { id: ClipChainId(3, PipelineId(1, 3)), parent: None })
HitTest(HitTestDisplayItem { rect: Box2D((0.0, 0.0), (1024.0, 2928.0)), clip_chain_id: ClipChainId(3, PipelineId(1, 3)), spatial_id: SpatialId(3, PipelineId(1, 3)), flags: IS_BACKFACE_VISIBLE, tag: (2, 1) })
CanvasBackgroundColor p=0x7f5a45b3d1c8 f=0x7f5a461d90c8(Canvas(html)(-1)) key=14 bounds(0,0,61440,175680) componentAlpha(0,0,0,0) clip(0,0,61440,107520) asr(<0x7f5a461d9238>) clipChain(0x7f5a45b3d268 <0,0,61440,107520> [0x7f5a461d9238], 0x7f5a45b3d170 <0,0,61440,40440> [root asr]) uniform (opaque 0,0,61440,175680) reuse-state(None) (rgba 255,255,255,255)
ClipChain(ClipChainItem { id: ClipChainId(4, PipelineId(1, 3)), parent: None })
Rectangle(RectangleDisplayItem { common: CommonItemProperties { clip_rect: Box2D((0.0, 0.0), (1024.0, 1792.0)), clip_chain_id: ClipChainId(4, PipelineId(1, 3)), spatial_id: SpatialId(3, PipelineId(1, 3)), flags: IS_BACKFACE_VISIBLE }, bounds: Box2D((0.0, 0.0), (1024.0, 2928.0)), color: Value(ColorF { r: 1.0, g: 1.0, b: 1.0, a: 1.0 }) })
... Lots of hit test and text items
And with the filter on the body:
PushStackingContext(PushStackingContextDisplayItem { origin: (0.0, 0.0), spatial_id: SpatialId(1, PipelineId(1, 3)), prim_flags: IS_BACKFACE_VISIBLE, stacking_context: StackingContext { transform_style: Flat, mix_blend_mode: Normal, clip_chain_id: None, raster_space: Screen, flags: 0x0 } })
SolidColor p=0x7fe2460450f8 f=0x7fe2467d9020(Viewport(-1)) key=50 bounds(0,0,61440,40440) componentAlpha(0,0,0,0) clip() asr() clipChain() uniform (opaque 0,0,61440,40440) reuse-state(None) (rgba 0,0,0,255)
Rectangle(RectangleDisplayItem { common: CommonItemProperties { clip_rect: Box2D((0.0, 0.0), (1024.0, 674.0)), clip_chain_id: ClipChainId(18446744073709551615, PipelineId(4294967295, 4294967295)), spatial_id: SpatialId(1, PipelineId(1, 3)), flags: IS_BACKFACE_VISIBLE | CHECKERBOARD_BACKGROUND }, bounds: Box2D((0.0, 0.0), (1024.0, 674.0)), color: Value(ColorF { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }) })
CompositorHitTestInfo p=0x7fe246043930 f=0x7fe2467d9198(HTMLScroll(html)(-1)) key=22 bounds(0,0,0,0) componentAlpha(0,0,0,0) clip() asr() clipChain() hitTestInfo(0x1) hitTestArea(0,0,61440,40440) reuse-state(None)
HitTest(HitTestDisplayItem { rect: Box2D((0.0, 0.0), (1024.0, 674.0)), clip_chain_id: ClipChainId(18446744073709551615, PipelineId(4294967295, 4294967295)), spatial_id: SpatialId(1, PipelineId(1, 3)), flags: IS_BACKFACE_VISIBLE, tag: (0, 1) })
AsyncZoom p=0x7fe246043d18 f=0x7fe2467d9198(HTMLScroll(html)(-1)) key=2 bounds(0,0,61440,40440) componentAlpha(420,900,60559,107520) clip(0,0,61440,40440) asr() clipChain(0x7fe24603f170 <0,0,61440,40440> [root asr]) (opaque 0,0,61440,175680) reuse-state(None) (flags 0x0) (scrolltarget 0)
RectClip(RectClipDisplayItem { id: ClipId(1, PipelineId(1, 3)), spatial_id: SpatialId(1, PipelineId(1, 3)), clip_rect: Box2D((0.0, 0.0), (1024.0, 674.0)) })
ClipChain(ClipChainItem { id: ClipChainId(0, PipelineId(1, 3)), parent: None })
PushReferenceFrame(ReferenceFrameDisplayListItem)
PushStackingContext(PushStackingContextDisplayItem { origin: (0.0, 0.0), spatial_id: SpatialId(2, PipelineId(1, 3)), prim_flags: IS_BACKFACE_VISIBLE, stacking_context: StackingContext { transform_style: Flat, mix_blend_mode: Normal, clip_chain_id: Some(ClipChainId(0, PipelineId(1, 3))), raster_space: Screen, flags: 0x0 } })
CompositorHitTestInfo p=0x7fe246043520 f=0x7fe2467d90c8(Canvas(html)(-1)) key=278 bounds(0,0,0,0) componentAlpha(0,0,0,0) clip() asr(<0x7fe2467d9238>) clipChain(0x7fe24603f170 <0,0,61440,40440> [root asr]) hitTestInfo(0x1) hitTestArea(0,0,61440,175680) reuse-state(None)
RectClip(RectClipDisplayItem { id: ClipId(2, PipelineId(1, 3)), spatial_id: SpatialId(2, PipelineId(1, 3)), clip_rect: Box2D((0.0, 0.0), (1024.0, 674.0)) })
ClipChain(ClipChainItem { id: ClipChainId(1, PipelineId(1, 3)), parent: None })
ClipChain(ClipChainItem { id: ClipChainId(2, PipelineId(1, 3)), parent: None })
ClipChain(ClipChainItem { id: ClipChainId(3, PipelineId(1, 3)), parent: None })
HitTest(HitTestDisplayItem { rect: Box2D((0.0, 0.0), (1024.0, 2928.0)), clip_chain_id: ClipChainId(3, PipelineId(1, 3)), spatial_id: SpatialId(3, PipelineId(1, 3)), flags: IS_BACKFACE_VISIBLE, tag: (2, 1) })
CanvasBackgroundColor p=0x7fe24603f1c8 f=0x7fe2467d90c8(Canvas(html)(-1)) key=14 bounds(0,0,61440,175680) componentAlpha(0,0,0,0) clip(0,0,61440,107520) asr(<0x7fe2467d9238>) clipChain(0x7fe24603f268 <0,0,61440,107520> [0x7fe2467d9238], 0x7fe24603f170 <0,0,61440,40440> [root asr]) uniform (opaque 0,0,61440,175680) reuse-state(None) (rgba 0,0,0,255)
ClipChain(ClipChainItem { id: ClipChainId(4, PipelineId(1, 3)), parent: None })
Rectangle(RectangleDisplayItem { common: CommonItemProperties { clip_rect: Box2D((0.0, 0.0), (1024.0, 1792.0)), clip_chain_id: ClipChainId(4, PipelineId(1, 3)), spatial_id: SpatialId(3, PipelineId(1, 3)), flags: IS_BACKFACE_VISIBLE }, bounds: Box2D((0.0, 0.0), (1024.0, 2928.0)), color: Value(ColorF { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }) })
CompositorHitTestInfo p=0x7fe246043110 f=0x7fe2467d9958(Block(html)(-1)) key=22 bounds(0,0,0,0) componentAlpha(0,0,0,0) clip(0,0,61440,107520) asr(<0x7fe2467d9238>) clipChain(0x7fe24603f268 <0,0,61440,107520> [0x7fe2467d9238], 0x7fe24603f170 <0,0,61440,40440> [root asr]) hitTestInfo(0x1) hitTestArea(0,0,61440,175680) reuse-state(None)
HitTest(HitTestDisplayItem { rect: Box2D((0.0, 0.0), (1024.0, 1792.0)), clip_chain_id: ClipChainId(4, PipelineId(1, 3)), spatial_id: SpatialId(3, PipelineId(1, 3)), flags: IS_BACKFACE_VISIBLE, tag: (2, 1) })
Filter p=0x7fe246043ab8 f=0x7fe2467d9a20(Block(body)(2)) key=27 bounds(480,960,60480,173760) componentAlpha(420,900,60559,107520) clip(0,0,61440,107520) asr(<0x7fe2467d9238>) clipChain(0x7fe24603f268 <0,0,61440,107520> [0x7fe2467d9238], 0x7fe24603f170 <0,0,61440,40440> [root asr]) reuse-state(None) effects=(filter)
RectClip(RectClipDisplayItem { id: ClipId(3, PipelineId(1, 3)), spatial_id: SpatialId(3, PipelineId(1, 3)), clip_rect: Box2D((0.0, 0.0), (1024.0, 1792.0)) })
ClipChain(ClipChainItem { id: ClipChainId(5, PipelineId(1, 3)), parent: None })
SetFilterOps
PushStackingContext(PushStackingContextDisplayItem { origin: (0.0, 0.0), spatial_id: SpatialId(3, PipelineId(1, 3)), prim_flags: IS_BACKFACE_VISIBLE, stacking_context: StackingContext { transform_style: Flat, mix_blend_mode: Normal, clip_chain_id: Some(ClipChainId(5, PipelineId(1, 3))), raster_space: Screen, flags: 0x0 } })
... Lots of hit test and text items
Markus, Glenn, can you think of anything we can do from the gecko or webrender end to help with this situation? Could we in some way detect when all of the filtered items have a certain spatial node and do better? In the broken case here everything that is filtered has the same spatial node, except for the CanvasBackgroundColor, but perhaps we can handle that in the simple case (ie solid color)
Comment 17•8 months ago
|
||
Sorry forgot to needinfo. See above comment
Comment 18•8 months ago
|
||
Yes, I think we should be able to handle the simple (common) case you mention, where the fixed content is a known solid color.
There are ways to produce content where you do need to rasterize every scroll (e.g. if the fixed content is a gradient or image), but they are rare compared to the fixed color case.
I'll need to ponder a bit the simplest way to detect this and how we would implement it, but it seems like a reasonable optimization to have that would handle most common cases where this occurs.
Comment 19•3 months ago
|
||
The severity field for this bug is set to S3. However, the Performance Impact
field flags this bug as having a HIGH
impact on performance.
:gw, could you consider increasing the severity of this performance-impacting bug? Alternatively, if you think the performance impact is lower than previously assessed, could you request a re-triage from the performance team by setting the Performance Impact
flag to ?
?
If this bug is considered an S3 despite the perf impact, can you please provide rationale? Thank you!
Updated•3 months ago
|
Comment 20•3 months ago
|
||
Hey Dennis, can you see if this is still happening for you? I don't reproduce on my Android.
Can you especially check if Wikipedia changed their CSS so that they don't use this property anymore?
Thanks!
Updated•3 months ago
|
Updated•2 months ago
|
Comment 21•3 days ago
|
||
My experience with this
I recently realized that I’ve been living with this bug on both my laptop and computer for years. Since I have powerful machines, the performance drop wasn’t very noticeable, I just assumed that CSS processing in Firefox was slightly slower than in Chrome.
However, when OpenStreetMap implemented a dark mode with complex filters applied to map tiles, I finally noticed something was wrong.
The easiest way to test if you have this bug is to visit https://issviewer.com/index.html which uses this filter in dark mode: brightness(0.6) invert(1) contrast(3) hue-rotate(200deg) saturate(0.3) brightness(0.7)
Try moving the map really fast in light mode and dark mode, for me dark mode was extremely laggy, running at around 10 FPS, while the light mode was perfectly smooth at 60 FPS.
I managed to fix my performance issue by disabling style customization with userChrome.css
(which I use to change bookmark icon in the toolbar) in about:config
toolkit.legacyUserProfileCustomizations.stylesheets
, then restart Firefox, and the best part is that I can re-enable it, restart, and now everything works perfectly!
Interestingly, I never had to do this on my laptop, style customization and hardware acceleration were always enabled and working, it seems the issue fixed itself in a recent update but not on my computer.
Now I also notice performance improvement on x.com, it wasn't bad enough before to suspect anything was wrong, scrolling was okay around 30 FPS, I thought it was because of all the videos in the timeline, but now it's super smooth at 60 FPS.
Description
•