From 0126b312c74bda4379779d2fb0cfa12548a31727 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Thu, 24 May 2018 13:44:41 -0700 Subject: [PATCH 001/135] Added package script shortcut for testing plain shell --- package.json | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 42bd8dafac..f48be320d0 100644 --- a/package.json +++ b/package.json @@ -38,11 +38,10 @@ "license": "BSD-3-Clause", "repository": "facebook/react-devtools", "private": true, - "workspaces": [ - "packages/*" - ], + "workspaces": ["packages/*"], "scripts": { - "build:extension": "yarn run build:extension:chrome && yarn run build:extension:firefox", + "build:extension": + "yarn run build:extension:chrome && yarn run build:extension:firefox", "build:extension:chrome": "node ./shells/chrome/build", "build:extension:firefox": "node ./shells/firefox/build", "build:standalone": "cd packages/react-devtools-core && yarn run build", @@ -51,13 +50,12 @@ "test": "jest", "test:chrome": "node ./shells/chrome/test", "test:firefox": "node ./shells/firefox/test", + "test:plain": "cd ./shells/plain && ./build.sh --watch", "test:standalone": "cd packages/react-devtools && yarn start", "typecheck": "flow check" }, "jest": { - "modulePathIgnorePatterns": [ - "/shells" - ] + "modulePathIgnorePatterns": ["/shells"] }, "devDependencies": { "chrome-launch": "^1.1.4", From dcff7db42d22011f07bb9983abc62fe48cef8520 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Wed, 30 May 2018 11:10:03 -0700 Subject: [PATCH 002/135] Added record profile button and ooked up to Store/Agent/Hook --- agent/Agent.js | 7 ++++ agent/inject.js | 3 +- backend/attachRendererFiber.js | 1 + backend/getDataFiber.js | 10 +++++ backend/installGlobalHook.js | 2 + frontend/Icons.js | 3 ++ frontend/SettingsPane.js | 50 ++++++++++++++++++++++- frontend/Store.js | 9 +++++ plugins/Profiler/ProfilerManager.js | 61 +++++++++++++++++++++++++++++ shells/chrome/test.js | 2 +- shells/plain/backend.js | 2 + shells/webextension/src/backend.js | 2 + 12 files changed, 149 insertions(+), 3 deletions(-) create mode 100644 plugins/Profiler/ProfilerManager.js diff --git a/agent/Agent.js b/agent/Agent.js index 4992a5d00c..6f6edf5c02 100644 --- a/agent/Agent.js +++ b/agent/Agent.js @@ -161,6 +161,7 @@ class Agent extends EventEmitter { bridge.on('startInspecting', () => this.emit('startInspecting')); bridge.on('stopInspecting', () => this.emit('stopInspecting')); bridge.on('selected', id => this.emit('selected', id)); + bridge.on('isRecording', isRecording => this.emit('isRecording', isRecording)); bridge.on('setInspectEnabled', enabled => { this._inspectEnabled = enabled; this.emit('stopInspecting'); @@ -218,6 +219,7 @@ class Agent extends EventEmitter { }); this.on('setSelection', data => bridge.send('select', data)); this.on('setInspectEnabled', data => bridge.send('setInspectEnabled', data)); + this.on('setIsRecording', data => bridge.send('setIsRecording', data)); } scrollToNode(id: ElementID): void { @@ -375,6 +377,11 @@ class Agent extends EventEmitter { this.emit('root', id); } + commitRoot(renderer: RendererID, internalInstance: OpaqueNodeHandle) { + var id = this.getId(internalInstance); + this.emit('commitRoot', id); + } + onMounted(renderer: RendererID, component: OpaqueNodeHandle, data: DataType) { var id = this.getId(component); this.renderers.set(id, renderer); diff --git a/agent/inject.js b/agent/inject.js index 0a05f60fc3..a92f94504b 100644 --- a/agent/inject.js +++ b/agent/inject.js @@ -21,8 +21,9 @@ module.exports = function(hook: Hook, agent: Agent) { agent.setReactInternals(id, helpers); helpers.walkTree(agent.onMounted.bind(agent, id), agent.addRoot.bind(agent, id)); }), - hook.sub('root', ({renderer, internalInstance}) => agent.addRoot(renderer, internalInstance)), + hook.sub('commitRoot', ({rendererID, root}) => agent.commitRoot(rendererID, root)), hook.sub('mount', ({renderer, internalInstance, data}) => agent.onMounted(renderer, internalInstance, data)), + hook.sub('root', ({renderer, internalInstance}) => agent.addRoot(renderer, internalInstance)), hook.sub('update', ({renderer, internalInstance, data}) => agent.onUpdated(internalInstance, data)), hook.sub('unmount', ({renderer, internalInstance}) => agent.onUnmounted(internalInstance)), ]; diff --git a/backend/attachRendererFiber.js b/backend/attachRendererFiber.js index e50739e2aa..b76a2f143f 100644 --- a/backend/attachRendererFiber.js +++ b/backend/attachRendererFiber.js @@ -226,6 +226,7 @@ function attachRendererFiber(hook: Hook, rid: string, renderer: ReactRenderer): function handleCommitFiberRoot(root) { const current = root.current; const alternate = current.alternate; + if (alternate) { // TODO: relying on this seems a bit fishy. const wasMounted = alternate.memoizedState != null && alternate.memoizedState.element != null; diff --git a/backend/getDataFiber.js b/backend/getDataFiber.js index c5d64a1e67..e76db8298a 100644 --- a/backend/getDataFiber.js +++ b/backend/getDataFiber.js @@ -55,6 +55,7 @@ function getDataFiber(fiber: Object, getOpaqueNode: (fiber: Object) => Object): var nodeType = null; var name = null; var text = null; + var profilerData = null; switch (fiber.tag) { case FunctionalComponent: @@ -206,6 +207,14 @@ function getDataFiber(fiber: Object, getOpaqueNode: (fiber: Object) => Object): } } + if (fiber.actualDuration !== undefined) { + profilerData = { + actualDuration: fiber.actualDuration, + actualStartTime: fiber.actualStartTime, + baseTime: fiber.treeBaseTime, + }; + } + // $FlowFixMe return { nodeType, @@ -214,6 +223,7 @@ function getDataFiber(fiber: Object, getOpaqueNode: (fiber: Object) => Object): ref, source, name, + profilerData, props, state, context, diff --git a/backend/installGlobalHook.js b/backend/installGlobalHook.js index e94f0aa74e..70a519bdc9 100644 --- a/backend/installGlobalHook.js +++ b/backend/installGlobalHook.js @@ -215,6 +215,8 @@ function installGlobalHook(window: Object) { } }, onCommitFiberRoot: function(rendererID, root) { + hook.emit('commitRoot', {rendererID, root}); + const mountedRoots = hook.getFiberRoots(rendererID); const current = root.current; const isKnownRoot = mountedRoots.has(root); diff --git a/frontend/Icons.js b/frontend/Icons.js index 6ce13c4c96..f2995f4b36 100644 --- a/frontend/Icons.js +++ b/frontend/Icons.js @@ -27,6 +27,9 @@ const Icons = { 11H23V13H20.95C20.5,17.17 17.17,20.5 13,20.95V23H11V20.95C6.83,20.5 3.5,17.17 3.05, 13M12,5A7,7 0 0,0 5,12A7,7 0 0,0 12,19A7,7 0 0,0 19,12A7,7 0 0,0 12,5Z `, + RECORD: ` + M4,12a8,8 0 1,0 16,0a8,8 0 1,0 -16,0 + `, SEARCH: ` M31.008 27.231l-7.58-6.447c-0.784-0.705-1.622-1.029-2.299-0.998 1.789-2.096 2.87-4.815 2.87-7.787 0-6.627-5.373-12-12-12s-12 5.373-12 12 5.373 12 12 12c2.972 0 5.691-1.081 diff --git a/frontend/SettingsPane.js b/frontend/SettingsPane.js index dc9ecbb4de..14be5b86f7 100644 --- a/frontend/SettingsPane.js +++ b/frontend/SettingsPane.js @@ -119,6 +119,17 @@ class SettingsPane extends React.Component { /> )} + {/* + TODO (bvaughn) + Only enable if ProfileMode exists and is supported. + Maybe we can determine this by looking for an "actualDuration" field on the root Fiber? + */} + + store.changeSearch(text), searchText: store.searchText, selectFirstSearchResult: store.selectFirstSearchResult.bind(store), @@ -179,6 +195,7 @@ var Wrapped = decorate({ store.showPreferencesPanel(); }, toggleInspectEnabled: () => store.setInspectEnabled(!store.isInspectEnabled), + toggleRecord: () => store.setIsRecording(!store.isRecording), }; }, }, SettingsPane); @@ -224,6 +241,23 @@ const SettingsMenuButton = Hoverable( ) ); +const RecordMenuButton = Hoverable( + ({ isActive, isHovered, onClick, onMouseEnter, onMouseLeave, theme }) => ( + + ) +); + function SearchIcon({ theme }) { return ( ({ fontSize: sansSerif.sizes.normal, }); +const recordMenuButtonStyle = (isActive: boolean, isHovered: boolean, theme: Theme) => ({ + display: 'flex', + background: 'none', + border: 'none', + outline: 'none', + color: isActive + ? theme.special03 + : isHovered ? theme.state06 : 'inherit', + filter: isActive + ? `drop-shadow( 0 0 2px ${theme.special03} )` + : 'none', +}); + const settingsMenuButtonStyle = (isHovered: boolean, theme: Theme) => ({ display: 'flex', background: 'none', border: 'none', + outline: 'none', marginRight: '0.5rem', color: isHovered ? theme.state06 : 'inherit', }); diff --git a/frontend/Store.js b/frontend/Store.js index 41a684dd4e..87bed151b1 100644 --- a/frontend/Store.js +++ b/frontend/Store.js @@ -93,6 +93,7 @@ class Store extends EventEmitter { // Public state isInspectEnabled: boolean; + isRecording: boolean; traceupdatesState: ?ControlState; colorizerState: ?ControlState; contextMenu: ?ContextMenu; @@ -125,6 +126,7 @@ class Store extends EventEmitter { // Public state this.isInspectEnabled = false; + this.isRecording = false; this.roots = new List(); this.contextMenu = null; this.searchRoots = null; @@ -163,6 +165,7 @@ class Store extends EventEmitter { this._bridge.on('update', (data) => this._updateComponent(data)); this._bridge.on('unmount', id => this._unmountComponent(id)); this._bridge.on('setInspectEnabled', (data) => this.setInspectEnabled(data)); + this._bridge.on('setIsRecording', (data) => this.setIsRecording(data)); this._bridge.on('select', ({id, quiet, offsetFromLeaf = 0}) => { // Backtrack if we want to skip leaf nodes while (offsetFromLeaf > 0) { @@ -572,6 +575,12 @@ class Store extends EventEmitter { this._bridge.send('setInspectEnabled', isInspectEnabled); } + setIsRecording(isRecording: boolean) { + this.isRecording = isRecording; + this.emit('isRecording'); + this._bridge.send('isRecording', isRecording); + } + // Private stuff _establishConnection() { var tries = 0; diff --git a/plugins/Profiler/ProfilerManager.js b/plugins/Profiler/ProfilerManager.js new file mode 100644 index 0000000000..8b590db674 --- /dev/null +++ b/plugins/Profiler/ProfilerManager.js @@ -0,0 +1,61 @@ +/** + * 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. + * + * @flow + */ + +'use strict'; + +type Agent = any; + +class ProfilerManager { + _agent: Agent; + _commitTime: number = 0; + _isRecording: boolean = false; + + constructor(agent: Agent) { + this._agent = agent; + + agent.on('commitRoot', this._onCommitRoot); + agent.on('isRecording', this._onIsRecording); + agent.on('mount', this._onMountOrUpdate); + agent.on('update', this._onMountOrUpdate); + } + + _onCommitRoot = id => { + // This will not match the commit time logged to Profilers in this commit, + // But that's probably okay. + // DevTools only needs it to group all of the profile timings, + // And to place them at a certain point in time in the replay view. + this._commitTime = performance.now(); + }; + + _onMountOrUpdate = (data: any) => { + if (!this._isRecording || !data.profilerData) { + return; + } + + // TODO If we're in profiling mode, loop through events and take snapsot. + } + + _onIsRecording = isRecording => { + this._isRecording = isRecording; + + if (!isRecording) { + // TODO: Dump previous data if we + } + }; +} + +function init(agent: Agent): ProfilerManager { + return new ProfilerManager(agent); +} + +module.exports = { + init, +}; diff --git a/shells/chrome/test.js b/shells/chrome/test.js index 3876c5d5a1..8d5aefdf83 100644 --- a/shells/chrome/test.js +++ b/shells/chrome/test.js @@ -4,7 +4,7 @@ const chromeLaunch = require('chrome-launch'); // eslint-disable-line import/no- const {resolve} = require('path'); const EXTENSION_PATH = resolve('shells/chrome/build/unpacked'); -const START_URL = 'https://facebook.github.io/react/'; +const START_URL = 'http://localhost:3000'; chromeLaunch(START_URL, { args: [`--load-extension=${EXTENSION_PATH}`], diff --git a/shells/plain/backend.js b/shells/plain/backend.js index ba31a8df3e..cfbd32756b 100644 --- a/shells/plain/backend.js +++ b/shells/plain/backend.js @@ -11,6 +11,7 @@ 'use strict'; var Agent = require('../../agent/Agent'); +var ProfilerManager = require('../../plugins/Profiler/ProfilerManager'); var TraceUpdatesBackendManager = require('../../plugins/TraceUpdates/TraceUpdatesBackendManager'); var Bridge = require('../../agent/Bridge'); var setupHighlighter = require('../../frontend/Highlighter/setup'); @@ -39,4 +40,5 @@ inject(window.__REACT_DEVTOOLS_GLOBAL_HOOK__, agent); setupHighlighter(agent); setupRelay(bridge, agent, window.__REACT_DEVTOOLS_GLOBAL_HOOK__); +ProfilerManager.init(agent); TraceUpdatesBackendManager.init(agent); diff --git a/shells/webextension/src/backend.js b/shells/webextension/src/backend.js index 53bfea5d1f..a85b2dfc62 100644 --- a/shells/webextension/src/backend.js +++ b/shells/webextension/src/backend.js @@ -11,6 +11,7 @@ 'use strict'; var Agent = require('../../../agent/Agent'); +var ProfilerManager = require('../../../plugins/Profiler/ProfilerManager'); var TraceUpdatesBackendManager = require('../../../plugins/TraceUpdates/TraceUpdatesBackendManager'); var Bridge = require('../../../agent/Bridge'); var inject = require('../../../agent/inject'); @@ -79,5 +80,6 @@ function setup(hook) { }); setupHighlighter(agent); + ProfilerManager.init(agent); TraceUpdatesBackendManager.init(agent); } From ecd2b8c4b84877bf2051832c5a3ce294597d3631 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Wed, 30 May 2018 11:27:08 -0700 Subject: [PATCH 003/135] ProfilerManager accummulates commit snapsots --- .gitignore | 1 + plugins/Profiler/ProfilerManager.js | 29 +++++++++++++++++++++++++---- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 1c37934e17..40bfd3b998 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,6 @@ build node_modules npm-debug.log +yarn-error.log .DS_Store diff --git a/plugins/Profiler/ProfilerManager.js b/plugins/Profiler/ProfilerManager.js index 8b590db674..2b2797fc77 100644 --- a/plugins/Profiler/ProfilerManager.js +++ b/plugins/Profiler/ProfilerManager.js @@ -13,10 +13,21 @@ type Agent = any; +type Snapshot = { + actualDuration: number, + actualStartTime: number, + baseTime: number, + commitTime: number, + name: string, +}; + +type Snapshots = {[commitTime: number]: Array}; + class ProfilerManager { _agent: Agent; _commitTime: number = 0; _isRecording: boolean = false; + _snapshots: Snapshots = {}; constructor(agent: Agent) { this._agent = agent; @@ -33,6 +44,10 @@ class ProfilerManager { // DevTools only needs it to group all of the profile timings, // And to place them at a certain point in time in the replay view. this._commitTime = performance.now(); + + if (this._isRecording) { + this._snapshots[this._commitTime] = []; + } }; _onMountOrUpdate = (data: any) => { @@ -40,14 +55,20 @@ class ProfilerManager { return; } - // TODO If we're in profiling mode, loop through events and take snapsot. - } + this._snapshots[this._commitTime].push({ + actualDuration: data.profilerData.actualDuration, + actualStartTime: data.profilerData.actualStartTime, + baseTime: data.profilerData.baseTime, + commitTime: this._commitTime, // TODO This is redundant. Maybe ditch it? + name: data.name, + }); + }; _onIsRecording = isRecording => { this._isRecording = isRecording; - if (!isRecording) { - // TODO: Dump previous data if we + console.log(this._snapshots); // TODO Debugging only; remove this. + this._snapshots = {}; } }; } From 245683db85fae313880595316ced71e640cd9844 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Wed, 30 May 2018 11:46:01 -0700 Subject: [PATCH 004/135] ProfilerManager turns profiling on/off for roots --- plugins/Profiler/ProfilerManager.js | 35 +++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/plugins/Profiler/ProfilerManager.js b/plugins/Profiler/ProfilerManager.js index 2b2797fc77..40b1417a25 100644 --- a/plugins/Profiler/ProfilerManager.js +++ b/plugins/Profiler/ProfilerManager.js @@ -21,13 +21,15 @@ type Snapshot = { name: string, }; -type Snapshots = {[commitTime: number]: Array}; +// TODO (bvaughn) Should this live in a shared constants file like ReactSymbols? +// Or should it be in a Fiber-specific file somewhere (like getData)? +const ProfileMode = 0b100; class ProfilerManager { _agent: Agent; _commitTime: number = 0; _isRecording: boolean = false; - _snapshots: Snapshots = {}; + _snapshots: {[commitTime: number]: Array} = {}; constructor(agent: Agent) { this._agent = agent; @@ -59,15 +61,40 @@ class ProfilerManager { actualDuration: data.profilerData.actualDuration, actualStartTime: data.profilerData.actualStartTime, baseTime: data.profilerData.baseTime, - commitTime: this._commitTime, // TODO This is redundant. Maybe ditch it? + commitTime: this._commitTime, // TODO (bvaughn) This is redundant. Maybe ditch it? name: data.name, }); }; _onIsRecording = isRecording => { this._isRecording = isRecording; + + // Flip ProfilerMode on or off for each root. + // This instructs React to collect profiling data for the tree. + this._agent.roots.forEach(id => { + const root = this._agent.internalInstancesById.get(id); + + // TODO (bvaughn) This check is flimsy. + if (root.name === 'Profiler') { + return; + } + + if (isRecording) { + root.mode |= ProfileMode; // eslint-disable-line no-bitwise + if (root.alternate !== null) { + root.alternate.mode |= ProfileMode; // eslint-disable-line no-bitwise + } + } else { + root.mode &= ~ProfileMode; // eslint-disable-line no-bitwise + if (root.alternate !== null) { + root.alternate.mode &= ~ProfileMode; // eslint-disable-line no-bitwise + } + } + }); + + // Dump snapshot data if we are done profiling. if (!isRecording) { - console.log(this._snapshots); // TODO Debugging only; remove this. + console.log(this._snapshots); // TODO (bvaughn) Debugging only; remove this. this._snapshots = {}; } }; From 6f6d3ac2113d11ce6ccf60dcadc34f4c7126b104 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Wed, 30 May 2018 13:28:24 -0700 Subject: [PATCH 005/135] Deeply enable ProfileMode for all roots --- plugins/Profiler/ProfilerManager.js | 55 ++++++++++++++++++----------- 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/plugins/Profiler/ProfilerManager.js b/plugins/Profiler/ProfilerManager.js index 40b1417a25..c0f57eea9a 100644 --- a/plugins/Profiler/ProfilerManager.js +++ b/plugins/Profiler/ProfilerManager.js @@ -57,6 +57,9 @@ class ProfilerManager { return; } + // TODO (bvaughn) Do I need to capture hierarchical information as well? + // So the resulting flame graph can mirror the tree structure somehow? + // Or do we want to always sort by most expensive to least expensive? this._snapshots[this._commitTime].push({ actualDuration: data.profilerData.actualDuration, actualStartTime: data.profilerData.actualStartTime, @@ -66,31 +69,41 @@ class ProfilerManager { }); }; + // Deeply enables ProfileMode. + // Newly inserted Fibers will inherit the mode, + // But existing Fibers need to be explicitly activated. + _enableProfileMode = fiber => { + // eslint-disable-next-line no-bitwise + if (fiber.mode & ProfileMode) { + // Bailout if profiling is already enabled for the subtree. + return; + } + + fiber.mode |= ProfileMode; // eslint-disable-line no-bitwise + if (fiber.alternate !== null) { + fiber.alternate.mode |= ProfileMode; // eslint-disable-line no-bitwise + } + + if (fiber.child !== null) { + this._enableProfileMode(fiber.child); + } + if (fiber.sibling !== null) { + this._enableProfileMode(fiber.sibling); + } + }; + _onIsRecording = isRecording => { this._isRecording = isRecording; - // Flip ProfilerMode on or off for each root. + // Flip ProfilerMode on or off for each tree. // This instructs React to collect profiling data for the tree. - this._agent.roots.forEach(id => { - const root = this._agent.internalInstancesById.get(id); - - // TODO (bvaughn) This check is flimsy. - if (root.name === 'Profiler') { - return; - } - - if (isRecording) { - root.mode |= ProfileMode; // eslint-disable-line no-bitwise - if (root.alternate !== null) { - root.alternate.mode |= ProfileMode; // eslint-disable-line no-bitwise - } - } else { - root.mode &= ~ProfileMode; // eslint-disable-line no-bitwise - if (root.alternate !== null) { - root.alternate.mode &= ~ProfileMode; // eslint-disable-line no-bitwise - } - } - }); + // Once profiling is enabled, we just leave it one (for simplicity). + // This way we don't risk turning it off for Fibers. + if (isRecording) { + this._agent.roots.forEach(id => { + this._enableProfileMode(this._agent.internalInstancesById.get(id)); + }); + } // Dump snapshot data if we are done profiling. if (!isRecording) { From 2d44eb2f47a3d585fa196f81a197cd1bd837fae9 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Wed, 30 May 2018 16:55:21 -0700 Subject: [PATCH 006/135] Added Profiler plugin/panel --- agent/Agent.js | 2 +- frontend/Panel.js | 3 + frontend/SettingsPane.js | 41 --------- frontend/Store.js | 5 -- frontend/TabbedPane.js | 15 ++-- packages/react-devtools-core/src/backend.js | 2 + plugins/Profiler/ProfilerPlugin.js | 68 +++++++++++++++ plugins/Profiler/ProfilerTab.js | 97 +++++++++++++++++++++ plugins/Profiler/Store.js | 42 +++++++++ plugins/Profiler/backend.js | 31 +++++++ shells/plain/backend.js | 2 + shells/webextension/src/backend.js | 2 + 12 files changed, 254 insertions(+), 56 deletions(-) create mode 100644 plugins/Profiler/ProfilerPlugin.js create mode 100644 plugins/Profiler/ProfilerTab.js create mode 100644 plugins/Profiler/Store.js create mode 100644 plugins/Profiler/backend.js diff --git a/agent/Agent.js b/agent/Agent.js index 6f6edf5c02..fcd9fee82a 100644 --- a/agent/Agent.js +++ b/agent/Agent.js @@ -219,7 +219,7 @@ class Agent extends EventEmitter { }); this.on('setSelection', data => bridge.send('select', data)); this.on('setInspectEnabled', data => bridge.send('setInspectEnabled', data)); - this.on('setIsRecording', data => bridge.send('setIsRecording', data)); + this.on('isRecording', isRecording => bridge.send('isRecording', isRecording)); } scrollToNode(id: ElementID): void { diff --git a/frontend/Panel.js b/frontend/Panel.js index 9c9050645b..5d7eb2b755 100644 --- a/frontend/Panel.js +++ b/frontend/Panel.js @@ -20,6 +20,7 @@ var assign = require('object-assign'); var Bridge = require('../agent/Bridge'); var {sansSerif} = require('./Themes/Fonts'); var NativeStyler = require('../plugins/ReactNativeStyle/ReactNativeStyle.js'); +var ProfilerPlugin = require('../plugins/Profiler/ProfilerPlugin'); var RelayPlugin = require('../plugins/Relay/RelayPlugin'); var Themes = require('./Themes/Themes'); var ThemeStore = require('./Themes/Store'); @@ -226,7 +227,9 @@ class Panel extends React.Component { var refresh = () => this.forceUpdate(); this.plugins = [ new RelayPlugin(this._store, this._bridge, refresh), + new ProfilerPlugin(this._store, this._bridge, refresh), ]; + this._keyListener = keyboardNav(this._store, window); window.addEventListener('keydown', this._keyListener); diff --git a/frontend/SettingsPane.js b/frontend/SettingsPane.js index 14be5b86f7..4f9a691f01 100644 --- a/frontend/SettingsPane.js +++ b/frontend/SettingsPane.js @@ -119,17 +119,6 @@ class SettingsPane extends React.Component { /> )} - {/* - TODO (bvaughn) - Only enable if ProfileMode exists and is supported. - Maybe we can determine this by looking for an "actualDuration" field on the root Fiber? - */} - - ( - - ) -); - function SearchIcon({ theme }) { return ( ({ fontSize: sansSerif.sizes.normal, }); -const recordMenuButtonStyle = (isActive: boolean, isHovered: boolean, theme: Theme) => ({ - display: 'flex', - background: 'none', - border: 'none', - outline: 'none', - color: isActive - ? theme.special03 - : isHovered ? theme.state06 : 'inherit', - filter: isActive - ? `drop-shadow( 0 0 2px ${theme.special03} )` - : 'none', -}); - const settingsMenuButtonStyle = (isHovered: boolean, theme: Theme) => ({ display: 'flex', background: 'none', diff --git a/frontend/Store.js b/frontend/Store.js index 87bed151b1..c2f9d7b8e5 100644 --- a/frontend/Store.js +++ b/frontend/Store.js @@ -93,7 +93,6 @@ class Store extends EventEmitter { // Public state isInspectEnabled: boolean; - isRecording: boolean; traceupdatesState: ?ControlState; colorizerState: ?ControlState; contextMenu: ?ContextMenu; @@ -126,7 +125,6 @@ class Store extends EventEmitter { // Public state this.isInspectEnabled = false; - this.isRecording = false; this.roots = new List(); this.contextMenu = null; this.searchRoots = null; @@ -165,7 +163,6 @@ class Store extends EventEmitter { this._bridge.on('update', (data) => this._updateComponent(data)); this._bridge.on('unmount', id => this._unmountComponent(id)); this._bridge.on('setInspectEnabled', (data) => this.setInspectEnabled(data)); - this._bridge.on('setIsRecording', (data) => this.setIsRecording(data)); this._bridge.on('select', ({id, quiet, offsetFromLeaf = 0}) => { // Backtrack if we want to skip leaf nodes while (offsetFromLeaf > 0) { @@ -576,8 +573,6 @@ class Store extends EventEmitter { } setIsRecording(isRecording: boolean) { - this.isRecording = isRecording; - this.emit('isRecording'); this._bridge.send('isRecording', isRecording); } diff --git a/frontend/TabbedPane.js b/frontend/TabbedPane.js index b8612ffa1e..1faf51c44e 100644 --- a/frontend/TabbedPane.js +++ b/frontend/TabbedPane.js @@ -63,24 +63,21 @@ const tabsStyle = (theme: Theme) => ({ flexShrink: 0, listStyle: 'none', margin: 0, - backgroundColor: theme.base00, + backgroundColor: theme.base01, borderBottom: `1px solid ${theme.base03}`, - padding: '0.25rem 0.25rem 0 0.25rem', + padding: '0 0.25rem', }); const tabStyle = (isSelected: boolean, theme: Theme) => { - const border = isSelected ? `1px solid ${theme.base03}` : 'none'; - return { - padding: '0.25rem 0.5rem', + padding: '0.25rem 0.75rem', lineHeight: '15px', fontSize: sansSerif.sizes.normal, fontFamily: sansSerif.family, cursor: 'pointer', - backgroundColor: isSelected ? theme.base01 : 'transparent', - borderLeft: border, - borderRight: border, - borderTop: border, + borderTop: '1px solid transparent', + borderBottom: isSelected ? `2px solid ${theme.state00}` : 'none', + marginBottom: isSelected ? '-1px' : '1px', }; }; diff --git a/packages/react-devtools-core/src/backend.js b/packages/react-devtools-core/src/backend.js index c4c9907e47..b43c59973e 100644 --- a/packages/react-devtools-core/src/backend.js +++ b/packages/react-devtools-core/src/backend.js @@ -25,6 +25,7 @@ var installRelayHook = require('../../../plugins/Relay/installRelayHook'); var inject = require('../../../agent/inject'); var invariant = require('assert'); var setupRNStyle = require('../../../plugins/ReactNativeStyle/setupBackend'); +var setupProfiler = require('../../../plugins/Profiler/backend'); var setupRelay = require('../../../plugins/Relay/backend'); installGlobalHook(window); @@ -139,6 +140,7 @@ function setupBackend(wall, resolveRNStyle) { setupRNStyle(bridge, agent, resolveRNStyle); } + setupProfiler(bridge, agent, window.__REACT_DEVTOOLS_GLOBAL_HOOK__); setupRelay(bridge, agent, window.__REACT_DEVTOOLS_GLOBAL_HOOK__); var _connectTimeout = setTimeout(() => { diff --git a/plugins/Profiler/ProfilerPlugin.js b/plugins/Profiler/ProfilerPlugin.js new file mode 100644 index 0000000000..cb4d8e17e9 --- /dev/null +++ b/plugins/Profiler/ProfilerPlugin.js @@ -0,0 +1,68 @@ +/** + * 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. + * + * @flow + */ +'use strict'; + +import type Bridge from '../../agent/Bridge'; +import type Store from '../../frontend/Store'; + +var React = require('react'); +var provideStore = require('../../frontend/provideStore'); + +var ProfilerStore = require('./Store'); +var ProfilerTab = require('./ProfilerTab'); +var StoreWrapper = provideStore('profilerStore'); + +class ProfilerPlugin { + hasProfiler: bool; + bridge: Bridge; + store: Store; + profilerStore: ProfilerStore; + + constructor(store: Store, bridge: Bridge, refresh: () => void) { + this.bridge = bridge; + this.store = store; + this.hasProfiler = false; + this.profilerStore = new ProfilerStore(bridge, store); + + // Wait until roots have been initialized... + setTimeout(() => { + bridge.call('profiler:check', [], hasProfiler => { + this.hasProfiler = hasProfiler; + if (hasProfiler) { + refresh(); + } + }); + }, 1000); + } + + panes(): Array<(node: Object, id: string) => React$Element> { + return []; + } + + teardown() { + } + + tabs(): ?{[key: string]: () => React$Element} { + if (!this.hasProfiler) { + return null; + } + + return { + Profiler: () => ( + + {() => } + + ), + }; + } +} + +module.exports = ProfilerPlugin; diff --git a/plugins/Profiler/ProfilerTab.js b/plugins/Profiler/ProfilerTab.js new file mode 100644 index 0000000000..ccfdf8168e --- /dev/null +++ b/plugins/Profiler/ProfilerTab.js @@ -0,0 +1,97 @@ +/** + * 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. + * + * @flow + */ +'use strict'; + +import type {Theme} from '../../frontend/types'; + +var React = require('react'); +var decorate = require('../../frontend/decorate'); +var {sansSerif} = require('../../frontend/Themes/Fonts'); +const SvgIcon = require('../../frontend/SvgIcon'); +const Icons = require('../../frontend/Icons'); +const Hoverable = require('../../frontend/Hoverable'); + +type Props = {| + isRecording: boolean, + toggleIsRecording: Function, +|}; + +class ProfilerTab extends React.Component { + static contextTypes = { + theme: React.PropTypes.object.isRequired, + }; + + render() { + const { theme } = this.context; + const { isRecording, toggleIsRecording } = this.props; + return ( +
+ Click the record button to start a new recording. +
+ ); + } +} + +const RecordButton = Hoverable( + ({ isActive, isHovered, onClick, onMouseEnter, onMouseLeave, theme }) => ( + + ) +); + +var styles = { + container: { + flex: 1, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + fontFamily: sansSerif.family, + fontSize: sansSerif.sizes.normal, + }, +}; + +const recordButtonStyle = (isActive: boolean, isHovered: boolean, theme: Theme) => ({ + display: 'flex', + background: 'none', + border: 'none', + outline: 'none', + color: isActive + ? theme.special03 + : isHovered ? theme.state06 : 'inherit', + filter: isActive + ? `drop-shadow( 0 0 2px ${theme.special03} )` + : 'none', +}); + +module.exports = decorate({ + store: 'profilerStore', + listeners: () => ['isRecording'], + props(store) { + return { + isRecording: !!store.isRecording, + toggleIsRecording: () => store.setIsRecording(!store.isRecording), + }; + }, +}, ProfilerTab); diff --git a/plugins/Profiler/Store.js b/plugins/Profiler/Store.js new file mode 100644 index 0000000000..dfc4267bd7 --- /dev/null +++ b/plugins/Profiler/Store.js @@ -0,0 +1,42 @@ +/** + * 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. + * + * @flow + */ +'use strict'; + +import type Bridge from '../../agent/Bridge'; + +var {EventEmitter} = require('events'); + +class Store extends EventEmitter { + _bridge: Bridge; + _mainStore: Object; + + isRecording: boolean = false; + + constructor(bridge: Bridge, mainStore: Object) { + super(); + + this._bridge = bridge; + this._mainStore = mainStore; + } + + off() { + // Noop + } + + setIsRecording(isRecording: boolean) { + console.log('[Profiler/Store] setIsRecording()', isRecording); + this.isRecording = isRecording; + this.emit('isRecording', isRecording); + this._mainStore.setIsRecording(isRecording); + } +} + +module.exports = Store; diff --git a/plugins/Profiler/backend.js b/plugins/Profiler/backend.js new file mode 100644 index 0000000000..52347aa3db --- /dev/null +++ b/plugins/Profiler/backend.js @@ -0,0 +1,31 @@ +/** + * 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. + * + * @flow + */ +'use strict'; + +import type Bridge from '../../agent/Bridge'; +import type Agent from '../../agent/Agent'; + +module.exports = (bridge: Bridge, agent: Agent, hook: Object) => { + bridge.onCall('profiler:check', () => { + let shouldEnable = false; + + agent.roots.forEach(id => { + const root = agent.internalInstancesById.get(id); + if ((root: any).hasOwnProperty('actualDuration')) { + shouldEnable = true; + } + }); + + shouldEnable = true; // TODO (bvaughn) Remove this + + return shouldEnable; + }); +}; diff --git a/shells/plain/backend.js b/shells/plain/backend.js index cfbd32756b..dfe5f157f4 100644 --- a/shells/plain/backend.js +++ b/shells/plain/backend.js @@ -15,6 +15,7 @@ var ProfilerManager = require('../../plugins/Profiler/ProfilerManager'); var TraceUpdatesBackendManager = require('../../plugins/TraceUpdates/TraceUpdatesBackendManager'); var Bridge = require('../../agent/Bridge'); var setupHighlighter = require('../../frontend/Highlighter/setup'); +var setupProfiler = require('../../plugins/Profiler/backend'); var setupRelay = require('../../plugins/Relay/backend'); var inject = require('../../agent/inject'); @@ -38,6 +39,7 @@ agent.addBridge(bridge); inject(window.__REACT_DEVTOOLS_GLOBAL_HOOK__, agent); setupHighlighter(agent); +setupProfiler(bridge, agent, window.__REACT_DEVTOOLS_GLOBAL_HOOK__); setupRelay(bridge, agent, window.__REACT_DEVTOOLS_GLOBAL_HOOK__); ProfilerManager.init(agent); diff --git a/shells/webextension/src/backend.js b/shells/webextension/src/backend.js index a85b2dfc62..c7968d720e 100644 --- a/shells/webextension/src/backend.js +++ b/shells/webextension/src/backend.js @@ -17,6 +17,7 @@ var Bridge = require('../../../agent/Bridge'); var inject = require('../../../agent/inject'); var setupRNStyle = require('../../../plugins/ReactNativeStyle/setupBackend'); var setupHighlighter = require('../../../frontend/Highlighter/setup'); +var setupProfiler = require('../../../plugins/Profiler/backend'); var setupRelay = require('../../../plugins/Relay/backend'); window.addEventListener('message', welcome); @@ -69,6 +70,7 @@ function setup(hook) { setupRNStyle(bridge, agent, hook.resolveRNStyle); } + setupProfiler(bridge, agent, hook); setupRelay(bridge, agent, hook); agent.on('shutdown', () => { From f1a8a5142611cf21a197086d88120bc90dd2af09 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Thu, 31 May 2018 08:14:20 -0700 Subject: [PATCH 007/135] Added some debug logging of commit snapshot --- plugins/Profiler/ProfilerManager.js | 70 ++++++++++++++++++++++++----- 1 file changed, 58 insertions(+), 12 deletions(-) diff --git a/plugins/Profiler/ProfilerManager.js b/plugins/Profiler/ProfilerManager.js index c0f57eea9a..0d575acda2 100644 --- a/plugins/Profiler/ProfilerManager.js +++ b/plugins/Profiler/ProfilerManager.js @@ -11,25 +11,37 @@ 'use strict'; +const getDisplayName = require('../../backend/getDisplayName'); + type Agent = any; type Snapshot = { actualDuration: number, actualStartTime: number, baseTime: number, - commitTime: number, + fiber: any, name: string, }; +type FiberToSnapshotMap = Map; + +type Commit = { + commitTime: number, + fiberToSnapshotMap: FiberToSnapshotMap, + root: any, +}; + // TODO (bvaughn) Should this live in a shared constants file like ReactSymbols? // Or should it be in a Fiber-specific file somewhere (like getData)? const ProfileMode = 0b100; +// TODO (bvaughn) This entire implementation is coupled to Fiber. +// This will likely cause pain in a future version of React. class ProfilerManager { _agent: Agent; - _commitTime: number = 0; + _commits: Array = []; + _fiberToSnapshotMap: FiberToSnapshotMap | null = null; _isRecording: boolean = false; - _snapshots: {[commitTime: number]: Array} = {}; constructor(agent: Agent) { this._agent = agent; @@ -40,16 +52,23 @@ class ProfilerManager { agent.on('update', this._onMountOrUpdate); } - _onCommitRoot = id => { + _onCommitRoot = rootID => { + if (!this._isRecording) { + return; + } + // This will not match the commit time logged to Profilers in this commit, // But that's probably okay. // DevTools only needs it to group all of the profile timings, // And to place them at a certain point in time in the replay view. - this._commitTime = performance.now(); + const commitTime = performance.now(); - if (this._isRecording) { - this._snapshots[this._commitTime] = []; - } + this._fiberToSnapshotMap = new Map(); + this._commits.push({ + commitTime, + fiberToSnapshotMap: this._fiberToSnapshotMap, + root: this._agent.internalInstancesById.get(rootID), + }); }; _onMountOrUpdate = (data: any) => { @@ -57,14 +76,16 @@ class ProfilerManager { return; } + const fiber = this._agent.internalInstancesById.get(data.id); + // TODO (bvaughn) Do I need to capture hierarchical information as well? // So the resulting flame graph can mirror the tree structure somehow? // Or do we want to always sort by most expensive to least expensive? - this._snapshots[this._commitTime].push({ + ((this._fiberToSnapshotMap: any): FiberToSnapshotMap).set(fiber, { actualDuration: data.profilerData.actualDuration, actualStartTime: data.profilerData.actualStartTime, baseTime: data.profilerData.baseTime, - commitTime: this._commitTime, // TODO (bvaughn) This is redundant. Maybe ditch it? + fiber, name: data.name, }); }; @@ -107,8 +128,13 @@ class ProfilerManager { // Dump snapshot data if we are done profiling. if (!isRecording) { - console.log(this._snapshots); // TODO (bvaughn) Debugging only; remove this. - this._snapshots = {}; + // TODO (bvaughn) Debugging only; remove this... + const commit = this._commits[this._commits.length - 1]; + printSnapshotTree(commit.root.current.child, commit); + // TODO (bvaughn) Debugging only; remove this^^^ + + this._commits = []; + this._fiberToSnapshotMap = null; } }; } @@ -120,3 +146,23 @@ function init(agent: Agent): ProfilerManager { module.exports = { init, }; + +// TODO (bvaughn) Debugging only; remove this... +const printSnapshotTree = (fiber, commit, depth = 0) => { + // TODO (bvaughn) This is hacky; it's because I'm doing root.current. + const snapshot = commit.fiberToSnapshotMap.get(fiber) || commit.fiberToSnapshotMap.get(fiber.alternate); + + if (snapshot) { + console.log('••'.repeat(depth), snapshot.name, (commit.fiberToSnapshotMap.has(fiber) ? '(fiber)' : '(alternate)'), 'duration:', snapshot.actualDuration); + } else { + console.log('••'.repeat(depth), getDisplayName(fiber)); + } + + if (fiber.sibling) { + printSnapshotTree(fiber.sibling, commit, depth); + } + if (fiber.child) { + printSnapshotTree(fiber.child, commit, depth + 1); + } +}; +// TODO (bvaughn) Debugging only; remove this^^^ From 5330628a17cb0e557ab7cc19e9ea60ada333a43d Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Thu, 31 May 2018 08:32:17 -0700 Subject: [PATCH 008/135] Removed react-addons-create-fragment dependency and usage --- frontend/PropVal.js | 39 ++++++++++++++++++--------------------- package.json | 12 +++++++----- yarn.lock | 7 ------- 3 files changed, 25 insertions(+), 33 deletions(-) diff --git a/frontend/PropVal.js b/frontend/PropVal.js index ee3a35cacb..7a92a413d5 100644 --- a/frontend/PropVal.js +++ b/frontend/PropVal.js @@ -14,7 +14,6 @@ var React = require('react'); var ReactDOM = require('react-dom'); var consts = require('../agent/consts'); -var createFragment = require('react-addons-create-fragment'); var {getInvertedWeak} = require('./Themes/utils'); var flash = require('./flash'); @@ -149,49 +148,47 @@ function previewProp(val: any, nested: boolean, inverted: boolean, theme: Theme) } function previewArray(val, inverted, theme) { - var items = {}; + var items = []; val.slice(0, 3).forEach((item, i) => { - items['n' + i] = ; - items['c' + i] = ', '; + if (i > 0) { + items.push({', '}); + } + items.push( + + ); }); - if (val.length > 3) { - items.last = '…'; - } else { - delete items['c' + (val.length - 1)]; - } var style = { color: inverted ? theme.base03 : theme.special01, }; return ( - [{createFragment(items)}] + {'['}{items}{val.length > 3 ? ', …' : ''}{']'} ); } function previewObject(val, inverted, theme) { var names = Object.keys(val); - var items = {}; + var items = []; var attrStyle = { color: inverted ? getInvertedWeak(theme.state02) : theme.special06, }; names.slice(0, 3).forEach((name, i) => { - items['k' + i] = {name}; - items['c' + i] = ': '; - items['v' + i] = ; - items['m' + i] = ', '; + items.push( // Fragment + + {i > 0 ? ', ' : ''} + {name} + {': '} + + + ); }); - if (names.length > 3) { - items.rest = '…'; - } else { - delete items['m' + (names.length - 1)]; - } var style = { color: inverted ? getInvertedWeak(theme.state02) : theme.special01, }; return ( - {'{'}{createFragment(items)}{'}'} + {'{'}{items}{names.length > 3 ? ', …' : ''}{'}'} ); } diff --git a/package.json b/package.json index f48be320d0..3116f551a5 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,6 @@ "object-assign": "4.0.1", "raw-loader": "^0.5.1", "react": "15.4.2", - "react-addons-create-fragment": "15.4.2", "react-color": "^2.11.7", "react-dom": "15.4.2", "react-portal": "^3.1.0", @@ -38,10 +37,11 @@ "license": "BSD-3-Clause", "repository": "facebook/react-devtools", "private": true, - "workspaces": ["packages/*"], + "workspaces": [ + "packages/*" + ], "scripts": { - "build:extension": - "yarn run build:extension:chrome && yarn run build:extension:firefox", + "build:extension": "yarn run build:extension:chrome && yarn run build:extension:firefox", "build:extension:chrome": "node ./shells/chrome/build", "build:extension:firefox": "node ./shells/firefox/build", "build:standalone": "cd packages/react-devtools-core && yarn run build", @@ -55,7 +55,9 @@ "typecheck": "flow check" }, "jest": { - "modulePathIgnorePatterns": ["/shells"] + "modulePathIgnorePatterns": [ + "/shells" + ] }, "devDependencies": { "chrome-launch": "^1.1.4", diff --git a/yarn.lock b/yarn.lock index 2526dc8383..d4929667ae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5666,13 +5666,6 @@ rc@~1.1.6: minimist "^1.2.0" strip-json-comments "~1.0.4" -react-addons-create-fragment@15.4.2: - version "15.4.2" - resolved "https://registry.yarnpkg.com/react-addons-create-fragment/-/react-addons-create-fragment-15.4.2.tgz#11372924f730a97dff4c690535211bb73d8f8815" - dependencies: - fbjs "^0.8.4" - object-assign "^4.1.0" - react-color@^2.11.7: version "2.11.7" resolved "https://registry.yarnpkg.com/react-color/-/react-color-2.11.7.tgz#746465b75feda63c2567607dfbcb276fc954a5b7" From 28ccef7de6ffde413b016bb8acc2a4a4024ad745 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Thu, 31 May 2018 08:41:07 -0700 Subject: [PATCH 009/135] Added prop-types package and ran codemod --- frontend/Breadcrumb.js | 3 ++- frontend/ContextMenu.js | 3 ++- frontend/DataView/DataView.js | 7 ++++--- frontend/DataView/Simple.js | 11 ++++++----- frontend/HighlightHover.js | 3 ++- frontend/Input.js | 3 ++- frontend/LeftPane.js | 2 +- frontend/Node.js | 5 +++-- frontend/Panel.js | 17 +++++++++-------- frontend/PreferencesPanel.js | 17 +++++++++-------- frontend/PropState.js | 5 +++-- frontend/PropVal.js | 3 ++- frontend/Props.js | 3 ++- frontend/SettingsPane.js | 6 +++--- frontend/SplitPane.js | 3 ++- frontend/TabbedPane.js | 3 ++- frontend/Themes/Editor/Editor.js | 3 ++- frontend/Themes/Preview.js | 5 +++-- frontend/TreeView.js | 5 +++-- frontend/decorate.js | 3 ++- frontend/detail_pane/DetailPaneSection.js | 3 ++- frontend/provideStore.js | 3 ++- package.json | 1 + plugins/Profiler/ProfilerTab.js | 4 +++- plugins/ReactNativeStyle/AutoSizeInput.js | 4 +++- plugins/ReactNativeStyle/BoxInspector.js | 4 +++- plugins/ReactNativeStyle/StyleEdit.js | 4 +++- plugins/Relay/ElementPanel.js | 4 +++- plugins/Relay/Query.js | 3 ++- plugins/Relay/StoreTab.js | 4 +++- shells/theme-preview/source/Application.js | 10 ++++++---- shells/theme-preview/source/LeftPane.js | 4 +++- shells/theme-preview/source/RightPane.js | 3 ++- yarn.lock | 22 +++++++++++++++++++++- 34 files changed, 121 insertions(+), 62 deletions(-) diff --git a/frontend/Breadcrumb.js b/frontend/Breadcrumb.js index 8c9fc3c729..d78169d367 100644 --- a/frontend/Breadcrumb.js +++ b/frontend/Breadcrumb.js @@ -16,6 +16,7 @@ import type {Theme} from './types'; var {sansSerif} = require('./Themes/Fonts'); var React = require('react'); +var PropTypes = require('prop-types'); var decorate = require('./decorate'); type BreadcrumbPath = Array<{id: ElementID, node: Object}>; @@ -79,7 +80,7 @@ class Breadcrumb extends React.Component { } Breadcrumb.contextTypes = { - theme: React.PropTypes.object.isRequired, + theme: PropTypes.object.isRequired, }; const containerStyle = (theme: Theme) => ({ diff --git a/frontend/ContextMenu.js b/frontend/ContextMenu.js index 67a55385ec..8d98e93faa 100644 --- a/frontend/ContextMenu.js +++ b/frontend/ContextMenu.js @@ -11,6 +11,7 @@ 'use strict'; var React = require('react'); +var PropTypes = require('prop-types'); var nullthrows = require('nullthrows').default; var {sansSerif} = require('./Themes/Fonts'); var HighlightHover = require('./HighlightHover'); @@ -120,7 +121,7 @@ class ContextMenu extends React.Component { } ContextMenu.contextTypes = { - theme: React.PropTypes.object.isRequired, + theme: PropTypes.object.isRequired, }; var Wrapped = decorate({ diff --git a/frontend/DataView/DataView.js b/frontend/DataView/DataView.js index 13ea62fdf7..9a5ca9995c 100644 --- a/frontend/DataView/DataView.js +++ b/frontend/DataView/DataView.js @@ -14,6 +14,7 @@ import type {Theme, DOMEvent} from '../types'; var {sansSerif} = require('../Themes/Fonts'); var React = require('react'); +var PropTypes = require('prop-types'); var Simple = require('./Simple'); var nullthrows = require('nullthrows').default; @@ -137,7 +138,7 @@ class DataView extends React.Component { } DataView.contextTypes = { - theme: React.PropTypes.object.isRequired, + theme: PropTypes.object.isRequired, }; type Props = { @@ -299,8 +300,8 @@ class DataItem extends React.Component { } DataItem.contextTypes = { - onChange: React.PropTypes.func, - theme: React.PropTypes.object.isRequired, + onChange: PropTypes.func, + theme: PropTypes.object.isRequired, }; function alphanumericSort(a: string, b: string): number { diff --git a/frontend/DataView/Simple.js b/frontend/DataView/Simple.js index 9d2be919d3..a5bb7725c8 100644 --- a/frontend/DataView/Simple.js +++ b/frontend/DataView/Simple.js @@ -12,6 +12,7 @@ var React = require('react'); var ReactDOM = require('react-dom'); +var PropTypes = require('prop-types'); var Input = require('../Input'); var flash = require('../flash'); @@ -148,14 +149,14 @@ class Simple extends React.Component { } Simple.propTypes = { - data: React.PropTypes.any, - path: React.PropTypes.array, - readOnly: React.PropTypes.bool, + data: PropTypes.any, + path: PropTypes.array, + readOnly: PropTypes.bool, }; Simple.contextTypes = { - onChange: React.PropTypes.func, - theme: React.PropTypes.object.isRequired, + onChange: PropTypes.func, + theme: PropTypes.object.isRequired, }; const inputStyle = (theme: Theme) => ({ diff --git a/frontend/HighlightHover.js b/frontend/HighlightHover.js index d58421cebd..18b4566c20 100644 --- a/frontend/HighlightHover.js +++ b/frontend/HighlightHover.js @@ -11,6 +11,7 @@ 'use strict'; var React = require('react'); +var PropTypes = require('prop-types'); var assign = require('object-assign'); import type {Theme} from './types'; @@ -56,7 +57,7 @@ class HighlightHover extends React.Component { } HighlightHover.contextTypes = { - theme: React.PropTypes.object.isRequired, + theme: PropTypes.object.isRequired, }; module.exports = HighlightHover; diff --git a/frontend/Input.js b/frontend/Input.js index ba6a710f0c..ac8770f8dd 100644 --- a/frontend/Input.js +++ b/frontend/Input.js @@ -11,6 +11,7 @@ 'use strict'; const React = require('react'); +const PropTypes = require('prop-types'); import type {Theme} from './types'; @@ -53,7 +54,7 @@ const Input = (props: Props, context: Context) => { }; Input.contextTypes = { - theme: React.PropTypes.object.isRequired, + theme: PropTypes.object.isRequired, }; const inputStyle = (theme: Theme) => ({ diff --git a/frontend/LeftPane.js b/frontend/LeftPane.js index 26b79bde2a..ebd9b1ce25 100644 --- a/frontend/LeftPane.js +++ b/frontend/LeftPane.js @@ -13,9 +13,9 @@ 'use strict'; var React = require('react'); +var PropTypes = require('prop-types'); var SettingsPane = require('./SettingsPane'); var TreeView = require('./TreeView'); -var {PropTypes} = React; type Props = { reload?: () => void, diff --git a/frontend/Node.js b/frontend/Node.js index 94894d4c12..97bbd618e7 100644 --- a/frontend/Node.js +++ b/frontend/Node.js @@ -11,6 +11,7 @@ 'use strict'; var React = require('react'); +var PropTypes = require('prop-types'); var nullthrows = require('nullthrows').default; var decorate = require('./decorate'); @@ -405,8 +406,8 @@ class Node extends React.Component { } Node.contextTypes = { - scrollTo: React.PropTypes.func, - theme: React.PropTypes.object.isRequired, + scrollTo: PropTypes.func, + theme: PropTypes.object.isRequired, }; var WrappedNode = decorate({ diff --git a/frontend/Panel.js b/frontend/Panel.js index 5d7eb2b755..eebaefc6b3 100644 --- a/frontend/Panel.js +++ b/frontend/Panel.js @@ -11,6 +11,7 @@ 'use strict'; var React = require('react'); +var PropTypes = require('prop-types'); var Container = require('./Container'); var Store = require('./Store'); var keyboardNav = require('./keyboardNav'); @@ -358,14 +359,14 @@ class Panel extends React.Component { } Panel.childContextTypes = { - browserName: React.PropTypes.string.isRequired, - defaultThemeName: React.PropTypes.string.isRequired, - showHiddenThemes: React.PropTypes.bool.isRequired, - showInspectButton: React.PropTypes.bool.isRequired, - store: React.PropTypes.object, - theme: React.PropTypes.object.isRequired, - themeName: React.PropTypes.string.isRequired, - themes: React.PropTypes.object.isRequired, + browserName: PropTypes.string.isRequired, + defaultThemeName: PropTypes.string.isRequired, + showHiddenThemes: PropTypes.bool.isRequired, + showInspectButton: PropTypes.bool.isRequired, + store: PropTypes.object, + theme: PropTypes.object.isRequired, + themeName: PropTypes.string.isRequired, + themes: PropTypes.object.isRequired, }; var panelRNStyle = (bridge, supportsMeasure, theme) => (node, id) => { diff --git a/frontend/PreferencesPanel.js b/frontend/PreferencesPanel.js index 02d96ffe39..354be8499a 100644 --- a/frontend/PreferencesPanel.js +++ b/frontend/PreferencesPanel.js @@ -11,6 +11,7 @@ 'use strict'; const React = require('react'); +const PropTypes = require('prop-types'); const decorate = require('./decorate'); const {sansSerif} = require('./Themes/Fonts'); @@ -164,16 +165,16 @@ class PreferencesPanel extends React.Component { } PreferencesPanel.contextTypes = { - browserName: React.PropTypes.string.isRequired, - showHiddenThemes: React.PropTypes.bool.isRequired, - theme: React.PropTypes.object.isRequired, - themeName: React.PropTypes.string.isRequired, - themes: React.PropTypes.object.isRequired, + browserName: PropTypes.string.isRequired, + showHiddenThemes: PropTypes.bool.isRequired, + theme: PropTypes.object.isRequired, + themeName: PropTypes.string.isRequired, + themes: PropTypes.object.isRequired, }; PreferencesPanel.propTypes = { - changeTheme: React.PropTypes.func, - hide: React.PropTypes.func, - open: React.PropTypes.bool, + changeTheme: PropTypes.func, + hide: PropTypes.func, + open: PropTypes.bool, }; diff --git a/frontend/PropState.js b/frontend/PropState.js index 3120e93c07..ea957edf97 100644 --- a/frontend/PropState.js +++ b/frontend/PropState.js @@ -17,6 +17,7 @@ var DetailPaneSection = require('./detail_pane/DetailPaneSection'); var {sansSerif} = require('./Themes/Fonts'); var PropVal = require('./PropVal'); var React = require('react'); +var PropTypes = require('prop-types'); var decorate = require('./decorate'); var invariant = require('./invariant'); @@ -182,11 +183,11 @@ class PropState extends React.Component { } PropState.contextTypes = { - theme: React.PropTypes.object.isRequired, + theme: PropTypes.object.isRequired, }; PropState.childContextTypes = { - onChange: React.PropTypes.func, + onChange: PropTypes.func, }; var WrappedPropState = decorate({ diff --git a/frontend/PropVal.js b/frontend/PropVal.js index 7a92a413d5..91f742968d 100644 --- a/frontend/PropVal.js +++ b/frontend/PropVal.js @@ -12,6 +12,7 @@ var React = require('react'); var ReactDOM = require('react-dom'); +var PropTypes = require('prop-types'); var consts = require('../agent/consts'); var {getInvertedWeak} = require('./Themes/utils'); @@ -47,7 +48,7 @@ class PropVal extends React.Component { } PropVal.contextTypes = { - theme: React.PropTypes.object.isRequired, + theme: PropTypes.object.isRequired, }; function previewProp(val: any, nested: boolean, inverted: boolean, theme: Theme) { diff --git a/frontend/Props.js b/frontend/Props.js index cced4d6246..f7cc5e7a9e 100644 --- a/frontend/Props.js +++ b/frontend/Props.js @@ -11,6 +11,7 @@ 'use strict'; var React = require('react'); +var PropTypes = require('prop-types'); var PropVal = require('./PropVal'); var {getInvertedMid} = require('./Themes/utils'); @@ -61,7 +62,7 @@ class Props extends React.Component { } Props.contextTypes = { - theme: React.PropTypes.object.isRequired, + theme: PropTypes.object.isRequired, }; const attributeNameStyle = (isInverted: boolean, theme: Theme) => ({ diff --git a/frontend/SettingsPane.js b/frontend/SettingsPane.js index 4f9a691f01..d7da75ebf3 100644 --- a/frontend/SettingsPane.js +++ b/frontend/SettingsPane.js @@ -13,10 +13,10 @@ const TraceUpdatesFrontendControl = require('../plugins/TraceUpdates/TraceUpdate const ColorizerFrontendControl = require('../plugins/Colorizer/ColorizerFrontendControl'); const React = require('react'); const ReactDOM = require('react-dom'); +const PropTypes = require('prop-types'); const {sansSerif} = require('./Themes/Fonts'); const SearchUtils = require('./SearchUtils'); const SvgIcon = require('./SvgIcon'); -const {PropTypes} = React; const Icons = require('./Icons'); const Input = require('./Input'); const Hoverable = require('./Hoverable'); @@ -156,8 +156,8 @@ class SettingsPane extends React.Component { } SettingsPane.contextTypes = { - showInspectButton: React.PropTypes.bool.isRequired, - theme: React.PropTypes.object.isRequired, + showInspectButton: PropTypes.bool.isRequired, + theme: PropTypes.object.isRequired, }; SettingsPane.propTypes = { isInspectEnabled: PropTypes.bool, diff --git a/frontend/SplitPane.js b/frontend/SplitPane.js index 97558a1458..c13dfeca7d 100644 --- a/frontend/SplitPane.js +++ b/frontend/SplitPane.js @@ -12,6 +12,7 @@ var React = require('react'); var ReactDOM = require('react-dom'); +var PropTypes = require('prop-types'); var Draggable = require('./Draggable'); var nullthrows = require('nullthrows').default; @@ -102,7 +103,7 @@ class SplitPane extends React.Component { } SplitPane.contextTypes = { - theme: React.PropTypes.object.isRequired, + theme: PropTypes.object.isRequired, }; const containerStyle = (isVertical: boolean) => ({ diff --git a/frontend/TabbedPane.js b/frontend/TabbedPane.js index 1faf51c44e..64bdfa2858 100644 --- a/frontend/TabbedPane.js +++ b/frontend/TabbedPane.js @@ -11,6 +11,7 @@ 'use strict'; var React = require('react'); +var PropTypes = require('prop-types'); var decorate = require('./decorate'); var {sansSerif} = require('./Themes/Fonts'); @@ -55,7 +56,7 @@ class TabbedPane extends React.Component { } TabbedPane.contextTypes = { - theme: React.PropTypes.object.isRequired, + theme: PropTypes.object.isRequired, }; const tabsStyle = (theme: Theme) => ({ diff --git a/frontend/Themes/Editor/Editor.js b/frontend/Themes/Editor/Editor.js index 7ea3c0c75d..ac1b4cce7c 100644 --- a/frontend/Themes/Editor/Editor.js +++ b/frontend/Themes/Editor/Editor.js @@ -13,6 +13,7 @@ const {copy} = require('clipboard-js'); const decorate = require('../../decorate'); const React = require('react'); +const PropTypes = require('prop-types'); const ColorInput = require('./ColorInput'); const ColorGroups = require('./ColorGroups'); const Hoverable = require('../../Hoverable'); @@ -199,7 +200,7 @@ class Editor extends React.Component { } Editor.childContextTypes = { - theme: React.PropTypes.object, + theme: PropTypes.object, }; const WrappedEditor = decorate({ diff --git a/frontend/Themes/Preview.js b/frontend/Themes/Preview.js index 5f30aa3dad..9e6875779c 100644 --- a/frontend/Themes/Preview.js +++ b/frontend/Themes/Preview.js @@ -11,6 +11,7 @@ 'use strict'; const React = require('react'); +const PropTypes = require('prop-types'); const {Map} = require('immutable'); const consts = require('../../agent/consts'); @@ -52,8 +53,8 @@ class Preview extends React.Component { } Preview.childContextTypes = { - scrollTo: React.PropTypes.func, - store: React.PropTypes.object, + scrollTo: PropTypes.func, + store: PropTypes.object, }; const fauxRef = { diff --git a/frontend/TreeView.js b/frontend/TreeView.js index 7827f70c9b..84bdf80942 100644 --- a/frontend/TreeView.js +++ b/frontend/TreeView.js @@ -13,6 +13,7 @@ var Breadcrumb = require('./Breadcrumb'); var Node = require('./Node'); var React = require('react'); +var PropTypes = require('prop-types'); var SearchUtils = require('./SearchUtils'); var decorate = require('./decorate'); @@ -133,11 +134,11 @@ class TreeView extends React.Component { } TreeView.childContextTypes = { - scrollTo: React.PropTypes.func, + scrollTo: PropTypes.func, }; TreeView.contextTypes = { - theme: React.PropTypes.object.isRequired, + theme: PropTypes.object.isRequired, }; const noSearchResultsStyle = (theme: Theme) => ({ diff --git a/frontend/decorate.js b/frontend/decorate.js index 22b17c5877..74bfaa17d5 100644 --- a/frontend/decorate.js +++ b/frontend/decorate.js @@ -11,6 +11,7 @@ 'use strict'; var React = require('react'); +var PropTypes = require('prop-types'); type Options = { /** A function determining whether the component should rerender when the @@ -127,7 +128,7 @@ module.exports = function(options: Options, Component: any): any { Wrapper.contextTypes = { // $FlowFixMe - [storeKey]: React.PropTypes.object, + [storeKey]: PropTypes.object, }; Wrapper.displayName = 'Wrapper(' + Component.name + ')'; diff --git a/frontend/detail_pane/DetailPaneSection.js b/frontend/detail_pane/DetailPaneSection.js index 3f2929109b..005475772e 100644 --- a/frontend/detail_pane/DetailPaneSection.js +++ b/frontend/detail_pane/DetailPaneSection.js @@ -11,6 +11,7 @@ 'use strict'; var React = require('react'); +var PropTypes = require('prop-types'); import type {Theme} from '../types'; @@ -39,7 +40,7 @@ class DetailPaneSection extends React.Component { } DetailPaneSection.contextTypes = { - theme: React.PropTypes.object.isRequired, + theme: PropTypes.object.isRequired, }; const sectionStyle = (theme: Theme) => ({ diff --git a/frontend/provideStore.js b/frontend/provideStore.js index 26c507d190..df9fd4f10a 100644 --- a/frontend/provideStore.js +++ b/frontend/provideStore.js @@ -11,6 +11,7 @@ 'use strict'; var React = require('react'); +var PropTypes = require('prop-types'); type Props = { children: () => React.Node, @@ -26,7 +27,7 @@ module.exports = function(name: string): Object { } } Wrapper.childContextTypes = { - [name]: React.PropTypes.object, + [name]: PropTypes.object, }; Wrapper.displayName = 'StoreProvider(' + name + ')'; return Wrapper; diff --git a/package.json b/package.json index 3116f551a5..0302d958c6 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "node-libs-browser": "0.5.3", "nullthrows": "^1.0.0", "object-assign": "4.0.1", + "prop-types": "^15.6.1", "raw-loader": "^0.5.1", "react": "15.4.2", "react-color": "^2.11.7", diff --git a/plugins/Profiler/ProfilerTab.js b/plugins/Profiler/ProfilerTab.js index ccfdf8168e..1f3340c157 100644 --- a/plugins/Profiler/ProfilerTab.js +++ b/plugins/Profiler/ProfilerTab.js @@ -12,6 +12,8 @@ import type {Theme} from '../../frontend/types'; +const PropTypes = require('prop-types'); + var React = require('react'); var decorate = require('../../frontend/decorate'); var {sansSerif} = require('../../frontend/Themes/Fonts'); @@ -26,7 +28,7 @@ type Props = {| class ProfilerTab extends React.Component { static contextTypes = { - theme: React.PropTypes.object.isRequired, + theme: PropTypes.object.isRequired, }; render() { diff --git a/plugins/ReactNativeStyle/AutoSizeInput.js b/plugins/ReactNativeStyle/AutoSizeInput.js index 0e42a31bee..191c24e19c 100644 --- a/plugins/ReactNativeStyle/AutoSizeInput.js +++ b/plugins/ReactNativeStyle/AutoSizeInput.js @@ -10,6 +10,8 @@ */ 'use strict'; +const PropTypes = require('prop-types'); + var React = require('react'); var nullthrows = require('nullthrows').default; var {monospace} = require('../../frontend/Themes/Fonts'); @@ -156,7 +158,7 @@ class AutoSizeInput extends React.Component { } AutoSizeInput.contextTypes = { - theme: React.PropTypes.object.isRequired, + theme: PropTypes.object.isRequired, }; const inputStyle = (text: ?string) => ({ diff --git a/plugins/ReactNativeStyle/BoxInspector.js b/plugins/ReactNativeStyle/BoxInspector.js index 94f32d7079..195c372872 100644 --- a/plugins/ReactNativeStyle/BoxInspector.js +++ b/plugins/ReactNativeStyle/BoxInspector.js @@ -10,6 +10,8 @@ */ 'use strict'; +const PropTypes = require('prop-types'); + var React = require('react'); var {sansSerif} = require('../../frontend/Themes/Fonts'); @@ -79,7 +81,7 @@ class BoxInspector extends React.Component { } BoxInspector.contextTypes = { - theme: React.PropTypes.object.isRequired, + theme: PropTypes.object.isRequired, }; const labelStyle = (theme: Theme) => ({ diff --git a/plugins/ReactNativeStyle/StyleEdit.js b/plugins/ReactNativeStyle/StyleEdit.js index f775aca46f..ee59cc5350 100644 --- a/plugins/ReactNativeStyle/StyleEdit.js +++ b/plugins/ReactNativeStyle/StyleEdit.js @@ -10,6 +10,8 @@ */ 'use strict'; +const PropTypes = require('prop-types'); + var React = require('react'); var AutoSizeInput = require('./AutoSizeInput'); @@ -112,7 +114,7 @@ class StyleEdit extends React.Component { } StyleEdit.contextTypes = { - theme: React.PropTypes.object.isRequired, + theme: PropTypes.object.isRequired, }; const blockClick = event => event.stopPropagation(); diff --git a/plugins/Relay/ElementPanel.js b/plugins/Relay/ElementPanel.js index 96c2b0a986..dd1b91636e 100644 --- a/plugins/Relay/ElementPanel.js +++ b/plugins/Relay/ElementPanel.js @@ -10,6 +10,8 @@ */ 'use strict'; +const PropTypes = require('prop-types'); + var React = require('react'); var decorate = require('../../frontend/decorate'); @@ -62,7 +64,7 @@ class ElementPanel extends React.Component { } ElementPanel.contextTypes = { - theme: React.PropTypes.object.isRequired, + theme: PropTypes.object.isRequired, }; const dataNodeStyle = (theme: Theme) => ({ diff --git a/plugins/Relay/Query.js b/plugins/Relay/Query.js index 852fefba97..e330279e3a 100644 --- a/plugins/Relay/Query.js +++ b/plugins/Relay/Query.js @@ -14,6 +14,7 @@ import type {Theme} from '../../frontend/types'; import type {Map} from 'immutable'; var {sansSerif} = require('../../frontend/Themes/Fonts'); +const PropTypes = require('prop-types'); var React = require('react'); type Props = { @@ -61,7 +62,7 @@ class Query extends React.Component { } Query.contextTypes = { - theme: React.PropTypes.object.isRequired, + theme: PropTypes.object.isRequired, }; const baseContainer = { diff --git a/plugins/Relay/StoreTab.js b/plugins/Relay/StoreTab.js index da961015d2..db5f703658 100644 --- a/plugins/Relay/StoreTab.js +++ b/plugins/Relay/StoreTab.js @@ -13,6 +13,8 @@ import type {Map} from 'immutable'; import type {Theme} from '../../frontend/types'; +const PropTypes = require('prop-types'); + var React = require('react'); var DataView = require('../../frontend/DataView/DataView'); var decorate = require('../../frontend/decorate'); @@ -56,7 +58,7 @@ class StoreTab extends React.Component { } StoreTab.contextTypes = { - theme: React.PropTypes.object.isRequired, + theme: PropTypes.object.isRequired, }; const loadingStyle = (theme: Theme) => ({ diff --git a/shells/theme-preview/source/Application.js b/shells/theme-preview/source/Application.js index 3b55209d17..8581d08b21 100644 --- a/shells/theme-preview/source/Application.js +++ b/shells/theme-preview/source/Application.js @@ -10,6 +10,8 @@ */ 'use strict'; +const PropTypes = require('prop-types'); + const React = require('react'); const LeftPane = require('./LeftPane'); @@ -87,12 +89,12 @@ class Application extends React.Component { } Application.childContextTypes = { - store: React.PropTypes.object, - theme: React.PropTypes.object, + store: PropTypes.object, + theme: PropTypes.object, }; Application.propTypes = { - theme: React.PropTypes.object, - updateTheme: React.PropTypes.func, + theme: PropTypes.object, + updateTheme: PropTypes.func, }; const applicationStyle = (theme: Theme) => ({ diff --git a/shells/theme-preview/source/LeftPane.js b/shells/theme-preview/source/LeftPane.js index 6b030f9b1a..49d0ba5808 100644 --- a/shells/theme-preview/source/LeftPane.js +++ b/shells/theme-preview/source/LeftPane.js @@ -10,6 +10,8 @@ */ 'use strict'; +const PropTypes = require('prop-types'); + const React = require('react'); const ExampleIconButton = require('./ExampleIconButton'); @@ -58,7 +60,7 @@ const LeftPane = (_: any, {theme}: Context) => ( ); LeftPane.contextTypes = { - theme: React.PropTypes.object, + theme: PropTypes.object, }; const noop = () => {}; diff --git a/shells/theme-preview/source/RightPane.js b/shells/theme-preview/source/RightPane.js index 9e58b91bdd..7ee0b37e60 100644 --- a/shells/theme-preview/source/RightPane.js +++ b/shells/theme-preview/source/RightPane.js @@ -11,6 +11,7 @@ 'use strict'; const Immutable = require('immutable'); +const PropTypes = require('prop-types'); const React = require('react'); const PropState = require('../../../frontend/PropState'); @@ -36,7 +37,7 @@ class RightPane extends React.Component { } RightPane.childContextTypes = { - onChange: React.PropTypes.func, + onChange: PropTypes.func, }; const fauxNode = Immutable.Map({ diff --git a/yarn.lock b/yarn.lock index d4929667ae..924436b1ae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2825,6 +2825,18 @@ fbjs@^0.8.1, fbjs@^0.8.4: setimmediate "^1.0.5" ua-parser-js "^0.7.9" +fbjs@^0.8.16: + version "0.8.16" + resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.16.tgz#5e67432f550dc41b572bf55847b8aca64e5337db" + dependencies: + core-js "^1.0.0" + isomorphic-fetch "^2.1.1" + loose-envify "^1.0.0" + object-assign "^4.1.0" + promise "^7.1.1" + setimmediate "^1.0.5" + ua-parser-js "^0.7.9" + fbjs@^0.8.9: version "0.8.12" resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.12.tgz#10b5d92f76d45575fd63a217d4ea02bea2f8ed04" @@ -5200,7 +5212,7 @@ object-assign@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-3.0.0.tgz#9bedd5ca0897949bca47e7ff408062d549f587f2" -object-assign@^4.1.0: +object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" @@ -5564,6 +5576,14 @@ prop-types@^15.5.4, prop-types@^15.5.8: fbjs "^0.8.9" loose-envify "^1.3.1" +prop-types@^15.6.1: + version "15.6.1" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.1.tgz#36644453564255ddda391191fb3a125cbdf654ca" + dependencies: + fbjs "^0.8.16" + loose-envify "^1.3.1" + object-assign "^4.1.1" + prr@~0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/prr/-/prr-0.0.0.tgz#1a84b85908325501411853d0081ee3fa86e2926a" From 5018f5ac0d428aef04daca532a56f7bd4ea39763 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Thu, 31 May 2018 08:50:43 -0700 Subject: [PATCH 010/135] Upgrade react and react-dom to 16 --- package.json | 4 ++-- yarn.lock | 36 +++++++++++++----------------------- 2 files changed, 15 insertions(+), 25 deletions(-) diff --git a/package.json b/package.json index 0302d958c6..5c5d7ba4d3 100644 --- a/package.json +++ b/package.json @@ -29,9 +29,9 @@ "object-assign": "4.0.1", "prop-types": "^15.6.1", "raw-loader": "^0.5.1", - "react": "15.4.2", + "react": "^16.4.0", "react-color": "^2.11.7", - "react-dom": "15.4.2", + "react-dom": "^16.4.0", "react-portal": "^3.1.0", "webpack": "1.12.9" }, diff --git a/yarn.lock b/yarn.lock index 924436b1ae..583e615b32 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2813,18 +2813,6 @@ fbjs@0.5.1: ua-parser-js "^0.7.9" whatwg-fetch "^0.9.0" -fbjs@^0.8.1, fbjs@^0.8.4: - version "0.8.8" - resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.8.tgz#02f1b6e0ea0d46c24e0b51a2d24df069563a5ad6" - dependencies: - core-js "^1.0.0" - isomorphic-fetch "^2.1.1" - loose-envify "^1.0.0" - object-assign "^4.1.0" - promise "^7.1.1" - setimmediate "^1.0.5" - ua-parser-js "^0.7.9" - fbjs@^0.8.16: version "0.8.16" resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.16.tgz#5e67432f550dc41b572bf55847b8aca64e5337db" @@ -5576,7 +5564,7 @@ prop-types@^15.5.4, prop-types@^15.5.8: fbjs "^0.8.9" loose-envify "^1.3.1" -prop-types@^15.6.1: +prop-types@^15.6.0, prop-types@^15.6.1: version "15.6.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.1.tgz#36644453564255ddda391191fb3a125cbdf654ca" dependencies: @@ -5696,13 +5684,14 @@ react-color@^2.11.7: reactcss "^1.2.0" tinycolor2 "^1.1.2" -react-dom@15.4.2: - version "15.4.2" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.4.2.tgz#015363f05b0a1fd52ae9efdd3a0060d90695208f" +react-dom@^16.4.0: + version "16.4.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.4.0.tgz#099f067dd5827ce36a29eaf9a6cdc7cbf6216b1e" dependencies: - fbjs "^0.8.1" + fbjs "^0.8.16" loose-envify "^1.1.0" - object-assign "^4.1.0" + object-assign "^4.1.1" + prop-types "^15.6.0" react-portal@^3.1.0: version "3.1.0" @@ -5710,13 +5699,14 @@ react-portal@^3.1.0: dependencies: prop-types "^15.5.8" -react@15.4.2: - version "15.4.2" - resolved "https://registry.yarnpkg.com/react/-/react-15.4.2.tgz#41f7991b26185392ba9bae96c8889e7e018397ef" +react@^16.4.0: + version "16.4.0" + resolved "https://registry.yarnpkg.com/react/-/react-16.4.0.tgz#402c2db83335336fba1962c08b98c6272617d585" dependencies: - fbjs "^0.8.4" + fbjs "^0.8.16" loose-envify "^1.1.0" - object-assign "^4.1.0" + object-assign "^4.1.1" + prop-types "^15.6.0" reactcss@^1.2.0: version "1.2.2" From 341f226045073935a44892282e88b4e494a6ffce Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Fri, 1 Jun 2018 10:42:59 -0700 Subject: [PATCH 011/135] Hackily building a tree with profiling data on commit --- backend/getDataFiber.js | 2 +- plugins/Profiler/ProfilerManager.js | 231 ++-- plugins/Profiler/ProfilerPlugin.js | 2 +- .../Profiler/{Store.js => ProfilerStore.js} | 8 +- plugins/Profiler/ProfilerTab.js | 72 +- plugins/Profiler/ProfilerTypes.js | 33 + plugins/Profiler/backend.js | 6 +- shells/plain/index.html | 113 +- test/example/build/sink.js | 1002 +--------------- test/example/build/target.js | 1012 +---------------- test/example/target.js | 536 +-------- 11 files changed, 489 insertions(+), 2528 deletions(-) rename plugins/Profiler/{Store.js => ProfilerStore.js} (84%) create mode 100644 plugins/Profiler/ProfilerTypes.js diff --git a/backend/getDataFiber.js b/backend/getDataFiber.js index e76db8298a..32fadadcaa 100644 --- a/backend/getDataFiber.js +++ b/backend/getDataFiber.js @@ -186,7 +186,7 @@ function getDataFiber(fiber: Object, getOpaqueNode: (fiber: Object) => Object): case PROFILER_SYMBOL_STRING: nodeType = 'Special'; props = fiber.memoizedProps; - name = 'Profiler'; + name = `Profiler(${fiber.memoizedProps.id})`; children = []; break; default: diff --git a/plugins/Profiler/ProfilerManager.js b/plugins/Profiler/ProfilerManager.js index 0d575acda2..87c1487262 100644 --- a/plugins/Profiler/ProfilerManager.js +++ b/plugins/Profiler/ProfilerManager.js @@ -11,25 +11,26 @@ 'use strict'; -const getDisplayName = require('../../backend/getDisplayName'); - type Agent = any; -type Snapshot = { - actualDuration: number, - actualStartTime: number, - baseTime: number, - fiber: any, - name: string, -}; +import type {Commit, FiberToProfilesMap, Profile, Snapshot} from './ProfilerTypes'; -type FiberToSnapshotMap = Map; - -type Commit = { - commitTime: number, - fiberToSnapshotMap: FiberToSnapshotMap, - root: any, -}; +var { + ASYNC_MODE_NUMBER, + ASYNC_MODE_SYMBOL_STRING, + CONTEXT_CONSUMER_NUMBER, + CONTEXT_CONSUMER_SYMBOL_STRING, + CONTEXT_PROVIDER_NUMBER, + CONTEXT_PROVIDER_SYMBOL_STRING, + FORWARD_REF_NUMBER, + FORWARD_REF_SYMBOL_STRING, + PROFILER_NUMBER, + PROFILER_SYMBOL_STRING, + STRICT_MODE_NUMBER, + STRICT_MODE_SYMBOL_STRING, + TIMEOUT_NUMBER, + TIMEOUT_SYMBOL_STRING, +} = require('../../backend/ReactSymbols'); // TODO (bvaughn) Should this live in a shared constants file like ReactSymbols? // Or should it be in a Fiber-specific file somewhere (like getData)? @@ -40,7 +41,8 @@ const ProfileMode = 0b100; class ProfilerManager { _agent: Agent; _commits: Array = []; - _fiberToSnapshotMap: FiberToSnapshotMap | null = null; + _commitTime: number = 0; + _fiberToProfilesMap: FiberToProfilesMap | null = null; _isRecording: boolean = false; constructor(agent: Agent) { @@ -52,21 +54,97 @@ class ProfilerManager { agent.on('update', this._onMountOrUpdate); } + _storeCurrentCommit() { + if (this._fiberToProfilesMap !== null) { + const mostRecentCommit = this._commits[this._commits.length - 1]; + const snapshot = this._createSnapshotTree(mostRecentCommit); + // TODO (bvaughn) STORE: Save commit data in ProfilerStore. + //TODO_DEBUG_crawlTree(mostRecentCommit.root.current); + TODO_DEBUBG_printSnapshot(snapshot); + } + } + + // Deeply enables ProfileMode. + // Newly inserted Fibers will inherit the mode, + // But existing Fibers need to be explicitly activated. + _enableProfileMode = fiber => { + // eslint-disable-next-line no-bitwise + if (fiber.mode & ProfileMode) { + // Bailout if profiling is already enabled for the subtree. + return; + } + + fiber.mode |= ProfileMode; // eslint-disable-line no-bitwise + if (fiber.alternate !== null) { + fiber.alternate.mode |= ProfileMode; // eslint-disable-line no-bitwise + } + + if (fiber.child !== null) { + this._enableProfileMode(fiber.child); + } + if (fiber.sibling !== null) { + this._enableProfileMode(fiber.sibling); + } + }; + + _createSnapshotTree = (commit: Commit): Snapshot => { + const profile = this._getOrCreateProfile(commit.root.current.child, commit); + return this._populateSnapshotTree(profile.fiber, null, commit); + }; + + _getOrCreateProfile = (fiber: any, commit: Commit): Profile => { + // TODO (bvaughn) This feels fishy. + const profile = commit.fiberToProfilesMap.get(fiber) || commit.fiberToProfilesMap.get(fiber.alternate); + if (profile) { + return profile; + } else { + // TODO (bvaughn) It's unclear how (or if) we'll use this data. + return { + actualDuration: 0, + baseTime: 0, + commitTime: 0, + fiber, + name: getDisplayName(fiber), + startTime: 0, + }; + } + }; + + _populateSnapshotTree = (fiber: any, parentSnapsot: Snapshot | null, commit: Commit): Snapshot => { + const snapshot: Snapshot = { + children: [], + profile: this._getOrCreateProfile(fiber, commit), + }; + + if (parentSnapsot !== null) { + parentSnapsot.children.push(snapshot); + } + + if (fiber.sibling) { + this._populateSnapshotTree(fiber.sibling, parentSnapsot, commit); + } + if (fiber.child) { + this._populateSnapshotTree(fiber.child, snapshot, commit); + } + + return snapshot; + }; + _onCommitRoot = rootID => { if (!this._isRecording) { return; } + this._storeCurrentCommit(); + // This will not match the commit time logged to Profilers in this commit, // But that's probably okay. // DevTools only needs it to group all of the profile timings, // And to place them at a certain point in time in the replay view. - const commitTime = performance.now(); - - this._fiberToSnapshotMap = new Map(); + this._commitTime = performance.now(); + this._fiberToProfilesMap = new Map(); this._commits.push({ - commitTime, - fiberToSnapshotMap: this._fiberToSnapshotMap, + fiberToProfilesMap: this._fiberToProfilesMap, root: this._agent.internalInstancesById.get(rootID), }); }; @@ -78,41 +156,16 @@ class ProfilerManager { const fiber = this._agent.internalInstancesById.get(data.id); - // TODO (bvaughn) Do I need to capture hierarchical information as well? - // So the resulting flame graph can mirror the tree structure somehow? - // Or do we want to always sort by most expensive to least expensive? - ((this._fiberToSnapshotMap: any): FiberToSnapshotMap).set(fiber, { + ((this._fiberToProfilesMap: any): FiberToProfilesMap).set(fiber, { actualDuration: data.profilerData.actualDuration, - actualStartTime: data.profilerData.actualStartTime, baseTime: data.profilerData.baseTime, + commitTime: this._commitTime, fiber, name: data.name, + startTime: data.profilerData.actualStartTime, }); }; - // Deeply enables ProfileMode. - // Newly inserted Fibers will inherit the mode, - // But existing Fibers need to be explicitly activated. - _enableProfileMode = fiber => { - // eslint-disable-next-line no-bitwise - if (fiber.mode & ProfileMode) { - // Bailout if profiling is already enabled for the subtree. - return; - } - - fiber.mode |= ProfileMode; // eslint-disable-line no-bitwise - if (fiber.alternate !== null) { - fiber.alternate.mode |= ProfileMode; // eslint-disable-line no-bitwise - } - - if (fiber.child !== null) { - this._enableProfileMode(fiber.child); - } - if (fiber.sibling !== null) { - this._enableProfileMode(fiber.sibling); - } - }; - _onIsRecording = isRecording => { this._isRecording = isRecording; @@ -126,15 +179,12 @@ class ProfilerManager { }); } - // Dump snapshot data if we are done profiling. - if (!isRecording) { - // TODO (bvaughn) Debugging only; remove this... - const commit = this._commits[this._commits.length - 1]; - printSnapshotTree(commit.root.current.child, commit); - // TODO (bvaughn) Debugging only; remove this^^^ - + if (isRecording) { + // TODO (bvaughn) STORE: Dump previous profiling session if we're starting a new one. + } else { + this._storeCurrentCommit(); this._commits = []; - this._fiberToSnapshotMap = null; + this._fiberToProfilesMap = null; } }; } @@ -147,22 +197,67 @@ module.exports = { init, }; -// TODO (bvaughn) Debugging only; remove this... -const printSnapshotTree = (fiber, commit, depth = 0) => { - // TODO (bvaughn) This is hacky; it's because I'm doing root.current. - const snapshot = commit.fiberToSnapshotMap.get(fiber) || commit.fiberToSnapshotMap.get(fiber.alternate); +// TODO (bvaughn) This is redundant with getDataFiber() and should be shared with it. +const getDisplayName = (fiber: any): string => { + const {type} = fiber; + if (typeof type === 'function') { + return type.displayName || type.name; + } + if (typeof type === 'string') { + return type; + } + switch (type) { + case ASYNC_MODE_NUMBER: + case ASYNC_MODE_SYMBOL_STRING: + return 'AsyncMode'; + case CONTEXT_CONSUMER_NUMBER: + case CONTEXT_CONSUMER_SYMBOL_STRING: + return 'Context.Consumer'; + case PROFILER_NUMBER: + case PROFILER_SYMBOL_STRING: + return `Profiler(${fiber.memoizedProps.id})`; + case CONTEXT_PROVIDER_NUMBER: + case CONTEXT_PROVIDER_SYMBOL_STRING: + return 'Context.Provider'; + case STRICT_MODE_NUMBER: + case STRICT_MODE_SYMBOL_STRING: + return 'StrictMode'; + case TIMEOUT_NUMBER: + case TIMEOUT_SYMBOL_STRING: + return 'Timeout'; + } + if (typeof type === 'object' && type !== null) { + switch (type.$$typeof) { + case FORWARD_REF_NUMBER: + case FORWARD_REF_SYMBOL_STRING: + const functionName = type.render.displayName || type.render.name || ''; + return functionName !== '' + ? `ForwardRef(${functionName})` + : 'ForwardRef'; + } + } + return 'Unknown'; +}; - if (snapshot) { - console.log('••'.repeat(depth), snapshot.name, (commit.fiberToSnapshotMap.has(fiber) ? '(fiber)' : '(alternate)'), 'duration:', snapshot.actualDuration); +const TODO_DEBUBG_printSnapshot = (snapshot: Snapshot, depth: number = 0): void => { + if (depth === 0) { + console.log('----- committed at', snapshot.profile.commitTime); + } + if (snapshot.profile.actualDuration > 0) { + console.log('•'.repeat(depth), snapshot.profile.name, 'start:', snapshot.profile.startTime, 'duration:', snapshot.profile.actualDuration); } else { - console.log('••'.repeat(depth), getDisplayName(fiber)); + console.log('•'.repeat(depth), snapshot.profile.name); } + + snapshot.children.forEach(child => TODO_DEBUBG_printSnapshot(child, depth + 1)); +}; +const TODO_DEBUG_crawlTree = (fiber: any): void => { + console.log(fiber.actualStartTime, '>', fiber.actualDuration, fiber); if (fiber.sibling) { - printSnapshotTree(fiber.sibling, commit, depth); + TODO_DEBUG_crawlTree(fiber.sibling); } if (fiber.child) { - printSnapshotTree(fiber.child, commit, depth + 1); + TODO_DEBUG_crawlTree(fiber.child); } }; -// TODO (bvaughn) Debugging only; remove this^^^ diff --git a/plugins/Profiler/ProfilerPlugin.js b/plugins/Profiler/ProfilerPlugin.js index cb4d8e17e9..ff9443fa57 100644 --- a/plugins/Profiler/ProfilerPlugin.js +++ b/plugins/Profiler/ProfilerPlugin.js @@ -16,7 +16,7 @@ import type Store from '../../frontend/Store'; var React = require('react'); var provideStore = require('../../frontend/provideStore'); -var ProfilerStore = require('./Store'); +var ProfilerStore = require('./ProfilerStore'); var ProfilerTab = require('./ProfilerTab'); var StoreWrapper = provideStore('profilerStore'); diff --git a/plugins/Profiler/Store.js b/plugins/Profiler/ProfilerStore.js similarity index 84% rename from plugins/Profiler/Store.js rename to plugins/Profiler/ProfilerStore.js index dfc4267bd7..f1225bf895 100644 --- a/plugins/Profiler/Store.js +++ b/plugins/Profiler/ProfilerStore.js @@ -10,6 +10,7 @@ */ 'use strict'; +import type {Commit} from './ProfilerTypes'; import type Bridge from '../../agent/Bridge'; var {EventEmitter} = require('events'); @@ -31,12 +32,15 @@ class Store extends EventEmitter { // Noop } - setIsRecording(isRecording: boolean) { - console.log('[Profiler/Store] setIsRecording()', isRecording); + setIsRecording(isRecording: boolean): void { this.isRecording = isRecording; this.emit('isRecording', isRecording); this._mainStore.setIsRecording(isRecording); } + + storeCommit(commit: Commit): void { + // TODO Store + } } module.exports = Store; diff --git a/plugins/Profiler/ProfilerTab.js b/plugins/Profiler/ProfilerTab.js index 1f3340c157..832f88f314 100644 --- a/plugins/Profiler/ProfilerTab.js +++ b/plugins/Profiler/ProfilerTab.js @@ -26,7 +26,7 @@ type Props = {| toggleIsRecording: Function, |}; -class ProfilerTab extends React.Component { +class ProfilerTab extends React.Component { static contextTypes = { theme: PropTypes.object.isRequired, }; @@ -34,18 +34,53 @@ class ProfilerTab extends React.Component { render() { const { theme } = this.context; const { isRecording, toggleIsRecording } = this.props; + + const profilingData = null; // TODO Read from Store/props + + let content; + if (isRecording) { + content = ( + + ); + } else if (profilingData) { + // TODO + } else { + content = ( + + ); + } + return (
- Click the record button to start a new recording. + {content}
); } } +const InactiveNoData = ({startRecording, theme}) => ( + + Click the record button to start a new recording. + +); + +const RecordingInProgress = ({stopRecording, theme}) => ( + + Recording profiling data... + + +); + const RecordButton = Hoverable( ({ isActive, isHovered, onClick, onMouseEnter, onMouseLeave, theme }) => (