Skip to content

[Bug]: macOS backend flickers/flashes due to use of timers #31991

Description

@iccir

Bug summary

Right now, the macOS backend has a lot of "misfires" where it will momentary render a blank figure. This typically occurs when the figure is first displayed as well as during a window resize.

Here's a screen recording of me resizing "galleries/examples/pie_and_polar_charts/polar_demo.py" on an old MacBook Air:

Video of problem

⚠️ WARNING: This video shows a flashing/flickering white box on top of a dark background.

movie.mp4

This also occurs on modern/faster hardware although not as pronounced.

The root cause is the use of timers to call setNeedsDisplay:YES. This was implemented in #21790 in order to fix #10980 and #17445. However, I believe that pull request was done without realizing it's possible to send the Agg buffer directly to an NSView (via layer-backing or layer-hosting) without the need of a -drawRect: callback.


At a very general level, the macOS run loop has different phases of execution:

  1. Event-processing / Timers
  2. Layout
  3. Drawing

Right now, in response to a draw_idle() call, the macosx backend creates a single-shot NSTimer and adds it to the run loop. At best, the timer will not fire until the next run loop cycle. If we are in "Phase 1" above, the earliest that _draw_idle() will be called is the next "Phase 1". We will skip over the actual draw phase ("Phase 3") of the current run loop cycle.

Again, this is a best case scenario. Per Timer Tolerance:

"The system reserves the right to apply a small amount of tolerance to certain timers regardless of the value of the tolerance property.

Basically, using an NSTimer is the equivalent of telling the system: "perform this invocation no earlier than X, but you can delay a bit after X to save power."

draw_idle() should instead do the following:

  1. If running in the main thread, immediately call setNeedsDisplay.
  2. If running on a background thread, dispatch_async() to the main thread and then call setNeedsDisplay.

Caution

This assumes a layer-backed/layer-hosted approach. It won't work with the current -drawRect: approach without breaking blitting.

Code for reproduction

# galleries/examples/pie_and_polar_charts/polar_demo.py

Actual outcome

Flicker, misalignment with macOS drawing phase

Expected outcome

No flicker when resizing. No flicker upon first showing the figure.

Additional information

No response

Operating system

macOS (Tested on 10.14, 14.7, and 27 beta 2)

Matplotlib Version

3.11

Matplotlib Backend

macosx

Python version

3.14.5

Jupyter version

No response

Installation

None

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions