Hey all!
I'm having an issue on a website where intermittent stuttering is occurring, but it is not exactly clear why it is happening or how to prevent it.
As you will see below, it appears that GC has something to do with the problem, but the frame stuttering is actually caused by the GPU doing more work as a result of the GC pass instead of the GC pass causing the main thread to not dispatch rendering commands in time.
What is clear is that this happens on Chrome/Edgium 99 and 100 and on Windows (Intel) and MacOS (M1 Max), so it is likely a behavior of Chrome itself.
Notably, this stuttering doesn't occur on Firefox or Safari.
For context, this website uses requestAnimationFrame() to create a rendering loop for a Three.js based 3D experience. Normally the loop runs at a solid ~160 FPS (to match my monitor refresh rate), but occasionally it will pause for a fraction of a second.
To make the issue more apparent, you can drag the solid purple block onto one of the square wireframes to cause it to animate along the path.
The stuttering happens regardless of if the block is animating, but it is easier to see when it is.
After profiling, it appears that these stutters occur after Major GC passes, but are not directly related to the GC pass.
You can see in the included profile that before each group of dropped frames there is a Major GC pause, but oddly the GC doesn't last long enough to actually cause frames to be dropped on the main thread.
Instead, we see a couple more "animation frame fired" events and then suddenly we're dropping frames. Looking at the GPU, we can see that it is very busy for those dropped frames.
Where a normal frame takes around ~1ms, the combined GPU activity after GC takes a whopping ~44ms.
After verifying that the issue still occurs, I recorded a trace and started inspecting work around the dropped frames.
And this is the confusing part, because it appears that the GC pass is triggering extra work on the GPU.
In normal operation, the following sequence of steps happen in order according to the trace and flow events:
1. The GpuVSyncThread posts a task to the VizCompositorThread at a set interval. (~160 Times per second)
2. The VizCompositorThread sends a message to the Renderer Process Compositor which then sends the BeginMainFrame event to the CrRendererMain thread.
3. VizCompositorThread then sends a message to CrGpuMain to tell it to swap the framebuffers.
4. CrRendererMain calls into V8 to trigger the requestAnimationFrame() callback, which then triggers a CommandBufferHelper::Flush which triggers CommandBufferStub::OnAsyncFlush on CrGpuMain indirectly via the GPU Process ChildIOThread. (Technically there is also some additional Compositor work that executes after the JS function call which triggers a second CommandBufferStub::OnAsyncFlush call as well, but I don't want to be too detailed)
5. CrGpuMain then executes all the command buffer commands and the frame rendering is done.
However when a stutter occurs, it looks like this:
1. The GpuVSyncThread posts a task to the VizCompositorThread at a set interval. (~160 Times per second)
2. The VizCompositorThread sends a message to the Renderer Process Compositor which then sends the BeginMainFrame event to the CrRendererMain thread.
3. VizCompositorThread then sends a message to CrGpuMain to tell it to swap the framebuffers.
4. CrRendererMain calls into V8 to trigger the requestAnimationFrame() callback, which then triggers a CommandBufferHelper::Flush which triggers CommandBufferStub::OnAsyncFlush on CrGpuMain indirectly via the GPU Process ChildIOThread. V8 also triggers a Minor GC pass.
5. Minor GC Pass executes and triggers several incremental GC passes.
6. Incremental GC schedules Major GC.
7. Another requestAnimationFrame() callback executes and properly flushes the command buffer and schedules the draw calls. (Just like the normal sequence)
8. Major GC starts, marks objects and then for some reason triggers two additional CommandBufferHelper::Flush calls during CppGC.SweepInvokePreFinalizers. One of these triggers a CommandBufferStub::OnAsyncFlush that takes ~18ms to execute.
9. A third requestAnimationFrame() callback executes and it flushes the command buffer like normal, however this time it takes the GPU ~35ms to execute it this time instead of the usual ~1ms.
10. Finally, the swap buffers task runs for the next real frame and that takes ~41ms instead of the usual ~1ms.
I'm a little at a loss for why GC would create additional work for the GPU.
For now, my best guess is that GC is incorrectly collecting the command buffers and triggers the destructor for CommandBufferHelper which then frees some of the associated memory resources.
Then on the next frame, everything needs to be setup again which causes the next two flushes to take much longer than they otherwise would. Part of my thinking for this is validated by the fact that CommandBufferHelper::~CommandBufferHelper() calls FreeRingBuffer() (in gpu/command_buffer/client/cmd_buffer_helper.cc), but I'm not really sure how the lost state would be reconciled - though I guess Chromium probably has a mechanism for freeing and resuming GPU resources in unused/idle/suspended tabs.
Does anyone have ideas on this?
I'm including both the profile and trace that I've recorded. Hopefully they're useful for debugging.
Thanks for helping!
--
--
Chromium Developers mailing list: chromi...@chromium.org
View archives, change email options, or unsubscribe:
http://groups.google.com/a/chromium.org/group/chromium-dev
---
You received this message because you are subscribed to the Google Groups "Chromium-dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email to chromium-dev...@chromium.org.
To view this discussion on the web visit https://groups.google.com/a/chromium.org/d/msgid/chromium-dev/0d05262c-8aaf-4b3b-a4ff-f56ca5434af2n%40chromium.org.