From ba0792e72bb87e448bbf86f96e2ce4045b250555 Mon Sep 17 00:00:00 2001 From: Ben Alpert Date: Wed, 25 Nov 2015 18:22:10 -0800 Subject: [PATCH 1/2] Add createClass stress test Running this is left as an exercise for the reader, since my measure.py isn't designed for this at present. But something like this might work: ```diff diff --git a/scripts/bench/measure.py b/scripts/bench/measure.py index 4cedf47..627ec97 100755 --- a/scripts/bench/measure.py +++ b/scripts/bench/measure.py @@ -79,15 +79,12 @@ def _measure_ssr_ms(engine, react_path, bench_name, bench_path, measure_warm): if (typeof React !== 'object') throw new Error('React not laoded'); report('factory_ms', END - START); - globalEval(readFile(ENV.bench_path)); - if (typeof Benchmark !== 'function') { - throw new Error('benchmark not loaded'); - } + globalEval("bm = (function(){" + readFile("bench-createclass-madman.js") + "})"); + bm(); var START = now(); - var html = React.renderToString(React.createElement(Benchmark)); - html.charCodeAt(0); // flatten ropes + bm(); var END = now(); - report('ssr_' + ENV.bench_name + '_cold_ms', END - START); + report('cc_' + ENV.bench_name + '_cold_ms', END - START); var warmup = ENV.measure_warm ? 80 : 0; var trials = ENV.measure_warm ? 40 : 0; @@ -119,7 +116,7 @@ def _main(): return 1 react_path = sys.argv[1] - trials = 30 + trials = 60 sys.stderr.write("Measuring SSR for PE benchmark (%d trials)\n" % trials) for i in range(trials): for engine in [ @@ -132,7 +129,7 @@ def _main(): sys.stderr.flush() sys.stderr.write("\n") - trials = 3 + trials = 0#3 sys.stderr.write("Measuring SSR for PE with warm JIT (%d slow trials)\n" % trials) for i in range(trials): for engine in [ ``` --- scripts/bench/bench-createclass.js | 243 +++++++++++++++++++++++++++++ 1 file changed, 243 insertions(+) create mode 100644 scripts/bench/bench-createclass.js diff --git a/scripts/bench/bench-createclass.js b/scripts/bench/bench-createclass.js new file mode 100644 index 0000000000..99d7759491 --- /dev/null +++ b/scripts/bench/bench-createclass.js @@ -0,0 +1,243 @@ +React.createClass({ + displayName: "UnimplementedView", + setNativeProps: function() {}, + render: function() {}, +}); +var mixin1 = { + measure: function() {}, + measureLayout: function() {}, + setNativeProps: function() {}, + focus: function() {}, + blur: function() {}, + componentWillMount: function() {}, + componentWillReceiveProps: function() {}, +}; +React.createClass({ + displayName: "View", + mixins: [mixin1], + viewConfig: {uiViewClassName: "RCTView", validAttributes: null}, + statics: {AccessibilityTraits: null, AccessibilityComponentType: null}, + propTypes: {accessible: function() {}, accessibilityLabel: function() {}, accessibilityComponentType: function() {}, accessibilityLiveRegion: function() {}, importantForAccessibility: function() {}, accessibilityTraits: function() {}, onAccessibilityTap: function() {}, onMagicTap: function() {}, testID: function() {}, onResponderGrant: function() {}, onResponderMove: function() {}, onResponderReject: function() {}, onResponderRelease: function() {}, onResponderTerminate: function() {}, onResponderTerminationRequest: function() {}, onStartShouldSetResponder: function() {}, onStartShouldSetResponderCapture: function() {}, onMoveShouldSetResponder: function() {}, onMoveShouldSetResponderCapture: function() {}, onLayout: function() {}, pointerEvents: function() {}, style: function() {}, removeClippedSubviews: function() {}, renderToHardwareTextureAndroid: function() {}, shouldRasterizeIOS: function() {}, collapsable: function() {}, needsOffscreenAlphaCompositing: function() {}}, + render: function() {}, +}); +var mixin2 = { + componentWillUnmount: function() {}, + setTimeout: function() {}, + clearTimeout: function() {}, + setInterval: function() {}, + clearInterval: function() {}, + setImmediate: function() {}, + clearImmediate: function() {}, + requestAnimationFrame: function() {}, + cancelAnimationFrame: function() {}, +}; +var mixin3 = { + componentWillUnmount: function() {}, + _interactionMixinHandles: {}, + createInteractionHandle: function() {}, + clearInteractionHandle: function() {}, + runAfterInteractions: function() {}, +}; +var mixin4 = { + componentWillMount: function() {}, + componentWillUnmount: function() {}, + addListenerOn: function() {}, +}; +React.createClass({ + displayName: "Navigator", + propTypes: {configureScene: function() {}, renderScene: function() {}, initialRoute: function() {}, initialRouteStack: function() {}, onWillFocus: function() {}, onDidFocus: function() {}, navigationBar: function() {}, navigator: function() {}, sceneStyle: function() {}}, + statics: {BreadcrumbNavigationBar: null, NavigationBar: null, SceneConfigs: null}, + mixins: [mixin2, mixin3, mixin4], + getDefaultProps: function() {}, + getInitialState: function() {}, + componentWillMount: function() {}, + componentDidMount: function() {}, + componentWillUnmount: function() {}, + immediatelyResetRouteStack: function() {}, + _transitionTo: function() {}, + _handleSpringUpdate: function() {}, + _completeTransition: function() {}, + _emitDidFocus: function() {}, + _emitWillFocus: function() {}, + _hideScenes: function() {}, + _disableScene: function() {}, + _enableScene: function() {}, + _onAnimationStart: function() {}, + _onAnimationEnd: function() {}, + _setRenderSceneToHardwareTextureAndroid: function() {}, + _handleTouchStart: function() {}, + _handleMoveShouldSetPanResponder: function() {}, + _doesGestureOverswipe: function() {}, + _deltaForGestureAction: function() {}, + _handlePanResponderRelease: function() {}, + _handlePanResponderTerminate: function() {}, + _attachGesture: function() {}, + _detachGesture: function() {}, + _handlePanResponderMove: function() {}, + _moveAttachedGesture: function() {}, + _matchGestureAction: function() {}, + _transitionSceneStyle: function() {}, + _transitionBetween: function() {}, + _handleResponderTerminationRequest: function() {}, + _getDestIndexWithinBounds: function() {}, + _jumpN: function() {}, + jumpTo: function() {}, + jumpForward: function() {}, + jumpBack: function() {}, + push: function() {}, + _popN: function() {}, + pop: function() {}, + replaceAtIndex: function() {}, + replace: function() {}, + replacePrevious: function() {}, + popToTop: function() {}, + popToRoute: function() {}, + replacePreviousAndPop: function() {}, + resetTo: function() {}, + getCurrentRoutes: function() {}, + _cleanScenesPastIndex: function() {}, + _renderScene: function() {}, + _renderNavigationBar: function() {}, + render: function() {}, + _getNavigationContext: function() {}, +}); +var mixin5 = { + mixins: [mixin4], + statics: {DecelerationRate: null}, + scrollResponderMixinGetInitialState: function() {}, + scrollResponderHandleScrollShouldSetResponder: function() {}, + scrollResponderHandleStartShouldSetResponder: function() {}, + scrollResponderHandleStartShouldSetResponderCapture: function() {}, + scrollResponderHandleResponderReject: function() {}, + scrollResponderHandleTerminationRequest: function() {}, + scrollResponderHandleTouchEnd: function() {}, + scrollResponderHandleResponderRelease: function() {}, + scrollResponderHandleScroll: function() {}, + scrollResponderHandleResponderGrant: function() {}, + scrollResponderHandleScrollBeginDrag: function() {}, + scrollResponderHandleScrollEndDrag: function() {}, + scrollResponderHandleMomentumScrollBegin: function() {}, + scrollResponderHandleMomentumScrollEnd: function() {}, + scrollResponderHandleTouchStart: function() {}, + scrollResponderHandleTouchMove: function() {}, + scrollResponderIsAnimating: function() {}, + scrollResponderScrollTo: function() {}, + scrollResponderScrollWithouthAnimationTo: function() {}, + scrollResponderZoomTo: function() {}, + scrollResponderScrollNativeHandleToKeyboard: function() {}, + scrollResponderInputMeasureAndScrollToKeyboard: function() {}, + scrollResponderTextInputFocusError: function() {}, + componentWillMount: function() {}, + scrollResponderKeyboardWillShow: function() {}, + scrollResponderKeyboardWillHide: function() {}, + scrollResponderKeyboardDidShow: function() {}, + scrollResponderKeyboardDidHide: function() {}, +}; +React.createClass({ + displayName: "ScrollView", + propTypes: {accessible: function() {}, accessibilityLabel: function() {}, accessibilityComponentType: function() {}, accessibilityLiveRegion: function() {}, importantForAccessibility: function() {}, accessibilityTraits: function() {}, onAccessibilityTap: function() {}, onMagicTap: function() {}, testID: function() {}, onResponderGrant: function() {}, onResponderMove: function() {}, onResponderReject: function() {}, onResponderRelease: function() {}, onResponderTerminate: function() {}, onResponderTerminationRequest: function() {}, onStartShouldSetResponder: function() {}, onStartShouldSetResponderCapture: function() {}, onMoveShouldSetResponder: function() {}, onMoveShouldSetResponderCapture: function() {}, onLayout: function() {}, pointerEvents: function() {}, style: function() {}, removeClippedSubviews: function() {}, renderToHardwareTextureAndroid: function() {}, shouldRasterizeIOS: function() {}, collapsable: function() {}, needsOffscreenAlphaCompositing: function() {}, automaticallyAdjustContentInsets: function() {}, contentInset: function() {}, contentOffset: function() {}, bounces: function() {}, bouncesZoom: function() {}, alwaysBounceHorizontal: function() {}, alwaysBounceVertical: function() {}, centerContent: function() {}, contentContainerStyle: function() {}, decelerationRate: function() {}, horizontal: function() {}, directionalLockEnabled: function() {}, canCancelContentTouches: function() {}, keyboardDismissMode: function() {}, keyboardShouldPersistTaps: function() {}, maximumZoomScale: function() {}, minimumZoomScale: function() {}, onScroll: function() {}, onScrollAnimationEnd: function() {}, onContentSizeChange: function() {}, pagingEnabled: function() {}, scrollEnabled: function() {}, scrollEventThrottle: function() {}, scrollIndicatorInsets: function() {}, scrollsToTop: function() {}, showsHorizontalScrollIndicator: function() {}, showsVerticalScrollIndicator: function() {}, stickyHeaderIndices: function() {}, snapToInterval: function() {}, snapToAlignment: function() {}, zoomScale: function() {}, onRefreshStart: function() {}}, + mixins: [mixin5], + getInitialState: function() {}, + setNativeProps: function() {}, + endRefreshing: function() {}, + getScrollResponder: function() {}, + getInnerViewNode: function() {}, + scrollTo: function() {}, + scrollWithoutAnimationTo: function() {}, + handleScroll: function() {}, + _handleContentOnLayout: function() {}, + render: function() {}, +}); +var mixin6 = { + componentWillUnmount: function() {}, + setTimeout: function() {}, + clearTimeout: function() {}, + setInterval: function() {}, + clearInterval: function() {}, + setImmediate: function() {}, + clearImmediate: function() {}, + requestAnimationFrame: function() {}, + cancelAnimationFrame: function() {}, +}; +React.createClass({ + displayName: "AdsManagerTabsModalView", + contextTypes: {navigation: function() {}}, + mixins: [mixin6, mixin4], + getDefaultProps: function() {}, + getInitialState: function() {}, + componentWillMount: function() {}, + componentDidMount: function() {}, + componentWillUnmount: function() {}, + onTabSelect: function() {}, + _handleConnectivityChange: function() {}, + _onTabTap: function() {}, + startCreateFlow: function() {}, + render: function() {}, + _renderTabs: function() {}, + _onAdCreated: function() {}, + _onRemoteNotification: function() {}, + _setAccountFromURL: function() {}, + _updateBadgeCount: function() {}, + _onRetry: function() {}, + _getUnseenNotifsCount: function() {}, + _pushNotifPermalink: function() {}, + _onMobileConfigsLoadDone: function() {}, + _getAccountRoute: function() {}, + _getNotifsRoute: function() {}, + _getSettingsRoute: function() {}, + _getCampaignsRoute: function() {}, + _getNavStack: function() {}, + _onAccountChanged: function() {}, + _onHelpCenterRequested: function() {}, + _onGlobalError: function() {}, + _onGlobalErrorToastDidClose: function() {}, + _onShowNUX: function() {}, + _closeToastAndPopover: function() {}, + _onTabLayout: function() {}, + _onRootNavigationWillChange: function() {}, + _handleOpenURL: function() {}, + _updateAccountIDFromInLink: function() {}, + _pushInLink: function() {}, + _pushExternalRoute: function() {}, + _canPushExernalRoute: function() {}, + _hasAccountTab: function() {}, + _hasCampaignsTab: function() {}, + _getTabForURL: function() {}, + _getTabForExternalRoute: function() {}, +}); +React.createClass({ + displayName: "ActionBarButton", + mixins: [mixin1, mixin6], + propTypes: {label: function() {}, iconOnly: function() {}, imageSource: function() {}, style: function() {}, onContentMeasured: function() {}, onPress: function() {}}, + componentDidMount: function() {}, + render: function() {}, +}); +React.createClass({ + displayName: "ListView", + mixins: [mixin5, mixin2], + statics: {DataSource: null}, + propTypes: {accessible: function() {}, accessibilityLabel: function() {}, accessibilityComponentType: function() {}, accessibilityLiveRegion: function() {}, importantForAccessibility: function() {}, accessibilityTraits: function() {}, onAccessibilityTap: function() {}, onMagicTap: function() {}, testID: function() {}, onResponderGrant: function() {}, onResponderMove: function() {}, onResponderReject: function() {}, onResponderRelease: function() {}, onResponderTerminate: function() {}, onResponderTerminationRequest: function() {}, onStartShouldSetResponder: function() {}, onStartShouldSetResponderCapture: function() {}, onMoveShouldSetResponder: function() {}, onMoveShouldSetResponderCapture: function() {}, onLayout: function() {}, pointerEvents: function() {}, style: function() {}, removeClippedSubviews: function() {}, renderToHardwareTextureAndroid: function() {}, shouldRasterizeIOS: function() {}, collapsable: function() {}, needsOffscreenAlphaCompositing: function() {}, automaticallyAdjustContentInsets: function() {}, contentInset: function() {}, contentOffset: function() {}, bounces: function() {}, bouncesZoom: function() {}, alwaysBounceHorizontal: function() {}, alwaysBounceVertical: function() {}, centerContent: function() {}, contentContainerStyle: function() {}, decelerationRate: function() {}, horizontal: function() {}, directionalLockEnabled: function() {}, canCancelContentTouches: function() {}, keyboardDismissMode: function() {}, keyboardShouldPersistTaps: function() {}, maximumZoomScale: function() {}, minimumZoomScale: function() {}, onScroll: function() {}, onScrollAnimationEnd: function() {}, onContentSizeChange: function() {}, pagingEnabled: function() {}, scrollEnabled: function() {}, scrollEventThrottle: function() {}, scrollIndicatorInsets: function() {}, scrollsToTop: function() {}, showsHorizontalScrollIndicator: function() {}, showsVerticalScrollIndicator: function() {}, stickyHeaderIndices: function() {}, snapToInterval: function() {}, snapToAlignment: function() {}, zoomScale: function() {}, onRefreshStart: function() {}, dataSource: function() {}, renderSeparator: function() {}, renderRow: function() {}, initialListSize: function() {}, onEndReached: function() {}, onEndReachedThreshold: function() {}, pageSize: function() {}, renderFooter: function() {}, renderHeader: function() {}, renderSectionHeader: function() {}, renderScrollComponent: function() {}, scrollRenderAheadDistance: function() {}, onChangeVisibleRows: function() {}}, + getMetrics: function() {}, + getScrollResponder: function() {}, + setNativeProps: function() {}, + getDefaultProps: function() {}, + getInitialState: function() {}, + getInnerViewNode: function() {}, + componentWillMount: function() {}, + componentDidMount: function() {}, + componentWillReceiveProps: function() {}, + componentDidUpdate: function() {}, + onRowHighlighted: function() {}, + render: function() {}, + _measureAndUpdateScrollProps: function() {}, + _onContentSizeChange: function() {}, + _onLayout: function() {}, + _setScrollVisibleLength: function() {}, + _updateChildFrames: function() {}, + _maybeCallOnEndReached: function() {}, + _renderMoreRowsIfNeeded: function() {}, + _pageInNewRows: function() {}, + _getDistanceFromEnd: function() {}, + _updateVisibleRows: function() {}, + _onScroll: function() {}, +}); From 50c81d5fe68096fec9e4c5ff6c6cee73cd07704e Mon Sep 17 00:00:00 2001 From: Ben Alpert Date: Wed, 25 Nov 2015 18:23:31 -0800 Subject: [PATCH 2/2] Make createClass 10-15% faster on complex specs --- src/isomorphic/classic/class/ReactClass.js | 33 +++++++++++----------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/src/isomorphic/classic/class/ReactClass.js b/src/isomorphic/classic/class/ReactClass.js index c606d625c2..441f3484f1 100644 --- a/src/isomorphic/classic/class/ReactClass.js +++ b/src/isomorphic/classic/class/ReactClass.js @@ -414,7 +414,7 @@ function validateTypeDef(Constructor, typeDef, location) { } } -function validateMethodOverride(proto, name) { +function validateMethodOverride(isAlreadyDefined, name) { var specPolicy = ReactClassInterface.hasOwnProperty(name) ? ReactClassInterface[name] : null; @@ -431,7 +431,7 @@ function validateMethodOverride(proto, name) { } // Disallow defining methods more than once unless explicitly allowed. - if (proto.hasOwnProperty(name)) { + if (isAlreadyDefined) { invariant( specPolicy === SpecPolicy.DEFINE_MANY || specPolicy === SpecPolicy.DEFINE_MANY_MERGED, @@ -465,6 +465,7 @@ function mixSpecIntoComponent(Constructor, spec) { ); var proto = Constructor.prototype; + var autoBindPairs = proto.__reactAutoBindPairs; // By handling mixins before any other properties, we ensure the same // chaining order is applied to methods with DEFINE_MANY policy, whether @@ -484,7 +485,8 @@ function mixSpecIntoComponent(Constructor, spec) { } var property = spec[name]; - validateMethodOverride(proto, name); + var isAlreadyDefined = proto.hasOwnProperty(name); + validateMethodOverride(isAlreadyDefined, name); if (RESERVED_SPEC_KEYS.hasOwnProperty(name)) { RESERVED_SPEC_KEYS[name](Constructor, property); @@ -495,7 +497,6 @@ function mixSpecIntoComponent(Constructor, spec) { // 2. Overridden methods (that were mixed in). var isReactClassMethod = ReactClassInterface.hasOwnProperty(name); - var isAlreadyDefined = proto.hasOwnProperty(name); var isFunction = typeof property === 'function'; var shouldAutoBind = isFunction && @@ -504,10 +505,7 @@ function mixSpecIntoComponent(Constructor, spec) { spec.autobind !== false; if (shouldAutoBind) { - if (!proto.__reactAutoBindMap) { - proto.__reactAutoBindMap = {}; - } - proto.__reactAutoBindMap[name] = property; + autoBindPairs.push(name, property); proto[name] = property; } else { if (isAlreadyDefined) { @@ -700,14 +698,14 @@ function bindAutoBindMethod(component, method) { * @param {object} component Component whose method is going to be bound. */ function bindAutoBindMethods(component) { - for (var autoBindKey in component.__reactAutoBindMap) { - if (component.__reactAutoBindMap.hasOwnProperty(autoBindKey)) { - var method = component.__reactAutoBindMap[autoBindKey]; - component[autoBindKey] = bindAutoBindMethod( - component, - method - ); - } + var pairs = component.__reactAutoBindPairs; + for (var i = 0; i < pairs.length; i += 2) { + var autoBindKey = pairs[i]; + var method = pairs[i + 1]; + component[autoBindKey] = bindAutoBindMethod( + component, + method + ); } } @@ -812,7 +810,7 @@ var ReactClass = { } // Wire up auto-binding - if (this.__reactAutoBindMap) { + if (this.__reactAutoBindPairs.length) { bindAutoBindMethods(this); } @@ -846,6 +844,7 @@ var ReactClass = { }; Constructor.prototype = new ReactClassComponent(); Constructor.prototype.constructor = Constructor; + Constructor.prototype.__reactAutoBindPairs = []; injectedMixins.forEach( mixSpecIntoComponent.bind(null, Constructor)