diff --git a/React/Base/RCTBridge.m b/React/Base/RCTBridge.m index 5e1aa7f27c..523333aac1 100644 --- a/React/Base/RCTBridge.m +++ b/React/Base/RCTBridge.m @@ -22,6 +22,7 @@ #import "RCTJavaScriptLoader.h" #import "RCTKeyCommands.h" #import "RCTLog.h" +#import "RCTPerfStats.h" #import "RCTProfile.h" #import "RCTRedBox.h" #import "RCTRootView.h" @@ -930,6 +931,11 @@ RCT_INNER_BRIDGE_ONLY(_invokeAndProcessModule:(NSString *)module method:(NSStrin _queuesByID = [[RCTSparseArray alloc] init]; _jsDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_jsThreadUpdate:)]; + if (RCT_DEV) { + _mainDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_mainThreadUpdate:)]; + [_mainDisplayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; + } + /** * Initialize executor to allow enqueueing calls */ @@ -1560,6 +1566,8 @@ RCT_INNER_BRIDGE_ONLY(_invokeAndProcessModule:(NSString *)module method:(NSStrin } RCTProfileEndEvent(@"DispatchFrameUpdate", @"objc_call", nil); + + [self.perfStats.jsGraph tick:displayLink.timestamp]; } - (void)_mainThreadUpdate:(CADisplayLink *)displayLink @@ -1567,6 +1575,8 @@ RCT_INNER_BRIDGE_ONLY(_invokeAndProcessModule:(NSString *)module method:(NSStrin RCTAssertMainThread(); RCTProfileImmediateEvent(@"VSYNC", displayLink.timestamp, @"g"); + + [self.perfStats.uiGraph tick:displayLink.timestamp]; } - (void)startProfiling @@ -1578,10 +1588,6 @@ RCT_INNER_BRIDGE_ONLY(_invokeAndProcessModule:(NSString *)module method:(NSStrin return; } - [_mainDisplayLink invalidate]; - _mainDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_mainThreadUpdate:)]; - [_mainDisplayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; - RCTProfileInit(); } @@ -1589,8 +1595,6 @@ RCT_INNER_BRIDGE_ONLY(_invokeAndProcessModule:(NSString *)module method:(NSStrin { RCTAssertMainThread(); - [_mainDisplayLink invalidate]; - NSString *log = RCTProfileEnd(); NSURL *bundleURL = _parentBridge.bundleURL; NSString *URLString = [NSString stringWithFormat:@"%@://%@:%@/profile", bundleURL.scheme, bundleURL.host, bundleURL.port]; diff --git a/React/Base/RCTDevMenu.h b/React/Base/RCTDevMenu.h index bb80ac208d..b260fca4a4 100644 --- a/React/Base/RCTDevMenu.h +++ b/React/Base/RCTDevMenu.h @@ -35,6 +35,11 @@ */ @property (nonatomic, assign) BOOL liveReloadEnabled; +/** + * Shows the FPS monitor for the JS and Main threads + */ +@property (nonatomic, assign) BOOL showFPS; + /** * Manually show the dev menu (can be called from JS). */ diff --git a/React/Base/RCTDevMenu.m b/React/Base/RCTDevMenu.m index 4e05f0e54c..421380e5b4 100644 --- a/React/Base/RCTDevMenu.m +++ b/React/Base/RCTDevMenu.m @@ -13,6 +13,7 @@ #import "RCTDefines.h" #import "RCTKeyCommands.h" #import "RCTLog.h" +#import "RCTPerfStats.h" #import "RCTProfile.h" #import "RCTRootView.h" #import "RCTSourceCode.h" @@ -145,6 +146,7 @@ RCT_EXPORT_MODULE() self.shakeToShow = [_settings[@"shakeToShow"] ?: @YES boolValue]; self.profilingEnabled = [_settings[@"profilingEnabled"] ?: @NO boolValue]; self.liveReloadEnabled = [_settings[@"liveReloadEnabled"] ?: @NO boolValue]; + self.showFPS = [_settings[@"showFPS"] ?: @NO boolValue]; self.executorClass = NSClassFromString(_settings[@"executorClass"]); } @@ -230,13 +232,14 @@ RCT_EXPORT_METHOD(show) NSString *debugTitleChrome = _executorClass && _executorClass == NSClassFromString(@"RCTWebSocketExecutor") ? @"Disable Chrome Debugging" : @"Debug in Chrome"; NSString *debugTitleSafari = _executorClass && _executorClass == NSClassFromString(@"RCTWebViewExecutor") ? @"Disable Safari Debugging" : @"Debug in Safari"; + NSString *fpsMonitor = _showFPS ? @"Hide FPS Monitor" : @"Show FPS Monitor"; UIActionSheet *actionSheet = [[UIActionSheet alloc] initWithTitle:@"React Native: Development" delegate:self cancelButtonTitle:nil destructiveButtonTitle:nil - otherButtonTitles:@"Reload", debugTitleChrome, debugTitleSafari, nil]; + otherButtonTitles:@"Reload", debugTitleChrome, debugTitleSafari, fpsMonitor, nil]; if (_liveReloadURL) { @@ -293,10 +296,14 @@ RCT_EXPORT_METHOD(reload) break; } case 3: { - self.liveReloadEnabled = !_liveReloadEnabled; + self.showFPS = !_showFPS; break; } case 4: { + self.liveReloadEnabled = !_liveReloadEnabled; + break; + } + case 5: { self.profilingEnabled = !_profilingEnabled; break; } @@ -368,6 +375,21 @@ RCT_EXPORT_METHOD(reload) } } +- (void)setShowFPS:(BOOL)showFPS +{ + if (_showFPS != showFPS) { + _showFPS = showFPS; + + if (showFPS) { + [_bridge.perfStats show]; + } else { + [_bridge.perfStats hide]; + } + + [self updateSetting:@"showFPS" value:@(showFPS)]; + } +} + - (void)checkForUpdates { if (!_jsLoaded || !_liveReloadEnabled || !_liveReloadURL) { diff --git a/React/Base/RCTFPSGraph.h b/React/Base/RCTFPSGraph.h new file mode 100644 index 0000000000..905829aba2 --- /dev/null +++ b/React/Base/RCTFPSGraph.h @@ -0,0 +1,23 @@ +/** + * 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 + +typedef NS_ENUM(NSUInteger, RCTFPSGraphPosition) { + RCTFPSGraphPositionLeft = 1, + RCTFPSGraphPositionRight = 2 +}; + +@interface RCTFPSGraph : UIView + +- (instancetype)initWithFrame:(CGRect)frame graphPosition:(RCTFPSGraphPosition)position name:(NSString *)name color:(UIColor *)color NS_DESIGNATED_INITIALIZER; + +- (void)tick:(NSTimeInterval)timestamp; + +@end diff --git a/React/Base/RCTFPSGraph.m b/React/Base/RCTFPSGraph.m new file mode 100644 index 0000000000..461f17e411 --- /dev/null +++ b/React/Base/RCTFPSGraph.m @@ -0,0 +1,132 @@ +/** + * 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 "RCTFPSGraph.h" + +#import "RCTDefines.h" + +#if RCT_DEV + +@implementation RCTFPSGraph +{ + CAShapeLayer *_graph; + NSString *_name; + NSTimeInterval _prevTime; + RCTFPSGraphPosition _position; + UILabel *_label; + + float *_frames; + int _frameCount; + int _maxFPS; + int _minFPS; + int _length; + int _margin; + int _height; +} + +- (instancetype)initWithFrame:(CGRect)frame graphPosition:(RCTFPSGraphPosition)position name:(NSString *)name color:(UIColor *)color +{ + if (self = [super initWithFrame:frame]) { + _margin = 2; + _prevTime = -1; + _maxFPS = 0; + _minFPS = 60; + _length = (frame.size.width - 2 * _margin) / 2; + _height = frame.size.height - 2 * _margin; + _frames = malloc(sizeof(float) * _length); + memset(_frames, 0, sizeof(float) * _length); + + _name = name; + _position = position; + _graph = [self createGraph:color]; + _label = [self createLabel:color]; + + [self addSubview:_label]; + [self.layer addSublayer:_graph]; + } + return self; +} + +- (void)dealloc +{ + free(_frames); +} + +- (void)layoutSubviews +{ + [super layoutSubviews]; +} + +- (CAShapeLayer *)createGraph:(UIColor *)color +{ + CGFloat left = _position & RCTFPSGraphPositionLeft ? 0 : _length; + CAShapeLayer *graph = [[CAShapeLayer alloc] init]; + graph.frame = CGRectMake(left, 0, 2 * _margin + _length, self.frame.size.height); + graph.backgroundColor = [[color colorWithAlphaComponent:.2] CGColor]; + graph.fillColor = [color CGColor]; + return graph; +} + +- (UILabel *)createLabel:(UIColor *)color +{ + CGFloat left = _position & RCTFPSGraphPositionLeft ? 2 * _margin + _length : 0; + UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(left, 0, _length, self.frame.size.height)]; + label.textColor = color; + label.font = [UIFont systemFontOfSize:9]; + label.minimumScaleFactor = .5; + label.adjustsFontSizeToFitWidth = YES; + label.numberOfLines = 3; + label.lineBreakMode = NSLineBreakByWordWrapping; + label.textAlignment = NSTextAlignmentCenter; + return label; +} + +- (void)tick:(NSTimeInterval)timestamp +{ + _frameCount++; + if (_prevTime == -1) { + _prevTime = timestamp; + } else if (timestamp - _prevTime > 1) { + float fps = round(_frameCount / (timestamp - _prevTime)); + _minFPS = MIN(_minFPS, fps); + _maxFPS = MAX(_maxFPS, fps); + + _label.text = [NSString stringWithFormat:@"%@\n%d FPS\n(%d - %d)", _name, (int)fps, _minFPS, _maxFPS]; + + float scale = 60.0 / _height; + for (int i = 0; i < _length - 1; i++) { + _frames[i] = _frames[i + 1]; + } + _frames[_length - 1] = fps / scale; + + CGMutablePathRef path = CGPathCreateMutable(); + if (_position & RCTFPSGraphPositionLeft) { + CGPathMoveToPoint(path, NULL, _margin, _margin + _height); + for (int i = 0; i < _length; i++) { + CGPathAddLineToPoint(path, NULL, _margin + i, _margin + _height - _frames[i]); + } + CGPathAddLineToPoint(path, NULL, _margin + _length - 1, _margin + _height); + } else { + CGPathMoveToPoint(path, NULL, _margin + _length - 1, _margin + _height); + for (int i = 0; i < _length; i++) { + CGPathAddLineToPoint(path, NULL, _margin + _length - i - 1, _margin + _height - _frames[i]); + } + CGPathAddLineToPoint(path, NULL, _margin, _margin + _height); + } + _graph.path = path; + CGPathRelease(path); + + _prevTime = timestamp; + _frameCount = 0; + } +} + +@end + +#endif diff --git a/React/Base/RCTPerfStats.h b/React/Base/RCTPerfStats.h new file mode 100644 index 0000000000..18c13cad68 --- /dev/null +++ b/React/Base/RCTPerfStats.h @@ -0,0 +1,27 @@ +/** + * 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 "RCTBridge.h" +#import "RCTFPSGraph.h" + +@interface RCTPerfStats : NSObject + +@property (nonatomic, strong) RCTFPSGraph *jsGraph; +@property (nonatomic, strong) RCTFPSGraph *uiGraph; + +- (void)show; +- (void)hide; + +@end + +@interface RCTBridge (RCTPerfStats) + +@property (nonatomic, strong, readonly) RCTPerfStats *perfStats; + +@end diff --git a/React/Base/RCTPerfStats.m b/React/Base/RCTPerfStats.m new file mode 100644 index 0000000000..2e5ec8d142 --- /dev/null +++ b/React/Base/RCTPerfStats.m @@ -0,0 +1,133 @@ +/** + * 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 "RCTPerfStats.h" + +#import "RCTDefines.h" + +#if RCT_DEV + +@interface RCTPerfStats() + +@end + +@implementation RCTPerfStats +{ + UIView *_container; +} + +RCT_EXPORT_MODULE() + +- (void)dealloc +{ + [self hide]; +} + +- (UIView *)container +{ + if (!_container) { + _container = [[UIView alloc] init]; + _container.backgroundColor = [UIColor colorWithRed:0 green:0 blue:34/255.0 alpha:1]; + _container.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleWidth; + } + return _container; +} + +- (RCTFPSGraph *)jsGraph +{ + if (!_jsGraph) { + UIColor *jsColor = [UIColor colorWithRed:0 green:1 blue:0 alpha:1]; + _jsGraph = [[RCTFPSGraph alloc] initWithFrame:CGRectMake(2, 2, 124, 34) + graphPosition:RCTFPSGraphPositionRight + name:@"[ JS ]" + color:jsColor]; + _jsGraph.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin; + } + return _jsGraph; +} + +- (RCTFPSGraph *)uiGraph +{ + if (!_uiGraph) { + UIColor *uiColor = [UIColor colorWithRed:0 green:1 blue:1 alpha:1]; + _uiGraph = [[RCTFPSGraph alloc] initWithFrame:CGRectMake(2, 2, 124, 34) + graphPosition:RCTFPSGraphPositionLeft + name:@"[ UI ]" + color:uiColor]; + } + return _uiGraph; +} + +- (void)show +{ + UIView *targetView = [[[[[UIApplication sharedApplication] delegate] window] rootViewController] view]; + + targetView.frame = (CGRect){ + targetView.frame.origin, + { + targetView.frame.size.width, + targetView.frame.size.height - 38, + } + }; + + self.container.frame = (CGRect){{0, targetView.frame.size.height}, {targetView.frame.size.width, 38}}; + self.jsGraph.frame = (CGRect){ + { + targetView.frame.size.width - self.uiGraph.frame.size.width - self.uiGraph.frame.origin.x, + self.uiGraph.frame.origin.x, + }, + self.uiGraph.frame.size, + }; + + [self.container addSubview:self.jsGraph]; + [self.container addSubview:self.uiGraph]; + [targetView addSubview:self.container]; +} + +- (void)hide +{ + UIView *targetView = _container.superview; + + targetView.frame = (CGRect){ + targetView.frame.origin, + { + targetView.frame.size.width, + targetView.frame.size.height + _container.frame.size.height + } + }; + + [_container removeFromSuperview]; +} + +- (dispatch_queue_t)methodQueue +{ + return dispatch_get_main_queue(); +} + +@end + +#else + +@implementation RCTPerfStats + +- (void)show {} +- (void)hide {} + +@end + +#endif + +@implementation RCTBridge (RCTPerfStats) + +- (RCTPerfStats *)perfStats +{ + return self.modules[RCTBridgeModuleNameForClass([RCTPerfStats class])]; +} + +@end diff --git a/React/React.xcodeproj/project.pbxproj b/React/React.xcodeproj/project.pbxproj index 42954d36e5..cf05f4e39c 100644 --- a/React/React.xcodeproj/project.pbxproj +++ b/React/React.xcodeproj/project.pbxproj @@ -44,9 +44,11 @@ 13E067561A70F44B002CDEE1 /* RCTViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E0674E1A70F44B002CDEE1 /* RCTViewManager.m */; }; 13E067571A70F44B002CDEE1 /* RCTView.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E067501A70F44B002CDEE1 /* RCTView.m */; }; 13E067591A70F44B002CDEE1 /* UIView+React.m in Sources */ = {isa = PBXBuildFile; fileRef = 13E067541A70F44B002CDEE1 /* UIView+React.m */; }; + 1403F2B31B0AE60700C2A9A4 /* RCTPerfStats.m in Sources */ = {isa = PBXBuildFile; fileRef = 1403F2B21B0AE60700C2A9A4 /* RCTPerfStats.m */; }; 14200DAA1AC179B3008EE6BA /* RCTJavaScriptLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 14200DA91AC179B3008EE6BA /* RCTJavaScriptLoader.m */; }; 14435CE51AAC4AE100FC20F4 /* RCTMap.m in Sources */ = {isa = PBXBuildFile; fileRef = 14435CE21AAC4AE100FC20F4 /* RCTMap.m */; }; 14435CE61AAC4AE100FC20F4 /* RCTMapManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 14435CE41AAC4AE100FC20F4 /* RCTMapManager.m */; }; + 146459261B06C49500B389AA /* RCTFPSGraph.m in Sources */ = {isa = PBXBuildFile; fileRef = 146459251B06C49500B389AA /* RCTFPSGraph.m */; }; 14F3620D1AABD06A001CE568 /* RCTSwitch.m in Sources */ = {isa = PBXBuildFile; fileRef = 14F362081AABD06A001CE568 /* RCTSwitch.m */; }; 14F3620E1AABD06A001CE568 /* RCTSwitchManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 14F3620A1AABD06A001CE568 /* RCTSwitchManager.m */; }; 14F484561AABFCE100FDF6B9 /* RCTSliderManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 14F484551AABFCE100FDF6B9 /* RCTSliderManager.m */; }; @@ -163,6 +165,8 @@ 13E067501A70F44B002CDEE1 /* RCTView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTView.m; sourceTree = ""; }; 13E067531A70F44B002CDEE1 /* UIView+React.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIView+React.h"; sourceTree = ""; }; 13E067541A70F44B002CDEE1 /* UIView+React.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIView+React.m"; sourceTree = ""; }; + 1403F2B11B0AE60700C2A9A4 /* RCTPerfStats.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTPerfStats.h; sourceTree = ""; }; + 1403F2B21B0AE60700C2A9A4 /* RCTPerfStats.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTPerfStats.m; sourceTree = ""; }; 14200DA81AC179B3008EE6BA /* RCTJavaScriptLoader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTJavaScriptLoader.h; sourceTree = ""; }; 14200DA91AC179B3008EE6BA /* RCTJavaScriptLoader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTJavaScriptLoader.m; sourceTree = ""; }; 1436DD071ADE7AA000A5ED7D /* RCTFrameUpdate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTFrameUpdate.h; sourceTree = ""; }; @@ -170,6 +174,8 @@ 14435CE21AAC4AE100FC20F4 /* RCTMap.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTMap.m; sourceTree = ""; }; 14435CE31AAC4AE100FC20F4 /* RCTMapManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTMapManager.h; sourceTree = ""; }; 14435CE41AAC4AE100FC20F4 /* RCTMapManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTMapManager.m; sourceTree = ""; }; + 146459241B06C49500B389AA /* RCTFPSGraph.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTFPSGraph.h; sourceTree = ""; }; + 146459251B06C49500B389AA /* RCTFPSGraph.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTFPSGraph.m; sourceTree = ""; }; 14F362071AABD06A001CE568 /* RCTSwitch.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTSwitch.h; sourceTree = ""; }; 14F362081AABD06A001CE568 /* RCTSwitch.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTSwitch.m; sourceTree = ""; }; 14F362091AABD06A001CE568 /* RCTSwitchManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTSwitchManager.h; sourceTree = ""; }; @@ -415,6 +421,10 @@ 1436DD071ADE7AA000A5ED7D /* RCTFrameUpdate.h */, 14F4D3891AE1B7E40049C042 /* RCTProfile.h */, 14F4D38A1AE1B7E40049C042 /* RCTProfile.m */, + 146459241B06C49500B389AA /* RCTFPSGraph.h */, + 146459251B06C49500B389AA /* RCTFPSGraph.m */, + 1403F2B11B0AE60700C2A9A4 /* RCTPerfStats.h */, + 1403F2B21B0AE60700C2A9A4 /* RCTPerfStats.m */, ); path = Base; sourceTree = ""; @@ -514,6 +524,7 @@ 13E067561A70F44B002CDEE1 /* RCTViewManager.m in Sources */, 58C571C11AA56C1900CDF9C8 /* RCTDatePickerManager.m in Sources */, 13B080061A6947C200A75B9A /* RCTScrollViewManager.m in Sources */, + 146459261B06C49500B389AA /* RCTFPSGraph.m in Sources */, 14200DAA1AC179B3008EE6BA /* RCTJavaScriptLoader.m in Sources */, 137327EA1AA5CF210034F82E /* RCTTabBarManager.m in Sources */, 13B080261A694A8400A75B9A /* RCTWrapperViewController.m in Sources */, @@ -549,6 +560,7 @@ 14435CE51AAC4AE100FC20F4 /* RCTMap.m in Sources */, 134FCB3E1A6E7F0800051CC8 /* RCTWebViewExecutor.m in Sources */, 13B0801C1A69489C00A75B9A /* RCTNavItem.m in Sources */, + 1403F2B31B0AE60700C2A9A4 /* RCTPerfStats.m in Sources */, 83CBBA691A601EF300E9B192 /* RCTEventDispatcher.m in Sources */, 13E0674A1A70F434002CDEE1 /* RCTUIManager.m in Sources */, 13B0801B1A69489C00A75B9A /* RCTNavigatorManager.m in Sources */,