Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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();
}
like image 985
Stanislav Pankevich Avatar asked Jun 06 '13 23:06

Stanislav Pankevich


3 Answers

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.

like image 142
Bryan Chen Avatar answered Oct 23 '22 16:10

Bryan Chen


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;
    }
};
like image 29
Buzzy Avatar answered Oct 23 '22 16:10

Buzzy


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;

    // http://stackoverflow.com/questions/7356820/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.

like image 39
Stanislav Pankevich Avatar answered Oct 23 '22 14:10

Stanislav Pankevich