/** * Copyright (c) 2015-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ #import "RCTDefines.h" #if RCT_DEV #import #import #import "RCTBridge.h" #import "RCTDevMenu.h" #import "RCTFPSGraph.h" #import "RCTInvalidating.h" #import "RCTJavaScriptExecutor.h" #import "RCTRootView.h" #import "RCTSparseArray.h" #import "RCTUIManager.h" static NSString *const RCTPerfMonitorKey = @"RCTPerfMonitorKey"; typedef BOOL (*RCTJSCSetOptionType)(const char *); static BOOL RCTJSCSetOption(const char *option) { static RCTJSCSetOptionType setOption; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ setOption = dlsym(RTLD_DEFAULT, "_ZN3JSC7Options9setOptionEPKc"); setOption("logGC=1"); }); if (setOption) { return setOption(option); } else { return NO; } } static vm_size_t RCTGetResidentMemorySize(void) { struct task_basic_info info; mach_msg_type_number_t size = sizeof(info); kern_return_t kerr = task_info(mach_task_self(), TASK_BASIC_INFO, (task_info_t)&info, &size); if (kerr != KERN_SUCCESS) { return 0; } return info.resident_size; } @class RCTDevMenuItem; @interface RCTPerfMonitor : NSObject @property (nonatomic, strong, readonly) RCTDevMenuItem *devMenuItem; @property (nonatomic, strong, readonly) UIPanGestureRecognizer *gestureRecognizer; @property (nonatomic, strong, readonly) UIView *container; @property (nonatomic, strong, readonly) UILabel *memory; @property (nonatomic, strong, readonly) UILabel *heap; @property (nonatomic, strong, readonly) UILabel *views; @property (nonatomic, strong, readonly) RCTFPSGraph *jsGraph; @property (nonatomic, strong, readonly) RCTFPSGraph *uiGraph; @property (nonatomic, strong, readonly) UILabel *jsGraphLabel; @property (nonatomic, strong, readonly) UILabel *uiGraphLabel; @end @implementation RCTPerfMonitor { RCTDevMenuItem *_devMenuItem; UIPanGestureRecognizer *_gestureRecognizer; UIView *_container; UILabel *_memory; UILabel *_heap; UILabel *_views; UILabel *_uiGraphLabel; UILabel *_jsGraphLabel; RCTFPSGraph *_uiGraph; RCTFPSGraph *_jsGraph; CADisplayLink *_uiDisplayLink; CADisplayLink *_jsDisplayLink; NSUInteger _heapSize; dispatch_queue_t _queue; dispatch_io_t _io; int _stderr; int _pipe[2]; NSString *_remaining; } @synthesize bridge = _bridge; RCT_EXPORT_MODULE() - (void)invalidate { [self hide]; } - (RCTDevMenuItem *)devMenuItem { if (!_devMenuItem) { __weak __typeof__(self) weakSelf = self; _devMenuItem = [RCTDevMenuItem toggleItemWithKey:RCTPerfMonitorKey title:@"Show Monitor" selectedTitle:@"Hide Monitor" handler: ^(BOOL selected) { if (selected) { [weakSelf show]; } else { [weakSelf hide]; } }]; } return _devMenuItem; } - (UIPanGestureRecognizer *)gestureRecognizer { if (!_gestureRecognizer) { _gestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(gesture:)]; } return _gestureRecognizer; } - (UIView *)container { if (!_container) { _container = [[UIView alloc] initWithFrame:CGRectMake(10, 25, 180, 50)]; _container.backgroundColor = UIColor.whiteColor; _container.layer.borderWidth = 2; _container.layer.borderColor = [UIColor grayColor].CGColor; [_container addGestureRecognizer:self.gestureRecognizer]; } return _container; } - (UILabel *)memory { if (!_memory) { _memory = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 44, 50)]; _memory.font = [UIFont systemFontOfSize:12]; _memory.numberOfLines = 3; _memory.textAlignment = NSTextAlignmentCenter; } return _memory; } - (UILabel *)heap { if (!_heap) { _heap = [[UILabel alloc] initWithFrame:CGRectMake(44, 0, 44, 50)]; _heap.font = [UIFont systemFontOfSize:12]; _heap.numberOfLines = 3; _heap.textAlignment = NSTextAlignmentCenter; } return _heap; } - (UILabel *)views { if (!_views) { _views = [[UILabel alloc] initWithFrame:CGRectMake(88, 0, 44, 50)]; _views.font = [UIFont systemFontOfSize:12]; _views.numberOfLines = 3; _views.textAlignment = NSTextAlignmentCenter; } return _views; } - (RCTFPSGraph *)uiGraph { if (!_uiGraph) { _uiGraph = [[RCTFPSGraph alloc] initWithFrame:CGRectMake(134, 14, 40, 30) color:[UIColor lightGrayColor]]; } return _uiGraph; } - (RCTFPSGraph *)jsGraph { if (!_jsGraph) { _jsGraph = [[RCTFPSGraph alloc] initWithFrame:CGRectMake(178, 14, 40, 30) color:[UIColor lightGrayColor]]; } return _jsGraph; } - (UILabel *)uiGraphLabel { if (!_uiGraphLabel) { _uiGraphLabel = [[UILabel alloc] initWithFrame:CGRectMake(134, 3, 40, 10)]; _uiGraphLabel.font = [UIFont systemFontOfSize:11]; _uiGraphLabel.textAlignment = NSTextAlignmentCenter; _uiGraphLabel.text = @"UI"; } return _uiGraphLabel; } - (UILabel *)jsGraphLabel { if (!_jsGraphLabel) { _jsGraphLabel = [[UILabel alloc] initWithFrame:CGRectMake(178, 3, 38, 10)]; _jsGraphLabel.font = [UIFont systemFontOfSize:11]; _jsGraphLabel.textAlignment = NSTextAlignmentCenter; _jsGraphLabel.text = @"JS"; } return _jsGraphLabel; } - (dispatch_queue_t)methodQueue { return dispatch_get_main_queue(); } - (void)setBridge:(RCTBridge *)bridge { _bridge = bridge; [_bridge.devMenu addItem:self.devMenuItem]; } - (void)show { if (_container) { return; } [self.container addSubview:self.memory]; [self.container addSubview:self.heap]; [self.container addSubview:self.views]; [self.container addSubview:self.uiGraph]; [self.container addSubview:self.uiGraphLabel]; [self redirectLogs]; RCTJSCSetOption("logGC=1"); [self updateStats]; UIWindow *window = [UIApplication sharedApplication].delegate.window; [window addSubview:self.container]; _uiDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(threadUpdate:)]; [_uiDisplayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; id executor = [_bridge valueForKey:@"javaScriptExecutor"]; if ([executor isKindOfClass:NSClassFromString(@"RCTContextExecutor")]) { self.container.frame = (CGRect) { self.container.frame.origin, { self.container.frame.size.width + 44, self.container.frame.size.height } }; [self.container addSubview:self.jsGraph]; [self.container addSubview:self.jsGraphLabel]; [executor executeBlockOnJavaScriptQueue:^{ _jsDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(threadUpdate:)]; [_jsDisplayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; }]; } } - (void)hide { if (!_container) { return; } [self.container removeFromSuperview]; _container = nil; _jsGraph = nil; _uiGraph = nil; RCTJSCSetOption("logGC=0"); [self stopLogs]; [_uiDisplayLink invalidate]; [_jsDisplayLink invalidate]; _uiDisplayLink = nil; _jsDisplayLink = nil; } - (void)redirectLogs { _stderr = dup(STDERR_FILENO); if (pipe(_pipe) != 0) { return; } dup2(_pipe[1], STDERR_FILENO); close(_pipe[1]); __weak __typeof__(self) weakSelf = self; _queue = dispatch_queue_create("com.facebook.react.RCTPerfMonitor", DISPATCH_QUEUE_SERIAL); _io = dispatch_io_create( DISPATCH_IO_STREAM, _pipe[0], _queue, ^(__unused int error) {}); dispatch_io_set_low_water(_io, 20); dispatch_io_read( _io, 0, SIZE_MAX, _queue, ^(__unused bool done, dispatch_data_t data, __unused int error) { if (!data) { return; } dispatch_data_apply( data, ^bool( __unused dispatch_data_t region, __unused size_t offset, const void *buffer, size_t size ) { write(_stderr, buffer, size); NSString *log = [[NSString alloc] initWithBytes:buffer length:size encoding:NSUTF8StringEncoding]; [weakSelf parse:log]; return true; }); }); } - (void)stopLogs { dup2(_stderr, STDERR_FILENO); dispatch_io_close(_io, 0); } - (void)parse:(NSString *)log { static NSRegularExpression *GCRegex; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ NSString *pattern = @"\\[GC: (Eden|Full)Collection, (?:Skipped copying|Did copy), ([\\d\\.]+) (\\wb), ([\\d.]+) (\\ws)\\]"; GCRegex = [NSRegularExpression regularExpressionWithPattern:pattern options:0 error:nil]; }); if (_remaining) { log = [_remaining stringByAppendingString:log]; _remaining = nil; } NSArray *lines = [log componentsSeparatedByString:@"\n"]; if (lines.count == 1) { // no newlines _remaining = log; return; } for (NSString *line in lines) { NSTextCheckingResult *match = [GCRegex firstMatchInString:line options:0 range:NSMakeRange(0, line.length)]; if (match) { NSString *heapSizeStr = [line substringWithRange:[match rangeAtIndex:2]]; _heapSize = [heapSizeStr integerValue]; } } } - (void)updateStats { RCTSparseArray *views = [_bridge.uiManager valueForKey:@"viewRegistry"]; NSUInteger viewCount = views.count; NSUInteger visibleViewCount = 0; for (UIView *view in views.allObjects) { if (view.window || view.superview.window) { visibleViewCount++; } } double mem = (double)RCTGetResidentMemorySize() / 1024 / 1024; self.memory.text =[NSString stringWithFormat:@"RAM\n%.2lf\nMB", mem]; self.heap.text = [NSString stringWithFormat:@"JSC\n%.2lf\nMB", (double)_heapSize / 1024]; self.views.text = [NSString stringWithFormat:@"Views\n%lu\n%lu", (unsigned long)visibleViewCount, (unsigned long)viewCount]; __weak __typeof__(self) weakSelf = self; dispatch_after( dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ __strong __typeof__(weakSelf) strongSelf = weakSelf; if (strongSelf && strongSelf->_container.superview) { [strongSelf updateStats]; } }); } - (void)gesture:(UIPanGestureRecognizer *)gestureRecognizer { CGPoint translation = [gestureRecognizer translationInView:self.container.superview]; self.container.center = CGPointMake( self.container.center.x + translation.x, self.container.center.y + translation.y ); [gestureRecognizer setTranslation:CGPointMake(0, 0) inView:self.container.superview]; } - (void)threadUpdate:(CADisplayLink *)displayLink { RCTFPSGraph *graph = displayLink == _jsDisplayLink ? _jsGraph : _uiGraph; [graph onTick:displayLink.timestamp]; } @end #endif