Skip to content

Instantly share code, notes, and snippets.

@Adlai-Holler
Last active June 12, 2023 06:23
Show Gist options
  • Save Adlai-Holler/91a3ba2388b6ada50647db97c23d2f02 to your computer and use it in GitHub Desktop.
Save Adlai-Holler/91a3ba2388b6ada50647db97c23d2f02 to your computer and use it in GitHub Desktop.
How to observe CoreAnimation transaction commits as activities. Covers run-loop commits and UIKit-direct-flush commits, but not CADisplayLink scroll view commits.
/**
* Don't use this in production!
*
* Screenshot of it working: https://user-images.githubusercontent.com/2466893/27601666-f65159f0-5b24-11e7-969d-fe86103c21de.png
*/
/**
* This is real, private CA API. Valid as of iOS 10.
*/
typedef enum {
kCATransactionPhasePreLayout,
kCATransactionPhasePreCommit,
kCATransactionPhasePostCommit,
} CATransactionPhase;
@interface CATransaction (Private)
+ (void)addCommitHandler:(void(^)(void))block forPhase:(CATransactionPhase)phase;
@end
@implementation CATransaction (ASDKSwizzles)
+ (void)asdk_flush
{
as_activity_scope(as_activity_create("CA transaction flush", OS_ACTIVITY_CURRENT, OS_ACTIVITY_FLAG_DEFAULT));
[self asdk_flush];
}
@end
+ (void)registerCATransactionObservers
{
// Swizzle +[CATransaction flush] with our +asdk_flush method which embeds an activity.
//
// UIKit sometimes calls [CATransaction flush] to manually commit the implicit transaction.
// See __UIApplicationFlushRunLoopCATransactionIfTooLate.
// In addition there is a manual flush at application launch time.
// Unfortunately, the run-loop-triggered commit does not call +flush, it calls directly into C++
// so there's no easy way to embed an activity that way.
Method origFlush = class_getClassMethod([CATransaction class], @selector(flush));
Method swizFlush = class_getClassMethod([CATransaction class], @selector(asdk_flush));
method_exchangeImplementations(origFlush, swizFlush);
/**
* To use modern activity tracing (the deprecated API doesn't work fully), we have to have control of the execution
* scope of the activity i.e. we have to have a stack-based state struct that survives the entire activity.
*
* So we add a run loop observer just before CA's run loop observer, and from inside that observer, scope the activity
* and spin the run loop
*/
static CFIndex const kCARunLoopObserverOrder = 2000000;
auto o = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopBeforeWaiting, true, kCARunLoopObserverOrder - 1, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
auto commitActivity = as_activity_create("CA transaction commit", OS_ACTIVITY_CURRENT, OS_ACTIVITY_FLAG_DEFAULT);
auto layoutActivity = as_activity_create("CA transaction commit - layout", rootActivity, OS_ACTIVITY_FLAG_DEFAULT);
/**
* NOTE: As of iOS 10, the API for actually getting activity tracing working is very restrictive.
* Even if you store the os_activity_scope_state at a lower level in the same stack, you cannot
* enter that scope, then return, then do some work, then leave the activity scope. So having os_activity_scope_enter
* inside of [CATransaction addCommitHandler:] blocks in order to separate out the commit-layout and commit-postlayout
* phases isn't an option. At the time of this writing, however, you can leave a scope that was started at a lower
* level in the stack and things will actually work so that's what we do.
*/
__block os_activity_scope_state_s commitScopeState = {};
__block os_activity_scope_state_s layoutScopeState = {};
// After the layout, leave the layout scope.
[CATransaction addCommitHandler:^{
as_activity_scope_leave(&layoutScopeState);
} forPhase:kCATransactionPhasePreCommit];
// After the commit, leave the commit scope.
[CATransaction addCommitHandler:^{
as_activity_scope_leave(&commitScopeState);
} forPhase:kCATransactionPhasePostCommit];
/**
* We need to run CA's observer _inside_ this frame for the activity to apply, so we turn the run loop once.
*/
as_activity_scope_enter(commitActivity, &commitScopeState);
as_activity_scope_enter(layoutActivity, &layoutScopeState);
CFRunLoopRunInMode(CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent()), 0, true);
});
CFRunLoopAddObserver(CFRunLoopGetCurrent(), o, kCFRunLoopCommonModes);
CFRelease(o);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment