RY 's Blog

Dive into CFRunLoop

2021-02-13

Background

Sometimes, you may want to collect on-device performance metrics in main thread to know how our App performs and help you find more clues to analyse performance issue. MetricKit is a useful utility framework to achieve that. It starts accumulating reports for your app after being called for the first time and delivers reports at most once per day. The reports contain the metrics from the past 24 hours and any previously undelivered daily reports. Then, you can go to Xcode->Organizer->Metric panel to check these info. However, you may want your in-house App Performance Monitoring framework to gain more controls on when to collect metrics, or how to upload it, or collect what you want. Earlier days, Tencent launched a iOS framework called matrix to monitor App performance metrics. When I explored this library, I saw they use CFRunLoop to detect hitch in the thread, like main thread. This intrigued me. So I investigated CFRunLoop to learn more.

What is RunLoop in iOS?

Before talking about RunLoop in iOS, we may have to know something about event loop and thread. In OS/360 Multiprogramming with a Variable Number of Tasks (MVT) in 1967, threads made an early appearance under the name of “tasks”. A thread in computer science is short for a thread of execution. Once the tasks in one Thread is all done, the thread finishes its job and exits. Sometimes, we need a way to keep it alive and handling events. Then, comes the event loop. The psudo code for event loop is like this:

1
2
3
4
5
6
7
function loop
initialize()
while message != quit
message := get_next_message()
process_message(message)
end while
end function

In Wikipedia, event loop is a programming construct or design pattern that waits for and dispatches events or messages in a program. In this event loop, it keeps waiting events -> receive events -> handle events until the exit condition is met.

Run loops are part of the fundamental infrastructure associated with threads. A run loop is an event processing loop that you use to schedule work and coordinate the receipt of incoming events. The purpose of a run loop is to keep your thread busy when there is work to do and put your thread to sleep when there is none.

A CFRunLoop object monitors sources of input to a task and dispatches control when they become ready for processing.

A run loop receives events from two different types of sources. Input sources deliver asynchronous events, usually messages from another thread or from a different application. Timer sources deliver synchronous events, occurring at a scheduled time or repeating interval.

It can handle

  • user input devices

  • port objects

  • network connections

  • periodic or time-delayed events

  • asynchronous callbacks

    image-20210128230441389

In Apple’s doc, this kind of event loop is implemented by CFRunLoop in low-level. In cocoa, the object is an instance of NSRunLoop There is exactly one run loop per thread.

Apple provides two APIs to get runloop object

  • CFRunLoopGetMain() // the main CFRunLoop object
  • CFRunLoopGetCurrent() // CFRunLoop object for the current thread

RunLoop Mode

A run loop mode is a collection of input sources and timers to be monitored and a collection of run loop observers to be notified. Each time you run your run loop, you specify (either explicitly or implicitly) a particular “mode” in which to run.During that pass of the run loop, only sources associated with that mode are monitored and allowed to deliver their events. — doc

A run loop mode contains a set of CFRunLoopSource, a list of CFRunLoopTimer and CFRunLoopObservers. They are all inputs for runloop.

image-20210128223829153

Inputs

Three kinds of inputs can be monitored by a run loop

  • CFRunLoopSource
  • CFRunLoopTimer
  • CFRunLoopObservers

Input Source - CFRunLoopSource

CFRunLoopSource

A CFRunLoopSource object is an abstraction of an input source that can be put into a run loop. Input sources typically generate asynchronous events, such as messages arriving on a network port or actions performed by the user.

1
2
3
4
5
6
7
8
9
10
11
struct __CFRunLoopSource {
CFRuntimeBase _base;
uint32_t _bits;
pthread_mutex_t _lock;
CFIndex _order; /* immutable */
CFMutableBagRef _runLoops;
union {
4 CFRunLoopSourceContext version0; /* immutable, except invalidation */
CFRunLoopSourceContext1 version1; /* immutable, except invalidation */
} _context;
};

_context is a union type. A union looks like a structure, but it will use the memory space for just one of the fields in its definition. So the _context is either an CFRunLoopSourceContext structure or CFRunLoopSourceContext1 structure.

Two categories of Input Source

As it is mentioned in this doc, we mainly care about two categories, port-base input sources, source1, and non-port-based input sources, source0, also called custom sources.

  • Version 0 sources, so named because the version field of their context structure is 0, are managed manually by the application.
    • When a source is ready to fire, some part of the application, perhaps code on a separatethread waiting for an event, must call CFRunLoopSourceSignal(_:) to tell the run loop that the source is ready to fire.
    • custom input source that allows you to perform a selector on any thread. Like performSelectorOnMainThread:withObject:waitUntilDone: , performSelector:withObject:afterDelay:. see more
    • Defining a Custom Input Source
  • Version 1 sources are managed by the run loop and kernel.
    • These sources use Mach ports to signal when the sources are ready to fire.
    • A source is automatically signaled by the kernel when a message arrives on the source’s Mach port.
    • see Configuring a Port-Based Input Source
      image-20210125183432020

bits field

It seems that bits field is used to mark the status of the CFRunLoopSouceRef .

1
2
3
4
5
6
7
8
9
10
11
CF_INLINE Boolean __CFRunLoopSourceIsSignaled(CFRunLoopSourceRef rls) {
return (Boolean)__CFBitfieldGetValue(rls->_bits, 1, 1);
}

CF_INLINE void __CFRunLoopSourceSetSignaled(CFRunLoopSourceRef rls) {
__CFBitfieldSetValue(rls->_bits, 1, 1, 1);
}

CF_INLINE void __CFRunLoopSourceUnsetSignaled(CFRunLoopSourceRef rls) {
__CFBitfieldSetValue(rls->_bits, 1, 1, 0);
}

CFRunLoopSourceSignal is used to signals a version 0 source , marking it as ready to fire. It actually updated the bits in the CFRunLoopSouceRef structure.

1
2
3
4
5
6
7
8
void CFRunLoopSourceSignal(CFRunLoopSourceRef rls) {
CHECK_FOR_FORK();
__CFRunLoopSourceLock(rls);
if (__CFIsValid(rls)) {
4__CFRunLoopSourceSetSignaled(rls);
}
__CFRunLoopSourceUnlock(rls);
}

CFRunLoopTimer

Besides CFRunLoopSource, there is another input for runloop. Timer Source, CFRunLoopTimer, which represents a specialized run loop source that fires at a preset time in the future. see doc for CFRunLoopTimer

There are two conditions for a timer to be fired:

  • one of the run loop modes to which the timer has been added is running
  • the timer’s firing time has passed

a timer is not a real-time mechanism

  1. Like input sources, timers are associated with specific modes of your run loop. If a timer is not in the mode currently being monitored by the run loop, it does not fire until you run the run loop in one of the timer’s supported modes.
  2. If a timer fires when the run loop is in the middle of executing a handler routine, the timer waits until the next time through the run loop to invoke its handler routine.
  3. If the run loop is not running at all, the timer never fires.

In Cocoa, you can create and schedule a timer all at once using either of these class methods:

1
2
scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:
scheduledTimerWithTimeInterval:invocation:repeats:

You can also create your NSTimer object and then add it to the run loop using the addTimer:forMode: method of NSRunLoop.
see how to configure timer source here

RunLoopObserver

How to use observer

  1. We can use these two APIs to create RunLoopObserver and associated it with handlers.
  • CFRunLoopObserverCreate(_:_:_:_:_:_:)
  • CFRunLoopObserverCreateWithHandler(_:_:_:_:_:)
  1. add the observer into the runloop

    1
    2
    3
    4
    5
    6

    CFRunLoopObserverRef runloopObserver = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopBeforeWaiting, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
    // handler code here
    });

    CFRunLoopAddObserver(CFRunLoopGetMain(), runloopObserver, kCFRunLoopDefaultMode);
  2. Observe specific RunLoop Activity

RunLoop Activity

The run loop stages in which an observer is scheduled are selected when the observer is created with CFRunLoopObserverCreate. -doc

There are several kinds of RunLoop Activity for CFRunLoop. You can associate run loop observers with these RunLoopActivity

1
2
3
4
5
6
7
8
9
10
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // The entrance of the run loop, before entering the event processing loop. This activity occurs once for each call to CFRunLoopRun() and CFRunLoopRunInMode(_:_:_:).
kCFRunLoopBeforeTimers = (1UL << 1), // Inside the event processing loop before any timers are processed.
kCFRunLoopBeforeSources = (1UL << 2), // Inside the event processing loop before any sources are processed.
kCFRunLoopBeforeWaiting = (1UL << 5),
kCFRunLoopAfterWaiting = (1UL << 6), // Inside the event processing loop after the run loop wakes up, but before processing the event that woke it up. This activity occurs only if the run loop did in fact go to sleep during the current loop.
kCFRunLoopExit = (1UL << 7), // The exit of the run loop, after exiting the event processing loop. This activity occurs once for each call to CFRunLoopRun() and CFRunLoopRunInMode(_:_:_:).
kCFRunLoopAllActivities = 0x0FFFFFFFU
};

https://developer.apple.com/documentation/corefoundation/cfrunloopactivity

Run Loop Sequence of Events

According to apple doc, when runloop running in a thread, it processes pending events and generates notifications for attached observers. Briefly, it works as the follow diagram shows.

image-20210209142244931

The implementation is in CFRunLoopRunSpecific and __CFRunLoopRun in CFRunloop.c .

Use case

Detect hitch block in main thread

In Tencent matrix, it leverages the Run Loop notifications to record timestamp when these notifications sent.

  1. create and add RunLoopObserver to current RunLoop CFRunLoopAddObserver

  2. record timestamp in callback function invoked when the observer runs

  3. start a monitor thread, then check periodically
  4. get thetimestamp diff to see if it is greater than threshold, g_RunLoopTimeOut

I did a small experiment to better understand it. I added a RunLoop Observer to the runloop in main thread. Then calculate the time gap between kCFRunLoopBeforeTimers notification in two continuous loop.

1
2
3
4
// 1. create runloop observer 
CFRunLoopObserverRef beginObserver = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, LONG_MIN, &myRunLoopCallback, &context);
// 2. add observer to runloop in the main thread
CFRunLoopAddObserver([[NSRunLoop mainRunLoop] getCFRunLoop], beginObserver, kCFRunLoopCommonModes);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 3. implemented callback for RunLoop observer 
static void myRunLoopCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
switch (activity) {
case kCFRunLoopEntry:
self.isRunloopRunning = YES;
break;
case kCFRunLoopBeforeTimers:
NSLog(@"[RY]kCFRunLoopBeforeTimers called %@", @(getCurrentMilliTimestamp() - monitor.runloopMilliTimestamp));
self.runloopMilliTimestamp = getCurrentMilliTimestamp();
self.isRunloopRunning = YES;
break;
case kCFRunLoopBeforeSources:
self.isRunloopRunning = YES;
break;
case kCFRunLoopBeforeWaiting:
self.isRunloopRunning = NO;
break;
case kCFRunLoopAfterWaiting:
self.isRunloopRunning = YES;
break;
case kCFRunLoopExit:
self.isRunloopRunning = NO;
break;b
default:
break;
}
}

Theoretically, the time diff between two continuous kCFRunLoopBeforeTimers notification should be within 16.67ms to achieve smooth user experience in main thread, which means RunLoop runs 60 times per second. In the following log, one frame takes about 72ms to executed.
However, since I put the logger in kCFRunLoopBeforeTimers, this may not be a hitch. It could be caused thread was sleeping while there is no event come.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 [RY]kCFRunLoopBeforeTimers called 3.628173828125
[RY]kCFRunLoopBeforeTimers called 17.784912109375
[RY]kCFRunLoopBeforeTimers called 0.041015625
[RY]kCFRunLoopBeforeTimers called 1.23388671875
[RY]kCFRunLoopBeforeTimers called 72.05419921875
[RY]kCFRunLoopBeforeTimers called 5.138916015625
[RY]kCFRunLoopBeforeTimers called 0.072021484375
ers called 1.296875
[RY]kCFRunLoopBeforeTimers called 0.070068359375
[RY]kCFRunLoopBeforeTimers called 0.035888671875
[RY]kCFRunLoopBeforeTimers called 0.051025390625
[RY]kCFRunLoopBeforeTimers called 0.057861328125
[RY]kCFRunLoopBeforeTimers called 0.01806640625
[RY]kCFRunLoopBeforeTimers called 0.260009765625
[RY]kCFRunLoopBeforeTimers called 0.03515625
[RY]kCFRunLoopBeforeTimers called 0.43212890625

Runloop in React Native

make JSThread long-lived

enqueue a block object on a given runloop

See More

scan qr code and share this article