mirror of
https://github.com/status-im/react-native.git
synced 2025-01-10 01:25:39 +00:00
9069bdf1c2
Summary: public Kill `RCTPerfStats` and introduce the new `RCTPerfMonitor`, including memory usage, JSC heap size, number of RN views in screen, FPS (both on UI and JS threads) and more to come. It removes all the previous traces that were previous spread across the bridge and the dev menu and moves everything to be more contained, so the whole thing can be safely striped in production. Reviewed By: nicklockwood Differential Revision: D2575158 fb-gh-sync-id: 6a6d0c4422adbddeeefddd32ec3409a7095ff2a9
617 lines
18 KiB
Objective-C
617 lines
18 KiB
Objective-C
/**
|
|
* 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 "RCTDevMenu.h"
|
|
|
|
#import "RCTAssert.h"
|
|
#import "RCTBridge.h"
|
|
#import "RCTDefines.h"
|
|
#import "RCTEventDispatcher.h"
|
|
#import "RCTKeyCommands.h"
|
|
#import "RCTLog.h"
|
|
#import "RCTProfile.h"
|
|
#import "RCTRootView.h"
|
|
#import "RCTSourceCode.h"
|
|
#import "RCTUtils.h"
|
|
|
|
#if RCT_DEV
|
|
|
|
@interface RCTBridge (Profiling)
|
|
|
|
- (void)startProfiling;
|
|
- (void)stopProfiling:(void (^)(NSData *))callback;
|
|
|
|
@end
|
|
|
|
static NSString *const RCTShowDevMenuNotification = @"RCTShowDevMenuNotification";
|
|
static NSString *const RCTDevMenuSettingsKey = @"RCTDevMenu";
|
|
|
|
@implementation UIWindow (RCTDevMenu)
|
|
|
|
- (void)RCT_motionEnded:(__unused UIEventSubtype)motion withEvent:(UIEvent *)event
|
|
{
|
|
if (event.subtype == UIEventSubtypeMotionShake) {
|
|
[[NSNotificationCenter defaultCenter] postNotificationName:RCTShowDevMenuNotification object:nil];
|
|
}
|
|
}
|
|
|
|
@end
|
|
|
|
typedef NS_ENUM(NSInteger, RCTDevMenuType) {
|
|
RCTDevMenuTypeButton,
|
|
RCTDevMenuTypeToggle
|
|
};
|
|
|
|
@interface RCTDevMenuItem ()
|
|
|
|
@property (nonatomic, assign, readonly) RCTDevMenuType type;
|
|
@property (nonatomic, copy, readonly) NSString *key;
|
|
@property (nonatomic, copy, readonly) NSString *title;
|
|
@property (nonatomic, copy, readonly) NSString *selectedTitle;
|
|
@property (nonatomic, copy) id value;
|
|
|
|
@end
|
|
|
|
@implementation RCTDevMenuItem
|
|
{
|
|
id _handler; // block
|
|
}
|
|
|
|
- (instancetype)initWithType:(RCTDevMenuType)type
|
|
key:(NSString *)key
|
|
title:(NSString *)title
|
|
selectedTitle:(NSString *)selectedTitle
|
|
handler:(id /* block */)handler
|
|
{
|
|
if ((self = [super init])) {
|
|
_type = type;
|
|
_key = [key copy];
|
|
_title = [title copy];
|
|
_selectedTitle = [selectedTitle copy];
|
|
_handler = [handler copy];
|
|
_value = nil;
|
|
}
|
|
return self;
|
|
}
|
|
|
|
RCT_NOT_IMPLEMENTED(- (instancetype)init)
|
|
|
|
+ (instancetype)buttonItemWithTitle:(NSString *)title
|
|
handler:(void (^)(void))handler
|
|
{
|
|
return [[self alloc] initWithType:RCTDevMenuTypeButton
|
|
key:nil
|
|
title:title
|
|
selectedTitle:nil
|
|
handler:handler];
|
|
}
|
|
|
|
+ (instancetype)toggleItemWithKey:(NSString *)key
|
|
title:(NSString *)title
|
|
selectedTitle:(NSString *)selectedTitle
|
|
handler:(void (^)(BOOL selected))handler
|
|
{
|
|
return [[self alloc] initWithType:RCTDevMenuTypeToggle
|
|
key:key
|
|
title:title
|
|
selectedTitle:selectedTitle
|
|
handler:handler];
|
|
}
|
|
|
|
- (void)callHandler
|
|
{
|
|
switch (_type) {
|
|
case RCTDevMenuTypeButton: {
|
|
if (_handler) {
|
|
((void(^)())_handler)();
|
|
}
|
|
break;
|
|
}
|
|
case RCTDevMenuTypeToggle: {
|
|
if (_handler) {
|
|
((void(^)(BOOL selected))_handler)([_value boolValue]);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
@end
|
|
|
|
@interface RCTDevMenu () <RCTBridgeModule, UIActionSheetDelegate, RCTInvalidating>
|
|
|
|
@property (nonatomic, strong) Class executorClass;
|
|
|
|
@end
|
|
|
|
@implementation RCTDevMenu
|
|
{
|
|
UIActionSheet *_actionSheet;
|
|
NSUserDefaults *_defaults;
|
|
NSMutableDictionary *_settings;
|
|
NSURLSessionDataTask *_updateTask;
|
|
NSURL *_liveReloadURL;
|
|
BOOL _jsLoaded;
|
|
NSArray *_presentedItems;
|
|
NSMutableArray *_extraMenuItems;
|
|
}
|
|
|
|
@synthesize bridge = _bridge;
|
|
|
|
RCT_EXPORT_MODULE()
|
|
|
|
+ (void)initialize
|
|
{
|
|
// We're swizzling here because it's poor form to override methods in a category,
|
|
// however UIWindow doesn't actually implement motionEnded:withEvent:, so there's
|
|
// no need to call the original implementation.
|
|
RCTSwapInstanceMethods([UIWindow class], @selector(motionEnded:withEvent:), @selector(RCT_motionEnded:withEvent:));
|
|
}
|
|
|
|
- (instancetype)init
|
|
{
|
|
if ((self = [super init])) {
|
|
|
|
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
|
|
|
|
[notificationCenter addObserver:self
|
|
selector:@selector(showOnShake)
|
|
name:RCTShowDevMenuNotification
|
|
object:nil];
|
|
|
|
[notificationCenter addObserver:self
|
|
selector:@selector(settingsDidChange)
|
|
name:NSUserDefaultsDidChangeNotification
|
|
object:nil];
|
|
|
|
[notificationCenter addObserver:self
|
|
selector:@selector(jsLoaded:)
|
|
name:RCTJavaScriptDidLoadNotification
|
|
object:nil];
|
|
|
|
_defaults = [NSUserDefaults standardUserDefaults];
|
|
_settings = [[NSMutableDictionary alloc] initWithDictionary:[_defaults objectForKey:RCTDevMenuSettingsKey]];
|
|
_extraMenuItems = [NSMutableArray new];
|
|
|
|
__weak RCTDevMenu *weakSelf = self;
|
|
|
|
[_extraMenuItems addObject:[RCTDevMenuItem toggleItemWithKey:@"showInspector"
|
|
title:@"Show Inspector"
|
|
selectedTitle:@"Hide Inspector"
|
|
handler:^(__unused BOOL enabled)
|
|
{
|
|
[weakSelf.bridge.eventDispatcher sendDeviceEventWithName:@"toggleElementInspector" body:nil];
|
|
}]];
|
|
|
|
// Delay setup until after Bridge init
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[weakSelf updateSettings:_settings];
|
|
});
|
|
|
|
#if TARGET_IPHONE_SIMULATOR
|
|
|
|
RCTKeyCommands *commands = [RCTKeyCommands sharedInstance];
|
|
|
|
// Toggle debug menu
|
|
[commands registerKeyCommandWithInput:@"d"
|
|
modifierFlags:UIKeyModifierCommand
|
|
action:^(__unused UIKeyCommand *command) {
|
|
[weakSelf toggle];
|
|
}];
|
|
|
|
// Toggle element inspector
|
|
[commands registerKeyCommandWithInput:@"i"
|
|
modifierFlags:UIKeyModifierCommand
|
|
action:^(__unused UIKeyCommand *command) {
|
|
[weakSelf.bridge.eventDispatcher
|
|
sendDeviceEventWithName:@"toggleElementInspector"
|
|
body:nil];
|
|
}];
|
|
|
|
// Reload in normal mode
|
|
[commands registerKeyCommandWithInput:@"n"
|
|
modifierFlags:UIKeyModifierCommand
|
|
action:^(__unused UIKeyCommand *command) {
|
|
weakSelf.executorClass = Nil;
|
|
}];
|
|
#endif
|
|
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (dispatch_queue_t)methodQueue
|
|
{
|
|
return dispatch_get_main_queue();
|
|
}
|
|
|
|
- (void)settingsDidChange
|
|
{
|
|
// Needed to prevent a race condition when reloading
|
|
__weak RCTDevMenu *weakSelf = self;
|
|
NSDictionary *settings = [_defaults objectForKey:RCTDevMenuSettingsKey];
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[weakSelf updateSettings:settings];
|
|
});
|
|
}
|
|
|
|
/**
|
|
* This method loads the settings from NSUserDefaults and overrides any local
|
|
* settings with them. It should only be called on app launch, or after the app
|
|
* has returned from the background, when the settings might have been edited
|
|
* outside of the app.
|
|
*/
|
|
- (void)updateSettings:(NSDictionary *)settings
|
|
{
|
|
[_settings setDictionary:settings];
|
|
|
|
// Fire handlers for items whose values have changed
|
|
for (RCTDevMenuItem *item in _extraMenuItems) {
|
|
if (item.key) {
|
|
id value = settings[item.key];
|
|
if (value != item.value && ![value isEqual:item.value]) {
|
|
item.value = value;
|
|
[item callHandler];
|
|
}
|
|
}
|
|
}
|
|
|
|
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"]);
|
|
}
|
|
|
|
/**
|
|
* This updates a particular setting, and then saves the settings. Because all
|
|
* settings are overwritten by this, it's important that this is not called
|
|
* before settings have been loaded initially, otherwise the other settings
|
|
* will be reset.
|
|
*/
|
|
- (void)updateSetting:(NSString *)name value:(id)value
|
|
{
|
|
// Fire handler for item whose values has changed
|
|
for (RCTDevMenuItem *item in _extraMenuItems) {
|
|
if ([item.key isEqualToString:name]) {
|
|
if (value != item.value && ![value isEqual:item.value]) {
|
|
item.value = value;
|
|
[item callHandler];
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Save the setting
|
|
id currentValue = _settings[name];
|
|
if (currentValue == value || [currentValue isEqual:value]) {
|
|
return;
|
|
}
|
|
if (value) {
|
|
_settings[name] = value;
|
|
} else {
|
|
[_settings removeObjectForKey:name];
|
|
}
|
|
[_defaults setObject:_settings forKey:RCTDevMenuSettingsKey];
|
|
[_defaults synchronize];
|
|
}
|
|
|
|
- (void)jsLoaded:(NSNotification *)notification
|
|
{
|
|
if (notification.userInfo[@"bridge"] != _bridge) {
|
|
return;
|
|
}
|
|
|
|
_jsLoaded = YES;
|
|
|
|
// Check if live reloading is available
|
|
_liveReloadURL = nil;
|
|
RCTSourceCode *sourceCodeModule = _bridge.modules[RCTBridgeModuleNameForClass([RCTSourceCode class])];
|
|
if (!sourceCodeModule.scriptURL) {
|
|
if (!sourceCodeModule) {
|
|
RCTLogWarn(@"RCTSourceCode module not found");
|
|
} else {
|
|
RCTLogWarn(@"RCTSourceCode module scriptURL has not been set");
|
|
}
|
|
} else if (!(sourceCodeModule.scriptURL).fileURL) {
|
|
// Live reloading is disabled when running from bundled JS file
|
|
_liveReloadURL = [[NSURL alloc] initWithString:@"/onchange" relativeToURL:sourceCodeModule.scriptURL];
|
|
}
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
// Hit these setters again after bridge has finished loading
|
|
self.profilingEnabled = _profilingEnabled;
|
|
self.liveReloadEnabled = _liveReloadEnabled;
|
|
self.executorClass = _executorClass;
|
|
|
|
// Inspector can only be shown after JS has loaded
|
|
if ([_settings[@"showInspector"] boolValue]) {
|
|
[self.bridge.eventDispatcher sendDeviceEventWithName:@"toggleElementInspector" body:nil];
|
|
}
|
|
});
|
|
}
|
|
|
|
- (void)invalidate
|
|
{
|
|
_presentedItems = nil;
|
|
[_updateTask cancel];
|
|
[_actionSheet dismissWithClickedButtonIndex:_actionSheet.cancelButtonIndex animated:YES];
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
}
|
|
|
|
- (void)showOnShake
|
|
{
|
|
if (_shakeToShow) {
|
|
[self show];
|
|
}
|
|
}
|
|
|
|
- (void)toggle
|
|
{
|
|
if (_actionSheet) {
|
|
[_actionSheet dismissWithClickedButtonIndex:_actionSheet.cancelButtonIndex animated:YES];
|
|
_actionSheet = nil;
|
|
} else {
|
|
[self show];
|
|
}
|
|
}
|
|
|
|
- (void)addItem:(NSString *)title handler:(void(^)(void))handler
|
|
{
|
|
[self addItem:[RCTDevMenuItem buttonItemWithTitle:title handler:handler]];
|
|
}
|
|
|
|
- (void)addItem:(RCTDevMenuItem *)item
|
|
{
|
|
[_extraMenuItems addObject:item];
|
|
|
|
// Fire handler for items whose saved value doesn't match the default
|
|
[self settingsDidChange];
|
|
}
|
|
|
|
- (NSArray *)menuItems
|
|
{
|
|
NSMutableArray *items = [NSMutableArray new];
|
|
|
|
// Add built-in items
|
|
|
|
__weak RCTDevMenu *weakSelf = self;
|
|
|
|
[items addObject:[RCTDevMenuItem buttonItemWithTitle:@"Reload" handler:^{
|
|
[weakSelf reload];
|
|
}]];
|
|
|
|
Class chromeExecutorClass = NSClassFromString(@"RCTWebSocketExecutor");
|
|
if (!chromeExecutorClass) {
|
|
[items addObject:[RCTDevMenuItem buttonItemWithTitle:@"Chrome Debugger Unavailable" handler:^{
|
|
UIAlertView *alert = RCTAlertView(@"Chrome Debugger Unavailable", @"You need to include the RCTWebSocket library to enable Chrome debugging", nil, @"OK", nil);
|
|
[alert show];
|
|
}]];
|
|
} else {
|
|
BOOL isDebuggingInChrome = _executorClass && _executorClass == chromeExecutorClass;
|
|
NSString *debugTitleChrome = isDebuggingInChrome ? @"Disable Chrome Debugging" : @"Debug in Chrome";
|
|
[items addObject:[RCTDevMenuItem buttonItemWithTitle:debugTitleChrome handler:^{
|
|
weakSelf.executorClass = isDebuggingInChrome ? Nil : chromeExecutorClass;
|
|
}]];
|
|
}
|
|
|
|
Class safariExecutorClass = NSClassFromString(@"RCTWebViewExecutor");
|
|
BOOL isDebuggingInSafari = _executorClass && _executorClass == safariExecutorClass;
|
|
NSString *debugTitleSafari = isDebuggingInSafari ? @"Disable Safari Debugging" : @"Debug in Safari";
|
|
[items addObject:[RCTDevMenuItem buttonItemWithTitle:debugTitleSafari handler:^{
|
|
weakSelf.executorClass = isDebuggingInSafari ? Nil : safariExecutorClass;
|
|
}]];
|
|
|
|
if (_liveReloadURL) {
|
|
NSString *liveReloadTitle = _liveReloadEnabled ? @"Disable Live Reload" : @"Enable Live Reload";
|
|
[items addObject:[RCTDevMenuItem buttonItemWithTitle:liveReloadTitle handler:^{
|
|
weakSelf.liveReloadEnabled = !_liveReloadEnabled;
|
|
}]];
|
|
|
|
NSString *profilingTitle = RCTProfileIsProfiling() ? @"Stop Systrace" : @"Start Systrace";
|
|
[items addObject:[RCTDevMenuItem buttonItemWithTitle:profilingTitle handler:^{
|
|
weakSelf.profilingEnabled = !_profilingEnabled;
|
|
}]];
|
|
}
|
|
|
|
[items addObjectsFromArray:_extraMenuItems];
|
|
|
|
return items;
|
|
}
|
|
|
|
RCT_EXPORT_METHOD(show)
|
|
{
|
|
if (_actionSheet || !_bridge || RCTRunningInAppExtension()) {
|
|
return;
|
|
}
|
|
|
|
UIActionSheet *actionSheet = [UIActionSheet new];
|
|
actionSheet.title = @"React Native: Development";
|
|
actionSheet.delegate = self;
|
|
|
|
NSArray *items = [self menuItems];
|
|
for (RCTDevMenuItem *item in items) {
|
|
switch (item.type) {
|
|
case RCTDevMenuTypeButton: {
|
|
[actionSheet addButtonWithTitle:item.title];
|
|
break;
|
|
}
|
|
case RCTDevMenuTypeToggle: {
|
|
BOOL selected = [item.value boolValue];
|
|
[actionSheet addButtonWithTitle:selected? item.selectedTitle : item.title];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
[actionSheet addButtonWithTitle:@"Cancel"];
|
|
actionSheet.cancelButtonIndex = actionSheet.numberOfButtons - 1;
|
|
|
|
actionSheet.actionSheetStyle = UIBarStyleBlack;
|
|
[actionSheet showInView:RCTSharedApplication().keyWindow.rootViewController.view];
|
|
_actionSheet = actionSheet;
|
|
_presentedItems = items;
|
|
}
|
|
|
|
- (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex
|
|
{
|
|
_actionSheet = nil;
|
|
if (buttonIndex == actionSheet.cancelButtonIndex) {
|
|
return;
|
|
}
|
|
|
|
RCTDevMenuItem *item = _presentedItems[buttonIndex];
|
|
switch (item.type) {
|
|
case RCTDevMenuTypeButton: {
|
|
[item callHandler];
|
|
break;
|
|
}
|
|
case RCTDevMenuTypeToggle: {
|
|
BOOL value = [_settings[item.key] boolValue];
|
|
[self updateSetting:item.key value:@(!value)]; // will call handler
|
|
break;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
RCT_EXPORT_METHOD(reload)
|
|
{
|
|
_jsLoaded = NO;
|
|
_liveReloadURL = nil;
|
|
[_bridge reload];
|
|
}
|
|
|
|
- (void)setShakeToShow:(BOOL)shakeToShow
|
|
{
|
|
_shakeToShow = shakeToShow;
|
|
[self updateSetting:@"shakeToShow" value:@(_shakeToShow)];
|
|
}
|
|
|
|
- (void)setProfilingEnabled:(BOOL)enabled
|
|
{
|
|
_profilingEnabled = enabled;
|
|
[self updateSetting:@"profilingEnabled" value:@(_profilingEnabled)];
|
|
|
|
if (_liveReloadURL && enabled != RCTProfileIsProfiling()) {
|
|
if (enabled) {
|
|
[_bridge startProfiling];
|
|
} else {
|
|
[_bridge stopProfiling:^(NSData *logData) {
|
|
RCTProfileSendResult(_bridge, @"systrace", logData);
|
|
}];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)setLiveReloadEnabled:(BOOL)enabled
|
|
{
|
|
_liveReloadEnabled = enabled;
|
|
[self updateSetting:@"liveReloadEnabled" value:@(_liveReloadEnabled)];
|
|
|
|
if (_liveReloadEnabled) {
|
|
[self checkForUpdates];
|
|
} else {
|
|
[_updateTask cancel];
|
|
_updateTask = nil;
|
|
}
|
|
}
|
|
|
|
- (void)setExecutorClass:(Class)executorClass
|
|
{
|
|
if (_executorClass != executorClass) {
|
|
_executorClass = executorClass;
|
|
[self updateSetting:@"executorClass" value:NSStringFromClass(executorClass)];
|
|
}
|
|
|
|
if (_bridge.executorClass != executorClass) {
|
|
|
|
// TODO (6929129): we can remove this special case test once we have better
|
|
// support for custom executors in the dev menu. But right now this is
|
|
// needed to prevent overriding a custom executor with the default if a
|
|
// custom executor has been set directly on the bridge
|
|
if (executorClass == Nil &&
|
|
(_bridge.executorClass != NSClassFromString(@"RCTWebSocketExecutor") &&
|
|
_bridge.executorClass != NSClassFromString(@"RCTWebViewExecutor"))) {
|
|
return;
|
|
}
|
|
|
|
_bridge.executorClass = executorClass;
|
|
[self reload];
|
|
}
|
|
}
|
|
|
|
- (void)setShowFPS:(BOOL)showFPS
|
|
{
|
|
_showFPS = showFPS;
|
|
[self updateSetting:@"showFPS" value:@(showFPS)];
|
|
}
|
|
|
|
- (void)checkForUpdates
|
|
{
|
|
if (!_jsLoaded || !_liveReloadEnabled || !_liveReloadURL) {
|
|
return;
|
|
}
|
|
|
|
if (_updateTask) {
|
|
[_updateTask cancel];
|
|
_updateTask = nil;
|
|
return;
|
|
}
|
|
|
|
__weak RCTDevMenu *weakSelf = self;
|
|
_updateTask = [[NSURLSession sharedSession] dataTaskWithURL:_liveReloadURL completionHandler:
|
|
^(__unused NSData *data, NSURLResponse *response, NSError *error) {
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
RCTDevMenu *strongSelf = weakSelf;
|
|
if (strongSelf && strongSelf->_liveReloadEnabled) {
|
|
NSHTTPURLResponse *HTTPResponse = (NSHTTPURLResponse *)response;
|
|
if (!error && HTTPResponse.statusCode == 205) {
|
|
[strongSelf reload];
|
|
} else {
|
|
strongSelf->_updateTask = nil;
|
|
[strongSelf checkForUpdates];
|
|
}
|
|
}
|
|
});
|
|
|
|
}];
|
|
|
|
[_updateTask resume];
|
|
}
|
|
|
|
@end
|
|
|
|
#else // Unavailable when not in dev mode
|
|
|
|
@implementation RCTDevMenu
|
|
|
|
- (void)show {}
|
|
- (void)reload {}
|
|
- (void)addItem:(NSString *)title handler:(dispatch_block_t)handler {}
|
|
- (void)addItem:(RCTDevMenu *)item {}
|
|
|
|
@end
|
|
|
|
#endif
|
|
|
|
@implementation RCTBridge (RCTDevMenu)
|
|
|
|
- (RCTDevMenu *)devMenu
|
|
{
|
|
#if RCT_DEV
|
|
return self.modules[RCTBridgeModuleNameForClass([RCTDevMenu class])];
|
|
#else
|
|
return nil;
|
|
#endif
|
|
}
|
|
|
|
@end
|