-
Star
(147)
You must be signed in to star a gist -
Fork
(33)
You must be signed in to fork a gist
-
-
Save clementgenzmer/4ff6c51224089cc65e9b to your computer and use it in GitHub Desktop.
| /* | |
| * This is an example provided by Facebook are for non-commercial testing and | |
| * evaluation purposes only. | |
| * | |
| * Facebook reserves all rights not expressly granted. | |
| * | |
| * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS | |
| * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL | |
| * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN | |
| * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN | |
| * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |
| * | |
| * | |
| * FBAnimationPerformanceTracker | |
| * ----------------------------------------------------------------------- | |
| * | |
| * This class provides animation performance tracking functionality. It basically | |
| * measures the app's frame rate during an operation, and reports this information. | |
| * | |
| * 1) In Foo's designated initializer, construct a tracker object | |
| * | |
| * 2) Add calls to -start and -stop in appropriate places, e.g. for a ScrollView | |
| * | |
| * - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { | |
| * [_apTracker start]; | |
| * } | |
| * | |
| * - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView | |
| * { | |
| * if (!scrollView.dragging) { | |
| * [_apTracker stop]; | |
| * } | |
| * } | |
| * | |
| * - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate { | |
| * if (!decelerate) { | |
| * [_apTracker stop]; | |
| * } | |
| * } | |
| * | |
| * Notes | |
| * ----- | |
| * [] The tracker operates by creating a CADisplayLink object to measure the frame rate of the display | |
| * during start/stop interval. | |
| * | |
| * [] Calls to -stop that were not preceded by a matching call to -start have no effect. | |
| * | |
| * [] 2 calls to -start in a row will trash the data accumulated so far and not log anything. | |
| * | |
| * | |
| * Configuration object for the core tracker | |
| * | |
| * =============================================================================== | |
| * I highly recommend for you to use the standard configuration provided | |
| * These are essentially here so that the computation of the metric is transparent | |
| * and you can feel confident in what the numbers mean. | |
| * =============================================================================== | |
| */ | |
| struct FBAnimationPerformanceTrackerConfig | |
| { | |
| // Number of frame drop that defines a "small" drop event. By default, 1. | |
| NSInteger smallDropEventFrameNumber; | |
| // Number of frame drop that defines a "large" drop event. By default, 4. | |
| NSInteger largeDropEventFrameNumber; | |
| // Number of maximum frame drops to which the drop will be trimmed down to. Currently 15. | |
| NSInteger maxFrameDropAccount; | |
| // If YES, will report stack traces | |
| BOOL reportStackTraces; | |
| }; | |
| typedef struct FBAnimationPerformanceTrackerConfig FBAnimationPerformanceTrackerConfig; | |
| @protocol FBAnimationPerformanceTrackerDelegate <NSObject> | |
| /** | |
| * Core Metric | |
| * | |
| * You are responsible for the aggregation of these metrics (it being on the client or the server). I recommend to implement both | |
| * to limit the payload you are sending to the server. | |
| * | |
| * The final recommended metric being: - SUM(duration) / SUM(smallDropEvent) aka the number of seconds between one frame drop or more | |
| * - SUM(duration) / SUM(largeDropEvent) aka the number of seconds between four frame drops or more | |
| * | |
| * The first metric will tell you how smooth is your scroll view. | |
| * The second metric will tell you how clowny your scroll view can get. | |
| * | |
| * Every time stop is called, this event will fire reporting the performance. | |
| * | |
| * NOTE on this metric: | |
| * - It has been tested at scale on many Facebook apps. | |
| * - It follows the curves of devices. | |
| * - You will need about 100K calls for the number to converge. | |
| * - It is perfectly correlated to X = Percentage of time spent at 60fps. Number of seconds between one frame drop = 1 / ( 1 - Time spent at 60 fps) | |
| * - We report fraction of drops. 7 frame drop = 1.75 of a large frame drop if a large drop is 4 frame drop. | |
| * This is to preserve the correlation mentionned above. | |
| */ | |
| - (void)reportDurationInMS:(NSInteger)duration smallDropEvent:(double)smallDropEvent largeDropEvent:(double)largeDropEvent; | |
| /** | |
| * Stack traces | |
| * | |
| * Dark magic of the animation tracker. In case of a frame drop, this will return a stack trace. | |
| * This will NOT be reported on the main-thread, but off-main thread to save a few CPU cycles. | |
| * | |
| * The slide is constant value that needs to be reported with the stack for processing. | |
| * This currently only allows for symbolication of your own image. | |
| * | |
| * Future work includes symbolicating all modules. I personnaly find it usually | |
| * good enough to know the name of the module. | |
| * | |
| * The stack will have the following format: | |
| * Foundation:0x123|MyApp:0x234|MyApp:0x345| | |
| * | |
| * The slide will have the following format: | |
| * 0x456 | |
| */ | |
| - (void)reportStackTrace:(NSString *)stack withSlide:(NSString *)slide; | |
| @end | |
| @interface FBAnimationPerformanceTracker : NSObject | |
| - (instancetype)initWithConfig:(FBAnimationPerformanceTrackerConfig)config; | |
| + (FBAnimationPerformanceTrackerConfig)standardConfig; | |
| @property (weak, nonatomic, readwrite) id<FBAnimationPerformanceTrackerDelegate> delegate; | |
| - (void)start; | |
| - (void)stop; | |
| @end | |
| #import "FBAnimationPerformanceTracker.h" | |
| #import <dlfcn.h> | |
| #import <map> | |
| #import <pthread.h> | |
| #import <QuartzCore/CADisplayLink.h> | |
| #import <mach-o/dyld.h> | |
| #import "execinfo.h" | |
| #include <mach/mach_time.h> | |
| static BOOL _signalSetup; | |
| static pthread_t _mainThread; | |
| static NSThread *_trackerThread; | |
| static std::map<void *, NSString *, std::greater<void *>> _imageNames; | |
| #ifdef __LP64__ | |
| typedef mach_header_64 fb_mach_header; | |
| typedef segment_command_64 fb_mach_segment_command; | |
| #define LC_SEGMENT_ARCH LC_SEGMENT_64 | |
| #else | |
| typedef mach_header fb_mach_header; | |
| typedef segment_command fb_mach_segment_command; | |
| #define LC_SEGMENT_ARCH LC_SEGMENT | |
| #endif | |
| static volatile BOOL _scrolling; | |
| pthread_mutex_t _scrollingMutex; | |
| pthread_cond_t _scrollingCondVariable; | |
| dispatch_queue_t _symbolicationQueue; | |
| // We record at most 16 frames since I cap the number of frames dropped measured at 15. | |
| // Past 15, something went very wrong (massive contention, priority inversion, rpc call going wrong...) . | |
| // It will only pollute the data to get more. | |
| static const int callstack_max_number = 16; | |
| static int callstack_i; | |
| static bool callstack_dirty; | |
| static int callstack_size[callstack_max_number]; | |
| static void *callstacks[callstack_max_number][128]; | |
| uint64_t callstack_time_capture; | |
| static void _callstack_signal_handler(int signr, siginfo_t *info, void *secret) | |
| { | |
| // This is run on the main thread every 16 ms or so during scroll. | |
| // Signals are run one by one so there is no risk of concurrency of a signal | |
| // by the same signal. | |
| // The backtrace call is technically signal-safe on Unix-based system | |
| // See: http://www.unix.com/man-page/all/3c/walkcontext/ | |
| // WARNING: this is signal handler, no memory allocation is safe. | |
| // Essentially nothing is safe unless specified it is. | |
| callstack_size[callstack_i] = backtrace(callstacks[callstack_i], 128); | |
| callstack_i = (callstack_i + 1) & (callstack_max_number - 1); // & is a cheap modulo (only works for power of 2) | |
| callstack_dirty = true; | |
| } | |
| @interface FBCallstack : NSObject | |
| @property (nonatomic, readonly, assign) int size; | |
| @property (nonatomic, readonly, assign) void **callstack; | |
| - (instancetype)initWithSize:(int)size callstack:(void *)callstack; | |
| @end | |
| @implementation FBCallstack | |
| - (instancetype)initWithSize:(int)size callstack:(void *)callstack | |
| { | |
| if (self = [super init]) { | |
| _size = size; | |
| _callstack = (void **)malloc(size * sizeof(void *)); | |
| memcpy(_callstack, callstack, size * sizeof(void *)); | |
| } | |
| return self; | |
| } | |
| - (void)dealloc | |
| { | |
| free(_callstack); | |
| } | |
| @end | |
| @implementation FBAnimationPerformanceTracker | |
| { | |
| FBAnimationPerformanceTrackerConfig _config; | |
| BOOL _tracking; | |
| BOOL _firstUpdate; | |
| NSTimeInterval _previousFrameTimestamp; | |
| CADisplayLink *_displayLink; | |
| BOOL _prepared; | |
| // numbers used to track the performance metrics | |
| double _durationTotal; | |
| double _maxFrameTime; | |
| double _smallDrops; | |
| double _largeDrops; | |
| } | |
| - (instancetype)initWithConfig:(FBAnimationPerformanceTrackerConfig)config | |
| { | |
| if (self = [super init]) { | |
| // Stack trace logging is not working well in debug mode | |
| // We don't want the data anyway. So let's bail. | |
| #if defined(DEBUG) | |
| config.reportStackTraces = NO; | |
| #endif | |
| _config = config; | |
| if (config.reportStackTraces) { | |
| [self _setupSignal]; | |
| } | |
| } | |
| return self; | |
| } | |
| + (FBAnimationPerformanceTrackerConfig)standardConfig | |
| { | |
| FBAnimationPerformanceTrackerConfig config = { | |
| .smallDropEventFrameNumber = 1, | |
| .largeDropEventFrameNumber = 4, | |
| .maxFrameDropAccount = 15, | |
| .reportStackTraces = NO, | |
| .reportLegacyMetrics = NO, | |
| }; | |
| return config; | |
| } | |
| + (void)_trackerLoop | |
| { | |
| while (true) { | |
| // If you are confused by this part, | |
| // Check out https://computing.llnl.gov/tutorials/pthreads/#ConditionVariables | |
| // Lock the mutex | |
| pthread_mutex_lock(&_scrollingMutex); | |
| while (!_scrolling) { | |
| // Unlock the mutex and sleep until the conditional variable is signaled | |
| pthread_cond_wait(&_scrollingCondVariable, &_scrollingMutex); | |
| // The conditional variable was signaled, but we need to check _scrolling | |
| // As nothing guarantees that it is still true | |
| } | |
| // _scrolling is true, go ahead and capture traces for a while. | |
| pthread_mutex_unlock(&_scrollingMutex); | |
| // We are scrolling, yay, capture traces | |
| while (_scrolling) { | |
| usleep(16000); | |
| // Here I use SIGPROF which is a signal supposed to be used for profiling | |
| // I haven't stumbled upon any collision so far. | |
| // There is no guarantee that it won't impact the system in unpredicted ways. | |
| // Use wisely. | |
| pthread_kill(_mainThread, SIGPROF); | |
| } | |
| } | |
| } | |
| - (void)_setupSignal | |
| { | |
| if (!_signalSetup) { | |
| // The signal hook should be setup once and only once | |
| _signalSetup = YES; | |
| // I actually don't know if the main thread can die. If it does, well, | |
| // this is not going to work. | |
| // UPDATE 4/2015: on iOS8, it looks like the main-thread never dies, and this pointer is correct | |
| _mainThread = pthread_self(); | |
| callstack_i = 0; | |
| // Setup the signal | |
| struct sigaction sa; | |
| sigfillset(&sa.sa_mask); | |
| sa.sa_flags = SA_SIGINFO; | |
| sa.sa_sigaction = _callstack_signal_handler; | |
| sigaction(SIGPROF, &sa, NULL); | |
| pthread_mutex_init(&_scrollingMutex, NULL); | |
| pthread_cond_init (&_scrollingCondVariable, NULL); | |
| // Setup the signal firing loop | |
| _trackerThread = [[NSThread alloc] initWithTarget:[self class] selector:@selector(_trackerLoop) object:nil]; | |
| // We wanna be higher priority than the main thread | |
| // On iOS8 : this will roughly stick us at priority 61, while the main thread oscillates between 20 and 47 | |
| _trackerThread.threadPriority = 1.0; | |
| [_trackerThread start]; | |
| _symbolicationQueue = dispatch_queue_create("com.facebook.symbolication", DISPATCH_QUEUE_SERIAL); | |
| dispatch_async(_symbolicationQueue, ^(void) {[self _setupSymbolication];}); | |
| } | |
| } | |
| - (void)_setupSymbolication | |
| { | |
| // This extract the starting slide of every module in the app | |
| // This is used to know which module an instruction pointer belongs to. | |
| // These operations is NOT thread-safe according to Apple docs | |
| // Do not call this multiple times | |
| int images = _dyld_image_count(); | |
| for (int i = 0; i < images; i ++) { | |
| intptr_t imageSlide = _dyld_get_image_vmaddr_slide(i); | |
| // Here we extract the module name from the full path | |
| // Typically it looks something like: /path/to/lib/UIKit | |
| // And I just extract UIKit | |
| NSString *fullName = [NSString stringWithUTF8String:_dyld_get_image_name(i)]; | |
| NSRange range = [fullName rangeOfString:@"/" options:NSBackwardsSearch]; | |
| NSUInteger startP = (range.location != NSNotFound) ? range.location + 1 : 0; | |
| NSString *imageName = [fullName substringFromIndex:startP]; | |
| // This is parsing the mach header in order to extract the slide. | |
| // See https://developer.apple.com/library/mac/documentation/DeveloperTools/Conceptual/MachORuntime/index.html | |
| // For the structure of mach headers | |
| fb_mach_header *header = (fb_mach_header*)_dyld_get_image_header(i); | |
| if (!header) { | |
| continue; | |
| } | |
| const struct load_command *cmd = | |
| reinterpret_cast<const struct load_command *>(header + 1); | |
| for (unsigned int c = 0; cmd && (c < header->ncmds); c++) { | |
| if (cmd->cmd == LC_SEGMENT_ARCH) { | |
| const fb_mach_segment_command *seg = | |
| reinterpret_cast<const fb_mach_segment_command *>(cmd); | |
| if (!strcmp(seg->segname, "__TEXT")) { | |
| _imageNames[(void *)(seg->vmaddr + imageSlide)] = imageName; | |
| break; | |
| } | |
| } | |
| cmd = reinterpret_cast<struct load_command*>((char *)cmd + cmd->cmdsize); | |
| } | |
| } | |
| } | |
| - (void)dealloc | |
| { | |
| if (_prepared) { | |
| [self _tearDownCADisplayLink]; | |
| } | |
| } | |
| #pragma mark - Tracking | |
| - (void)start | |
| { | |
| if (!_tracking) { | |
| if ([self prepare]) { | |
| _displayLink.paused = NO; | |
| _tracking = YES; | |
| [self _reset]; | |
| if (_config.reportStackTraces) { | |
| pthread_mutex_lock(&_scrollingMutex); | |
| _scrolling = YES; | |
| // Signal the tracker thread to start firing the signals | |
| pthread_cond_signal(&_scrollingCondVariable); | |
| pthread_mutex_unlock(&_scrollingMutex); | |
| } | |
| } | |
| } | |
| } | |
| - (void)stop | |
| { | |
| if (_tracking) { | |
| _tracking = NO; | |
| _displayLink.paused = YES; | |
| if (_durationTotal > 0) { | |
| [_delegate reportDurationInMS:round(1000.0 * _durationTotal) smallDropEvent:_smallDrops largeDropEvent:_largeDrops]; | |
| if (_config.reportStackTraces) { | |
| pthread_mutex_lock(&_scrollingMutex); | |
| _scrolling = NO; | |
| pthread_mutex_unlock(&_scrollingMutex); | |
| } | |
| } | |
| } | |
| } | |
| - (BOOL)prepare | |
| { | |
| if (_prepared) { | |
| return YES; | |
| } | |
| [self _setUpCADisplayLink]; | |
| _prepared = YES; | |
| return YES; | |
| } | |
| - (void)_setUpCADisplayLink | |
| { | |
| _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_update)]; | |
| [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; | |
| _displayLink.paused = YES; | |
| } | |
| - (void)_tearDownCADisplayLink | |
| { | |
| [_displayLink invalidate]; | |
| _displayLink = nil; | |
| } | |
| - (void)_reset | |
| { | |
| _firstUpdate = YES; | |
| _previousFrameTimestamp = 0.0; | |
| _durationTotal = 0; | |
| _maxFrameTime = 0; | |
| _largeDrops = 0; | |
| _smallDrops = 0; | |
| _histogram = FBAnimationFrameTimeHistogramZero; | |
| } | |
| - (void)_addFrameTime:(NSTimeInterval)actualFrameTime singleFrameTime:(NSTimeInterval)singleFrameTime | |
| { | |
| _maxFrameTime = MAX(actualFrameTime, _maxFrameTime); | |
| NSInteger frameDropped = round(actualFrameTime / singleFrameTime) - 1; | |
| frameDropped = MAX(frameDropped, 0); | |
| // This is to reduce noise. Massive frame drops will just add noise to your data. | |
| frameDropped = MIN(_config.maxFrameDropAccount, frameDropped); | |
| _durationTotal += (frameDropped + 1) * singleFrameTime; | |
| // We account 2 frame drops as 2 small events. This way the metric correlates perfectly with Time at X fps. | |
| _smallDrops += (frameDropped >= _config.smallDropEventFrameNumber) ? ((double) frameDropped) / (double)_config.smallDropEventFrameNumber : 0.0; | |
| _largeDrops += (frameDropped >= _config.largeDropEventFrameNumber) ? ((double) frameDropped) / (double)_config.largeDropEventFrameNumber : 0.0; | |
| if (frameDropped >= 1) { | |
| if (_config.reportStackTraces) { | |
| callstack_dirty = false; | |
| for (int ci = 0; ci <= frameDropped ; ci ++) { | |
| // This is computing the previous indexes | |
| // callstack - 1 - ci takes us back ci frames | |
| // I want a positive number so I add callstack_max_number | |
| // And then just modulo it, with & (callstack_max_number - 1) | |
| int callstackPreviousIndex = ((callstack_i - 1 - ci) + callstack_max_number) & (callstack_max_number - 1); | |
| FBCallstack *callstackCopy = [[FBCallstack alloc] initWithSize:callstack_size[callstackPreviousIndex] callstack:callstacks[callstackPreviousIndex]]; | |
| // Check that in between the beginning and the end of the copy the signal did not fire | |
| if (!callstack_dirty) { | |
| // The copy has been made. We are now fine, let's punt the rest off main-thread. | |
| __weak FBAnimationPerformanceTracker *weakSelf = self; | |
| dispatch_async(_symbolicationQueue, ^(void) { | |
| [weakSelf _reportStackTrace:callstackCopy]; | |
| }); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| - (void)_update | |
| { | |
| if (!_tracking) { | |
| return; | |
| } | |
| if (_firstUpdate) { | |
| _firstUpdate = NO; | |
| _previousFrameTimestamp = _displayLink.timestamp; | |
| return; | |
| } | |
| NSTimeInterval currentTimestamp = _displayLink.timestamp; | |
| NSTimeInterval frameTime = currentTimestamp - _previousFrameTimestamp; | |
| [self _addFrameTime:frameTime singleFrameTime:_displayLink.duration]; | |
| _previousFrameTimestamp = currentTimestamp; | |
| } | |
| - (void)_reportStackTrace:(FBCallstack *)callstack | |
| { | |
| static NSString *slide; | |
| static dispatch_once_t slide_predicate; | |
| dispatch_once(&slide_predicate, ^{ | |
| slide = [NSString stringWithFormat:@"%p", (void *)_dyld_get_image_header(0)]; | |
| }); | |
| @autoreleasepool { | |
| NSMutableString *stack = [NSMutableString string]; | |
| for (int j = 2; j < callstack.size; j ++) { | |
| void *instructionPointer = callstack.callstack[j]; | |
| auto it = _imageNames.lower_bound(instructionPointer); | |
| NSString *imageName = (it != _imageNames.end()) ? it->second : @"???"; | |
| [stack appendString:imageName]; | |
| [stack appendString:@":"]; | |
| [stack appendString:[NSString stringWithFormat:@"%p", instructionPointer]]; | |
| [stack appendString:@"|"]; | |
| } | |
| [_delegate reportStackTrace:stack withSlide:slide]; | |
| } | |
| } | |
| @end |
anyway to include this in a project that doesn't break @import and FOUNDATION_EXPORT. Since it's obj-c++, I guess modules don't work. and i don't really want to change around my whole project just to use this.
@tettoffensive did you ever solve your issue? i'm encountering the same.
i had success getting this to build by changing the objc class that used it from .m to .mm. I had some other issues using the code in this gist after that:
- FBAnimationPerformanceTrackerConfig struct was missing a member for reportLegacyMetrics
- _histogram and FBAnimationFrameTimeHistogramZero were not defined (see _reset method)
For now I commented that out of the _reset function just to get it to build. After building I'm still struggling to figure out how to assemble the results into something meaningful, but I thought I'd post this here in case it helps others.
If anyone has a sample project that uses this to measure their own scrolling behavior I'd love to see it to get a better idea of how to apply this to my situation. I have complex cells that I'm having trouble getting to scroll smoothly. My issues appear to be related to using NSAttributedStrings in text views in the tableview cells.
May I have your email? I have some questions to ask you for advice.