/** * 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 "RCTJSCExecutor.h" #import "RCTPerformanceLogger.h" #import "RCTRootView.h" #import "RCTUIManager.h" #import "RCTBridge+Private.h" static NSString *const RCTPerfMonitorKey = @"RCTPerfMonitorKey"; static NSString *const RCTPerfMonitorCellIdentifier = @"RCTPerfMonitorCellIdentifier"; static CGFloat const RCTPerfMonitorBarHeight = 50; static CGFloat const RCTPerfMonitorExpandHeight = 250; typedef BOOL (*RCTJSCSetOptionType)(const char *); static BOOL RCTJSCSetOption(const char *option) { static RCTJSCSetOptionType setOption; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ /** * JSC private C++ static method to toggle options at runtime * * JSC::Options::setOptions - JavaScriptCore/runtime/Options.h */ setOption = dlsym(RTLD_DEFAULT, "_ZN3JSC7Options9setOptionEPKc"); if (RCT_DEBUG && setOption == NULL) { RCTLogWarn(@"The symbol used to enable JSC runtime options is not available in this iOS version"); } }); 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) UITableView *metrics; @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; UITableView *_metrics; 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; CGRect _storedMonitorFrame; NSArray *_perfLoggerMarks; } @synthesize bridge = _bridge; RCT_EXPORT_MODULE() - (instancetype)init { // We're only overriding this to ensure the module gets created at startup // TODO (t11106126): Remove once we have more declarative control over module setup. return [super init]; } - (dispatch_queue_t)methodQueue { return dispatch_get_main_queue(); } - (void)setBridge:(RCTBridge *)bridge { _bridge = bridge; // TODO: enable on cxx bridge if ([_bridge isKindOfClass:[RCTBatchedBridge class]]) { [_bridge.devMenu addItem:self.devMenuItem]; } } - (void)invalidate { [self hide]; } - (RCTDevMenuItem *)devMenuItem { if (!_devMenuItem) { __weak __typeof__(self) weakSelf = self; _devMenuItem = [RCTDevMenuItem toggleItemWithKey:RCTPerfMonitorKey title:@"Show Perf Monitor" selectedTitle:@"Hide Perf 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, RCTPerfMonitorBarHeight)]; _container.backgroundColor = UIColor.whiteColor; _container.layer.borderWidth = 2; _container.layer.borderColor = [UIColor lightGrayColor].CGColor; [_container addGestureRecognizer:self.gestureRecognizer]; [_container addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tap)]]; } return _container; } - (UILabel *)memory { if (!_memory) { _memory = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 44, RCTPerfMonitorBarHeight)]; _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, RCTPerfMonitorBarHeight)]; _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, RCTPerfMonitorBarHeight)]; _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; } - (UITableView *)metrics { if (!_metrics) { _metrics = [[UITableView alloc] initWithFrame:CGRectMake( 0, RCTPerfMonitorBarHeight, self.container.frame.size.width, self.container.frame.size.height - RCTPerfMonitorBarHeight )]; _metrics.dataSource = self; _metrics.delegate = self; _metrics.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth; [_metrics registerClass:[UITableViewCell class] forCellReuseIdentifier:RCTPerfMonitorCellIdentifier]; } return _metrics; } - (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:[RCTJSCExecutor class]]) { 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:^{ self->_jsDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(threadUpdate:)]; [self->_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(self->_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 { NSDictionary *views = [_bridge.uiManager valueForKey:@"viewRegistry"]; NSUInteger viewCount = views.count; NSUInteger visibleViewCount = 0; for (UIView *view in views.allValues) { 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)tap { if (CGRectIsEmpty(_storedMonitorFrame)) { _storedMonitorFrame = CGRectMake(0, 20, self.container.window.frame.size.width, RCTPerfMonitorExpandHeight); [self.container addSubview:self.metrics]; [self loadPerformanceLoggerData]; } [UIView animateWithDuration:.25 animations:^{ CGRect tmp = self.container.frame; self.container.frame = self->_storedMonitorFrame; self->_storedMonitorFrame = tmp; }]; } - (void)threadUpdate:(CADisplayLink *)displayLink { RCTFPSGraph *graph = displayLink == _jsDisplayLink ? _jsGraph : _uiGraph; [graph onTick:displayLink.timestamp]; } - (void)loadPerformanceLoggerData { NSUInteger i = 0; NSMutableArray *data = [NSMutableArray new]; RCTPerformanceLogger *performanceLogger = [_bridge performanceLogger]; NSArray *values = [performanceLogger valuesForTags]; for (NSString *label in [performanceLogger labelsForTags]) { long long value = values[i+1].longLongValue - values[i].longLongValue; NSString *unit = @"ms"; if ([label hasSuffix:@"Size"]) { unit = @"b"; } else if ([label hasSuffix:@"Count"]) { unit = @""; } [data addObject:[NSString stringWithFormat:@"%@: %lld%@", label, value, unit]]; i += 2; } _perfLoggerMarks = [data copy]; } #pragma mark - UITableViewDataSource - (NSInteger)numberOfSectionsInTableView:(__unused UITableView *)tableView { return 1; } - (NSInteger)tableView:(__unused UITableView *)tableView numberOfRowsInSection:(__unused NSInteger)section { return _perfLoggerMarks.count; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:RCTPerfMonitorCellIdentifier forIndexPath:indexPath]; if (!cell) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:RCTPerfMonitorCellIdentifier]; } cell.textLabel.text = _perfLoggerMarks[indexPath.row]; cell.textLabel.font = [UIFont systemFontOfSize:12]; return cell; } #pragma mark - UITableViewDelegate - (CGFloat)tableView:(__unused UITableView *)tableView heightForRowAtIndexPath:(__unused NSIndexPath *)indexPath { return 20; } @end #endif