Is it possible to check that main thread is idle / to drain a main run loop?
Asked Answered
H

3

5

I've just read the following post and have tried to implement the approach described there:

Writing iOS acceptance tests using Kiwi - Being Agile

All the stuff described there does work perfectly. But! there is one thing that breaks determinism when I am running my acceptance tests.

Here is the repo on Github where author of the post pushed his experiments (it can be found on the bottom of the page in the comments): https://github.com/moredip/2012-Olympics-iOS--iPad-and-iPhone--source-code/tree/kiwi-acceptance-mk1

Consider this code he uses for tapping a view:

- (void) tapViewViaSelector:(NSString *)viewSelector{
    [UIAutomationBridge tapView:[self viewViaSelector:viewSelector]];
    sleepFor(0.1); //ugh
}

...where sleepFor has the following definition behind itself:

#define sleepFor(interval) (CFRunLoopRunInMode(kCFRunLoopDefaultMode, interval, false))

It is a naive attempt ('naive' is not about the author, but about the fact that it is the first thing that comes into a head) to wait for a tiny period of time until all the animations are processed and soak all the possible events that were(or could be) scheduled to a main run loop (see also this comment).

The problem is that this naive code does not work in a deterministic way. There are a bunches of UI interactions which cause fx next button tap to be pressed before the current edited textfield's keyboard is disappeared and so on...

If I just increase the time from 0.1 to fx 1 all the problems disappear, but this leads to that every single interaction like "fill in textfield with a text..." or "tap button with title..." become to cost One second!

So I don't mean just increasing a wait time here, but rather a way to make such artificial waits guarantee that I do can proceed my test case with a next step.

I hope that it should be a more reliable way to wait enough until all the stuff caused by current action (all the transitions/animations or whatever main run loop stuff) are done.

To summarize it all to be a question:

Is there a way to exhaust/drain/soak all the stuff scheduled to a main thread and its run loop to be sure that main thread is idle and its run loop is "empty"?

This was my initial solution:

// DON'T like it
static inline void runLoopIfNeeded() {
    // https://developer.apple.com/library/mac/#documentation/CoreFOundation/Reference/CFRunLoopRef/Reference/reference.html

    while (CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.1, YES) == kCFRunLoopRunHandledSource);

    // DON'T like it
    if (CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.1, YES) == kCFRunLoopRunHandledSource) runLoopIfNeeded();
}
Heirship answered 6/6, 2013 at 23:15 Comment(0)
H
5

you can try this

while (CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, true) == kCFRunLoopRunHandledSource);

this will run until no more things in the run loop. you can try to change the time interval to 0.1 if 0 is not working.

History answered 7/6, 2013 at 2:28 Comment(2)
thanks for the answer. This was my original solution, before I've created this post. I will post my current one in a minutes. I am really glad we are thinking in one direction!Heirship
I updated my question with my first attempt, so you could catch my original thinking and the way how it evolved (hope so). /cc @xlcHeirship
M
5

To check on the status of a run loop associated with a thread and register callbacks for separate phases, you may use a CFRunLoopObserverRef. This allows for extremely fine grained control over when the callbacks are invoked. Also, you don't have to depend on hacky timeouts and such.

One can be added like so (notice I am adding one to the main run loop)

CFRunLoopObserverRef obs = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, true, 0 /* order */, handler);
CFRunLoopAddObserver([NSRunLoop mainRunLoop].getCFRunLoop, obs, kCFRunLoopCommonModes);
CFRelease(obs);

Depending on the activities you register for, your handler will get invoked appropriately. In the sample above, the observer listens for all activities. You probably only need kCFRunLoopBeforeWaiting

You handler could look like this

id handler = ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
    switch (activity) {
        case kCFRunLoopEntry:
            // About to enter the processing loop. Happens
            // once per `CFRunLoopRun` or `CFRunLoopRunInMode` call
            break;
        case kCFRunLoopBeforeTimers:
        case kCFRunLoopBeforeSources:
            // Happens before timers or sources are about to be handled
            break;
        case kCFRunLoopBeforeWaiting:
            // All timers and sources are handled and loop is about to go
            // to sleep. This is most likely what you are looking for :)
            break;
        case kCFRunLoopAfterWaiting:
            // About to process a timer or source
            break;
        case kCFRunLoopExit:
            // The `CFRunLoopRun` or `CFRunLoopRunInMode` call is about to
            // return
            break;
    }
};
Mora answered 24/10, 2014 at 3:53 Comment(0)
H
1

Here is my current solution, I will add some comments and explanations to the code a bit later, if nobody tell me I am wrong or suggests a better answer first:

// It is much better, than it was, but still unsure
static inline void runLoopIfNeeded() {
    // https://developer.apple.com/library/mac/#documentation/CoreFOundation/Reference/CFRunLoopRef/Reference/reference.html

    __block BOOL flag = NO;

    // https://mcmap.net/q/1320758/-specify-to-call-someting-when-main-thread-is-idle
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
        dispatch_async(dispatch_get_main_queue(), ^{
            flag = YES;
        });
    });

    while (CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.1, YES) == kCFRunLoopRunHandledSource);

    if (flag == NO) runLoopIfNeeded();
}

Right now I don't have any ideas how this could be made more effective.

Heirship answered 7/6, 2013 at 2:55 Comment(4)
I can't see the benefit of the dispatch_async and you should try to make it as a while loop instead of using recursionHistory
The benefit of dispatch_async is to ensure that any blocks dispatched to main queue before runLoopIfNeeded() was called are drained first. Am I wrong in this my reasoning?Heirship
the while loop around CFRunLoopRunInMode should ensure that. The only problem is that some even may have delay and you don't know the delay time. e.g. animation completion handler will be called after the animation duration so you have to wait for animation duration amount of time.History
Yes, I know about these cases. I use special helper eventually() for this purpose, like [[theValue(eventually(^{ return hasLabelWithText(@"Some text"); })) should] beYes]; This eventually() helper is based on runLoopIfNeeded().Heirship

© 2022 - 2024 — McMap. All rights reserved.