Skip to content

Timer + long-running callback expectations #29029

Open

Description

@greglucas

Do we want to say anything about how we expect our event timers to behave with regard to when they fire? Should a timer be ticking continuously and firing when requested regardless of other tasks occurring (maybe in a separate thread), or should the timers fire at (time in callback + interval)?

Following up with some results about what the various backends currently do for callbacks that last longer than the timers with the following test script that will either print ~1/second or every 2-seconds.

import time
import matplotlib.pyplot as plt

fig = plt.figure()

def on_timer():
    print(time.ctime())
    time.sleep(1)

timer = fig.canvas.new_timer(interval=1000)
timer.add_callback(on_timer)
timer.start()
fig.canvas.start_event_loop(10)

macos: 2-seconds
tkagg: 2-seconds
qtagg: 1-second
gtk4agg: 1-second
wxagg: 2-seconds

So I can't say we have a standard right now, or if we should even try to set an expectation of one or the other cases to make things consistent.

Originally posted by @greglucas in #29023 (comment)

Activity

  1. jklymak commented on Oct 28, 2024

    @jklymak
    Member

    You can imagine either behaviour is desired. Certainly it should be the same between backends!

  2. anntzer commented on Oct 29, 2024

    @anntzer
    Contributor

    Timers are internally mostly (only?) used for animations, and if I request a 1fps animation I don't care how much time matplotlib takes to render the canvas, so I would think that requesting a 1s interval should lead to a trigger every second (to the extent where this is possible), even if the callback takes 950ms to execute?

  3. tacaswell commented on Oct 30, 2024

    @tacaswell
    Member

    I'm also in favor of doing our best to hit the requested trigger rate (independent if we spend that time doing stuff or letting the event loop twiddle its thumbs).

  4. QuLogic commented on Oct 31, 2024

    @QuLogic
    Member

    What do you intend to happen when the callback takes longer than the timer duration?

  5. anntzer commented on Oct 31, 2024

    @anntzer
    Contributor

    Then we just run as fast as possible?

  6. QuLogic commented on Oct 31, 2024

    @QuLogic
    Member

    But does the callback overlap or only run once after?

  7. anntzer commented on Oct 31, 2024

    @anntzer
    Contributor

    I think they all run in the same thread, so they can't overlap, can they?

  8. QuLogic commented on Oct 31, 2024

    @QuLogic
    Member

    Sorry, not overlap, but bunch up. Say you have a 1-second timer, and your callback takes a fraction of second, except one time it's doing file I/O or something, and takes 5 seconds. Do you get one call at the sixth second after that one started, or 5? Your "as fast as possible" could be interpreted either way.

    Now, I'm pretty sure all our toolkits say that you will get 1, but if we start fiddling with things, then we should codify that that's what we expect as well.

  9. anntzer commented on Oct 31, 2024

    @anntzer
    Contributor

    Good question. I would probably go for option 1, but it's not something I have properly thought about.

  10. greglucas commented on Oct 31, 2024

    @greglucas
    ContributorAuthor

    Currently all timers are added to the main thread of the respective toolkit, and all work is also scheduled on that thread (unless the callback spawns a separate thread itself). This means that the timer doesn't fire during the long callback, so there is not a buildup of events.

    An example script to demonstrate all of this and the current inconsistencies between backends is here:

    import time
    import matplotlib.pyplot as plt
    
    fig = plt.figure()
    
    orig_time = time.perf_counter()
    
    x = 0
    
    def on_timer():
        global x
        print(f"{x:02d}: {time.perf_counter() - orig_time}")
        if x == 0:
            time.sleep(0.5)
        else:
            time.sleep(0.1)
        x += 1
    
    timer = fig.canvas.new_timer(interval=200)
    timer.add_callback(on_timer)
    timer.start()
    fig.canvas.start_event_loop(2)

    All backends skip forward without any events during the first interval (and without a large emittance of events after that period).

    qt

    00: 0.21131729101762176
    01: 0.7137517910450697
    02: 0.9049600410507992
    03: 1.0955628330120817
    04: 1.2855080000590533
    05: 1.4748726249672472
    06: 1.6652399160666391
    07: 1.8556103330338374
    

    tk

    00: 0.2004480001050979
    01: 0.903072000015527
    02: 1.2058004171121866
    03: 1.5083235841011629
    04: 1.810946417041123
    

    macosx

    00: 0.20077033306006342
    01: 0.8008098329883069
    02: 1.0011580830905586
    03: 1.2011711250524968
    04: 1.4011293749790639
    05: 1.6004785409895703
    06: 1.8011370829772204
    07: 2.0011435829801485
    

    gtk4

    00: 0.2083017498953268
    01: 0.7096816659905016
    02: 0.9115212078904733
    03: 1.112383124884218
    04: 1.3139261659234762
    05: 1.5162364579737186
    06: 1.7181554159615189
    07: 1.9188694159965962
    08: 2.1189793329685926
    09: 2.3196491659618914
    10: 2.530244749970734
    11: 2.731236832914874
    12: 2.9332209998974577
    13: 3.134136249893345
    14: 3.3347702079918236
    15: 3.5365237499354407
    16: 3.737358415964991
    17: 3.938122790888883
    18: 4.138816708000377
    19: 4.339410665910691
    20: 4.5408435829449445
    21: 4.742604082915932
    

    wxagg

    00: 0.20114350004587322
    01: 0.8007950000464916
    02: 1.0011488751042634
    03: 1.2011583750136197
    04: 1.4002930410206318
    05: 1.6009382910560817
    06: 1.8011555830016732
    07: 2.001155333011411
    

    I have a few separate PRs up making these more consistent, but maybe I should merge them all into a single PR so it is easier to review since there are multiple overlapping issues that need to be fixed in the above examples.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions