Progress indicator (mw.progress.start) in .45+ / basic printing support

to show that some longer action was running you could use some code like this in the past:

mw.progress.start()
# some action
mw.progress.finish()

see e.g. Damien’s Basic Printing Support add-on, source.

Up to version .44 this code in an add-on gave you a spinning wheel while the action was running, after .45+ this rotating wheel is no longer shown so that there’s no feedback that the action is running.

How would you fix the code for .45+? Is there another add-on that actually shows the rotating wheel during longer running actions on versions .45+?

I think you’re supposed to use QueryOp or CollectionOp now.
Also see Add-on porting notes for Anki 2.1.45.

3 Likes

I added some notes about this to the docs today:

https://addon-docs.ankiweb.net/background-ops.html

3 Likes

(the fact that direct calls worked in the past was due to ugly hacks that updated the UI when progress.update() or the DB progress hook was called - these resulted in choppy updates, and could lead to crashes)

1 Like

dae: Thank you very much. Your step-by-step is very useful for someone like me.

Rumo: I could/should have thought of checking this …

@dae Could you add some documentation on how to update the progress bar in this case?

Also I think there is some errors in the documentation code. The function is name “my_background_op” in the def but “my_background_operation” elsewhere. Also the loop variable is called id, but then the loop uses “note_id”.

def **my_background_op**(col: Collection, note_ids: list[int]) -> int:
    # some long-running op, eg
    for **id** in note_ids:
        note = col.get_note(**note_id**)
        # ...

    return 123

def on_success(count: int) -> None:
    showInfo(f"my_background_op() returned {count}")

def my_ui_action(note_ids: list[int]):
    op = QueryOp(
        # the active window (main window in this case)
        parent=mw,
        # the operation is passed the collection for convenience; you can
        # ignore it if you wish
        op=lambda col: **my_background_operation**(col, note_ids),
        # this function will be called if op completes successfully,
        # and it is given the return value of the op
        success=on_success,
    )

    # if with_progress() is not called, no progress window will be shown.
    # note: QueryOp.with_progress() was broken until Anki 2.1.50
    op.with_progress().run_in_background()

Thank you, I’ve fixed the incorrect function name. That page has an example of how progress can be updated from within the op - see the “run_on_main” part.

1 Like

Hi there,

I’ve been checking the docs and the source and just want to understand in this thread – if I’m using QueryOp, the UI shouldn’t lock up with a spinner and the user should be able to do whatever at that time, right? And if I call .with_progress() the user should be able to see a progress bar of some sorts?

Right now, my UI locks up with a spinner. I’m running some image scrapers in the background, and I’m launching one query op per note. It looks something like this:

jobs = []
processed_notes = set()
updated_notes: List[Note] = []
def on_success(updated_result):
        processed_notes.add(updated_result.note_id)
        updated_notes.append(apply_result_to_note(updated_result))

for _, note_id in enumerate(note_ids, 1):
        note = mw.col.get_note(note_id)
        op = QueryOp(
                    parent=mw,
                    op=lambda _: BingScraper().launch_scrape_job(config),
                    success=on_success)
        op.with_progress().run_in_background()

Per the docs, I’ve double checked that BingScraper() has nothing side-effecting the UI. It sends requests, does some image processing, etc but the module using that scraper is doing nothing related to Qt. If I understood correctly, I must be accidentally blocking the main thread somehow – any advice on what to look out for in this case, or am I using QueryOp incorrectly here?

apply_result_to_note doesn’t persist anything to the database – the notes are collected and sent to update_notes later (I’ve checked that that’s not the part it’s blocking on).

With the snippet below (in Anki’s REPL) you can see that the behaviour is exactly like you described.
I would incrementally add your logic to it until it stops working.

from aqt.operations import QueryOp   

def sleep(_): 
    import time 
    time.sleep(3)  

for i in range(10):
         op = QueryOp(parent=mw, op=sleep, success=lambda _, i=i: print(i))
         op.with_progress().run_in_background()  
1 Like

There is some overhead when running a background op, so you typically want to make an operation that does all the work in one go, instead of creating lots of separate operations for each id. If you want bounded parallelism (eg 4 network requests in flight), then you’ll need to roll your own solution.

2 Likes

Yeah, perhaps it’s an issue of volume, in this case I was kicking off about 1 op per card and testing it on anywhere from 30-50 cards. With the on_success callback it shouldn’t be too hard to implement a loop or some type of queue to throttle the rate of the ops.

Thanks a lot for the fast response.

While of course it’s wasteful to create more ops than is necessary for your use case, I don’t understand how this would block the main thread. :thinking: And your 30-50 queries are committed to a threadpool, so it’s not as if they would actually all run in parallel.