Page MenuHome

Python API: bpy.app.timers (Python only)
AbandonedPublic

Authored by Jacques Lucke (JacquesLucke) on Sun, Nov 25, 6:53 PM.

Details

Summary

This implements a new timers submodule in bpy.app.
Currently two operations are supported:

  • add(function: Callable[[], Union[None, float]], *, first_interval: float = 0): This will register a new timer. Does someone have a better name for the second parameter? first_interval is zero by default to avoid unnecessary duplication of information. E.g. when you want your function to be executed once every minute, it should not be necessary to write 60 in two places.

    The callback function is expected to return None or float. If None is returned or an exception is raised or some invalid type is returned, the timer will be unregistered. If float is returned, the function will be executed again in that many seconds. If the number is <= 0, the function will be executed in the next main loop iteration.
  • remove(function): Remove the timer. Throws an exception, if the function is currently not registered.

Currently it takes O(n) time to remove one timer, where n is the number of registered functions.
This can be problematic if someone registers thousands of timers and wants to remove
them all in a short period of time. However, it felt like making handling this case
better is not worth the complexity right now. I might add this later.

Diff Detail

Repository
rB Blender
Branch
timers (branched from blender2.8)
Build Status
Buildable 2541
Build 2541: arc lint + arc unit

Event Timeline

  • make removal more robust

In general this is OK, some notes:

  • We should support persistent timers in the same way we have persistent handlers (so loading a file doesn't clear these handlers).
  • Since this wraps a list, there is no way to:
    • Check if a function has been added (unless you know the secret attr)
    • Re-order timers (or at least add to the front of the list).

      It would be possible to hack the API to support these features but it wouldn't be pretty.

      We could add add(..., head=True) argument, as we have for keymap items, re-ordering can probably be ignored since it's not typical usage.

There is potential for optimizations here, it's not that great that it's having to do Python attr lookups every tick, even when there is nothing to do.

While not needed for initial working version, think this should eventually store a queue where only the next item to run needs to be checked against the timer.


Long term I think we should use a generic C API, but don't think its a requirement for this API being added.

As far as I can see there is no reason it needs to be Python spesific. It might even be good to replace wmTimer for events such as auto-save.

Then Python would wrap this, which should be straightforward with the current API in this patch.

source/blender/python/intern/bpy_app_timers.c
38

We could reuse _next_invoke_time for this, set it to None to remove?

41

We could check PyList_GET_SIZE(timers) before locking the GIL. Then in cases when there are no timers, could early exit and not add overhead (this is what bpy_app_generic_callback does).

67

Wouldn't bother checking numeric types (it's too picky), Py convention is to use:

value = PyFloat_AsDouble(pyvalue);
if ((value == -1.0f && PyErr_Occurred()) == 0) {
    /* Handle error. */
}

This way all types that implement number methods can be used.

76

Error message should be more informative:

eg:

Error: 'bpy.app.timers' callback 'module_name.some_fn' returned a 'some_type' expected None or a float.
114

typo (preassign).

138

This should raise an error if the function has already been added, since it's not going to work properly and it will give confusing results. Just check if the attribute exists, removing must delete the attr.

169–171

Wonder if we should warn/error of this is already tagged for removal, nearly all container types behave this way, yet in this case it's not needed because internally it's tagging.

It may be that in the future removal isn't a tag, so to future proof and to act in a predictable way, we shouldn't allow multiple remove calls.

Campbell Barton (campbellbarton) requested changes to this revision.Sun, Nov 25, 10:45 PM
This revision now requires changes to proceed.Sun, Nov 25, 10:45 PM
source/blender/python/intern/bpy_app_timers.c
38

On second thoughts, deleting the attribute can be a tag for removal.

Will implement the "persistent" functionality tomorrow.

I really wonder in what case reordering or adding it to the head of the list is necessary. It does not really make sense to me in this context.... If an addon really needs it, it should just register one timer and do everything in the right order inside of this timer..

Also the problem with adding to the head is that it makes it less robust. This is because adding to the beginning of the list moves everything else and therefore invalidates the current index when timers are added inside of another timer. Currently new timers can only be added to the end+only the current timer at the current index is removed, both of these things together should make a conflict impossible.

Checking if a function is already added could easily be implemented, but it doesn't feel necessary to me.

For optimization: Some kind of priority queue where the highest priority is the first callback that should be called would be nice. But it is also not worth the complexity for now..

And yes, I also don't see any reason why timers should be python specific. I never did something with wmTimer, why should it be replaced?

On a second thought: Maybe it would be easier if I just make this a generic c api directly tomorrow. Should be much cleaner than having this weird C and Python mix.
If you say that is ok, I'll work on it tommorow and hopefully get if ready for review.
I'm just not sure if this is the best approach if I want to get it into the beta so that I can use the api for my other addon project which doesn't really work without it... Or is it too late already in any case, what do you think?

re: adding to head, if a timer runs continuously - it may want to get the state before or after other changes have been made.

Ideally these kinds of inter-dependencies can be avoided
however scripts may not function properly if they read the state based on changes which haven't been evaluated. (think multiple timers from different add-ons running at once).

We can leave it out initially and see if it becomes a pain point.


Checking if a function is already added could easily be implemented, but it doesn't feel necessary to me.

Why not? It's just an attribute check and will avoid some otherwise hard to detect behavior.


Re: optimization, even ignoring the gains of a priority queue, every timer that may run occasionally is continuously doing byte/unicode conversion (see PyObject_GetAttrString), small allocations. Not urgent - but we could avoid it, and will likely need to avoid it once this is a C-API.


re: wmTimer, it's hard to say at this point if we should replace them with a more generic C-API. Only mentioning it since we already have a timer API and we should avoid duplicate functionality where possible (the way wmTimer's are handled in the event-loop makes them useful for modal-operators, so we most likely wouldn't replace usage there).

+1 for making a C-API for timers and the Py API use this.

What are the intended use cases for these timers? How does it relate to the timers in D3977: Python API: callback_utils? When would you use these instead of wmTimer, or is it just a Python equivalent?

As for intended use-case, Python developers often used scene_update_pre as an always handler, for things which didn't need to run all the time. An example would be, an operator executes a process which needs to monitored, once it's finished a region can be tagged to redraw, and the timer removed.

Lots of these kinds of operations don't need to run continuously, so using a timer is a handy way to avoid running something all the time when it's not needed.

I'm told this is useful for Python async too, although I havn't used this myself.


I assumed this patch makes D3977 redundant, as for wmTimer

looking into this now and we might be able to use them so Python timers use these internally instead of having a separate C-API.

We'd probably have a new timer type (similar to TIMERJOBS / TIMERAUTOSAVE) which don't send wmEvent's to operators.

We might want these timers not to interfere with the sleeping though (if timers run we don't do the PIL_sleep_ms, see: wm_window_process_events, which could be abused if Py devs make their timers run continuously - using 100% CPU always). Or, timers that run with zero delay could be detected so they don't interfere with blenders *rest* state.

Otherwise think it could go OK. Although there may be some down-sides, eg: their association with a window (looks like we could keep that NULL but currently we don't do this). or processing ghost events before timers.

Jacques Lucke (JacquesLucke) retitled this revision from Python API: bpy.app.timers to Python API: bpy.app.timers (Python only).Mon, Nov 26, 2:43 PM