├── .babelrc ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── README.md ├── __tests__ ├── __snapshots__ │ ├── commitChanges.test.js.snap │ ├── hostEffects.test.js.snap │ ├── lifecycle.test.js.snap │ ├── mapData.test.js.snap │ └── reactPerformanceData.test.js.snap ├── commitChanges.test.js ├── hostEffects.test.js ├── lifecycle.test.js ├── mapData.test.js ├── measures.test.js ├── reactPerformanceData.test.js └── registerObserver.test.js ├── art ├── Demo.gif ├── RPLogo.png ├── Tool.png └── tab.png ├── extension ├── dependencies │ ├── Chart.js │ ├── react-dom.js │ ├── react.js │ └── require.js ├── devtools.html ├── icon │ ├── RP128.png │ ├── RP16.png │ └── RP48.png ├── load.html ├── load.js ├── manifest.json ├── panel.js └── styles │ └── index.css ├── index.d.ts ├── index.js ├── package.json ├── samples └── measures.js ├── src ├── extension │ ├── components │ │ ├── Buttons.js │ │ ├── ComponentTime.js │ │ ├── ErrorComponent.js │ │ ├── Graphics.js │ │ ├── Measures.js │ │ ├── Metrics.js │ │ ├── ProgressLoader.js │ │ ├── ReactPerfDevtool.js │ │ └── Stats.js │ ├── theme.js │ └── util.js ├── hook.js ├── index.js ├── npm │ └── hook.js └── shared │ ├── commitChanges.js │ ├── generate.js │ ├── hostEffects.js │ ├── lifecycle.js │ ├── math.js │ ├── parse.js │ ├── parseMeasures.js │ └── totalTime.js ├── webpack ├── webpack.config.hook.js └── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env", "react"], 3 | "plugins": ["transform-class-properties", "transform-object-rest-spread"] 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | extension/build 4 | trash 5 | yarn-error.log 6 | extension.crx 7 | extension.zip 8 | extension.pem 9 | lib 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | notifications: 5 | email: false 6 | script: 7 | - yarn format:nowrite 8 | - yarn test 9 | - yarn build 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing guide 2 | 3 | I'm excited to have you helping out. Thank you so much for your time 😄 4 | 5 | ## Contributing 6 | 7 | ### Understanding the codebase 8 | 9 | The source code for the table and results lives in [`src`](./src) folder. 10 | There are three folders inside `src`, [extension](./src/extension), [npm](./src/npm), and [shared](./src/shared). The `extension` folder contains the devtool UI components, `npm` folder contains the code for registering the observer and `shared` contains the code that which is shared by both `extension` and `npm`, and is responsible for parsing the React performance data and generating an output which is easier to work with. 11 | 12 | In the root directory, [`extension`](./extension) folder contains the dependencies used by `react-perf-devtool` and a [`load.js`](./extension/load.js) script to load the devtool. 13 | 14 | ### Setting up the development environment 15 | 16 | **Installation** 17 | 18 | Considering you've forked and cloned the repo on your system, switch to the directory and install the dependencies. 19 | 20 | ``` 21 | cd react-perf-devtool 22 | yarn install 23 | ``` 24 | 25 | **Build** 26 | 27 | After you've made changes to the project, you'll need to load the unpacked extension in Chrome to test whether its working or not. For this, you'll first need to generate a build. 28 | 29 | ``` 30 | yarn build 31 | ``` 32 | 33 | Running this command will create a build directory in `./extension` folder. 34 | 35 | 36 | You can also automatically rebuild the extension on every file change using the watch command: 37 | ``` 38 | yarn build:watch 39 | ``` 40 | 41 | **Test** 42 | 43 | To test your changes, simply run `jest --watch` if you've installed Jest globally on your system or run `yarn test`. 44 | 45 | > [Read the documentation about Jest](https://facebook.github.io/jest/) 46 | 47 | **Format** 48 | 49 | To format all the files in `./src` directory, run `yarn format`. This will format all the files using [Prettier](https://prettier.io/). 50 | 51 | **Generate** 52 | 53 | To generate a `.zip` extension (required when uploading the extension), run `yarn generate`. This will create a `.zip` for the extension and will also generate a build directory. 54 | 55 | **Uploading the extension on Chrome and Firefox** 56 | 57 | This extension is cross-browser compatible because it uses the [Web Extension API](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API) but with few changes to `manifest.json` file. 58 | 59 | To upload the extension on Google Chrome: 60 | 61 | * Generate a build using `yarn build`. 62 | * Go to `chrome://extensions`. 63 | * Load the unpacked extension by loading the `./extension` directory. 64 | * Start your development server (eg - localhost:3000?react_perf) and open the extension using Chrome devtools. 65 | * Reload the inspected window if necessary and you should see the performance measures of your React components. 66 | 67 | To upload the extension on Firefox: 68 | 69 | * Follow the same procedure for generating the build. 70 | * Go to `about:debugging` and check the box for *Enable add-on debugging*. 71 | * Then click the button **Load Temporary Add-on** and select any file from the extension directory. 72 | * Open the devtools and you should see the extension loaded. 73 | 74 | **Inspecting the measures** 75 | 76 | After uploading the extension in the browser, you will see that it will collect the measures using `window.performance` API and will display the performance stats in a table. Below the table, you'll see some more results which includes commit host effects time, commit changes time, lifecycle methods and total time. 77 | 78 | To learn more about these stats and different phases, see [this](https://github.com/nitin42/react-perf-devtool#description) guide. 79 | 80 | ### Submitting pull requests 81 | 82 | * Create a new branch for the new feature: git checkout -b new-feature 83 | * Make your changes. 84 | * Test everything with yarn test. 85 | * Commit your changes: git commit -m 'Added some new feature' 86 | * Push to the branch: git push origin new-feature 87 | * Submit a pull request with full remarks documenting your changes. 88 | * Test the changes locally 89 | * You can test your changes locally by first running `yarn build` and then loading the extension in Chrome by navigating to `chrome://extensions/`. Browse the extension folder in the root directory and load it. 90 | 91 | That's it! I am excited to see your pull request. 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **Looking for maintainers** 2 | 3 | # React Performance Devtool 4 | 5 | [![Build Status](https://travis-ci.org/nitin42/react-perf-devtool.svg?branch=master)](https://travis-ci.org/nitin42/react-perf-devtool) 6 | ![Release Status](https://img.shields.io/badge/status-stable-brightgreen.svg) 7 | ![Author](https://img.shields.io/badge/author-Nitin%20Tulswani-lightgrey.svg) 8 | ![current-version](https://img.shields.io/badge/version-3.1.8-blue.svg) 9 | ![extension](https://img.shields.io/badge/extension-5.3-ff69b4.svg) 10 | [![npm downloads](https://img.shields.io/npm/dt/react-perf-devtool.svg)](https://www.npmjs.com/package/react-perf-devtool) 11 | 12 | > A devtool for inspecting the performance of React Components 13 | 14 |
15 | 16 |

17 | 18 |

19 | 20 |
21 | 22 | ## Table of contents 23 | 24 | * [Introduction](#introduction) 25 | 26 | * [Demo](#demo) 27 | * [Browser extension](#browser-extension) 28 | * [Log the measures to console](#log-the-measures-to-a-console) 29 | 30 | * [Uses](#uses) 31 | 32 | * [Install](#install) 33 | 34 | * [Usage](#usage) 35 | * [Using the browser extension](#using-the-browser-extension) 36 | * [Printing the measures to console](#printing-the-measures-to-the-console) 37 | 38 | * [Description](#description) 39 | 40 | * [Phases](#phases) 41 | 42 | * [Implementation](#implementation) 43 | 44 | * [Contributing](#contributing) 45 | 46 | * [License](#license) 47 | 48 | 49 | ## Introduction 50 | 51 | **React Performance Devtool** is a browser extension for inspecting the performance of React Components. It statistically examines the performance of React components based on the measures which are collected by React using `window.performance` API. 52 | 53 | Along with the browser extension, the measures can also be inspected in a console. See the [usage](#usage) section for more details. 54 | 55 | This project started with a purpose of extending the work done by [Will Chen](https://github.com/wwwillchen) on a proposal for React performance table. You can read more about it [here](https://github.com/facebook/react-devtools/issues/801#issuecomment-350919145). 56 | 57 | ## Demo 58 | 59 | ### Browser extension 60 | 61 | A demo of the extension being used to examine the performance of React components on my website. 62 | 63 | 64 | 65 | ### Log the measures to a console 66 | 67 | Performance measures can also be logged to a console. With every re-render, measures are updated and logged to the console. 68 | 69 | 70 | 71 | ## Uses 72 | 73 | * Remove or unmount the component instances which are not being used. 74 | 75 | * Inspect what is blocking or taking more time after an operation has been started. 76 | 77 | * Examine the table and see for which components, you need to write [shouldComponentUpdate](https://reactjs.org/docs/react-component.html#shouldcomponentupdate) lifecycle hook. 78 | 79 | * Examine which components are taking more time to load. 80 | 81 | ## Install 82 | 83 | To use this devtool, you'll need to install a npm module which will register a listener (read more about this in [usage](#usage) section) and the browser extension. 84 | 85 | **Installing the extension** 86 | 87 | The below extensions represent the current stable release. 88 | 89 | * [Chrome extension](https://chrome.google.com/webstore/detail/react-performance-devtool/fcombecpigkkfcbfaeikoeegkmkjfbfm) 90 | * [Firefox extension](https://addons.mozilla.org/en-US/firefox/addon/nitin-tulswani/) 91 | * **Standalone app coming soon** 92 | 93 | **Installing the npm module** 94 | 95 | ``` 96 | npm install react-perf-devtool 97 | ``` 98 | 99 | A `umd` build is also available via [unpkg](https://www.unpkg.com) 100 | 101 | ```js 102 | 103 | ``` 104 | 105 | > This extension and package also depends on react. Please make sure you have those installed as well. 106 | 107 | > Note - The npm module is important and required to use the devtool. So make sure you've installed it before using the browser extension. 108 | 109 | ## Usage 110 | 111 | This section of the documentation explain the usage of devtool and the API for registering an observer in a React app. 112 | 113 | ### Browser Compatibility 114 | `react-perf-devtool` relies on the native `window.PerformanceObserver` API that got added in **Chrome v52** and **Firefox v57**. For further information, see the official Mozilla Docs [here](https://developer.mozilla.org/en-US/docs/Web/API/PerformanceObserver#Browser_compatibility). 115 | 116 | ### Using the browser extension 117 | 118 | To use this devtool extension, you'll need to register an observer in your app which will observe a collection of data (performance measures) over a time. 119 | 120 | **Register observer** 121 | 122 | Registering an observer is very simple and is only one function call away. Let's see how! 123 | 124 | ```js 125 | const { registerObserver } = require('react-perf-devtool') 126 | 127 | // assign the observer to the global scope, as the GC will delete it otherwise 128 | window.observer = registerObserver() 129 | ``` 130 | 131 | You can place this code inside your `index.js` file (recommended) or any other file in your app. 132 | 133 | > Note - This should only be used in development mode when you need to inspect the performance of React components. Make sure to remove it when building for production. 134 | 135 | Registering an observer hooks an object containing information about the **events** and **performance measures** of React components to the 136 | [window](https://developer.mozilla.org/en-US/docs/Web/API/Window/window) object, which can then be accessed inside the inspected window using [eval()](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/devtools.inspectedWindow/eval). 137 | 138 | With every re-render, this object is updated with new measures and events count. 139 | The extension takes care of clearing up the memory and also the cache. 140 | 141 | You can also pass an **`option`** object and an optional **`callback`** which receives an argument containing the parsed and aggregated measures 142 | 143 | **Using the callback** 144 | 145 | An optional callback can also be passed to `registerObserver` which receives parsed measures as its argument. 146 | 147 | You can use this callback to inspect the parsed and aggregated measures, or you can integrate it with any other use case. You can also leverage these performance measures using Google Analytics by sending these measures to analytics dashboard . This process is documented [here](https://developers.google.com/web/updates/2017/06/user-centric-performance-metrics). 148 | 149 | Example - 150 | 151 | ```js 152 | const { registerObserver } = require('react-perf-devtool') 153 | 154 | function callback(measures) { 155 | // do something with the measures 156 | } 157 | 158 | // assign the observer to the global scope, as the GC will delete it otherwise 159 | window.observer = registerObserver({}, callback) 160 | ``` 161 | 162 | After you've registered the observer, start your local development server and go to `http://localhost:3000/`. 163 | 164 | > Note - This extension works only for React 16 or above versions of it. 165 | 166 | After you've installed the extension successfully, you'll see a tab called **React Performance** in Chrome Developer Tools. 167 | 168 | 169 | 170 | ### Printing the measures to the console 171 | 172 | The performance measures can also be logged to the console. However, the process of printing the measures is not direct. You'll need to set up a server which will listen the measures. For this, you can use [micro](https://github.com/zeit/micro) by [Zeit](https://zeit.co/) which is a HTTP microservice. 173 | 174 | ``` 175 | npm install --save micro 176 | ``` 177 | 178 | 179 | You can pass an **option** object as an argument to `registerObserver` to enable logging and setting up a port number. 180 | 181 | **Using the option object** 182 | 183 | ```js 184 | { 185 | shouldLog: boolean, // default value: false 186 | port: number // default value: 8080 187 | timeout: number // default value: 2000 188 | } 189 | ``` 190 | 191 | You can pass three properties to the **`option`** object, `shouldLog` and `port`. 192 | 193 | * `shouldLog` - It takes a **boolean** value. If set to true, measures will be logged to the console. 194 | 195 | * `port` - Port number for the server where the measures will be send 196 | 197 | * `timeout` - A timeout value to defer the initialisation of the extension. 198 | 199 | If your application takes time to load, it's better to defer the initialisation of extension by specifying the timeout value through `timeout` property. This ensures that the extension will load only after your application has properly loaded in the browser so that the updated measures can be rendered. However, you can skip this property if your application is in small size. 200 | 201 | **Example** 202 | 203 | ```js 204 | // index.js file in your React App 205 | 206 | const React = require('react') 207 | const ReactDOM = require('react-dom') 208 | const { registerObserver } = require('react-perf-devtool') 209 | 210 | const Component = require('./Component') // Some React Component 211 | 212 | const options = { 213 | shouldLog: true, 214 | port: 8080, 215 | timeout: 12000 // Load the extension after 12 sec. 216 | } 217 | 218 | function callback(measures) { 219 | // do something with the measures 220 | } 221 | 222 | // assign the observer to the global scope, as the GC will delete it otherwise 223 | window.observer = registerObserver(options, callback) 224 | 225 | ReactDOM.render(, document.getElementById('root')) 226 | ``` 227 | 228 | ```js 229 | // server.js 230 | const { json } = require('micro') 231 | 232 | module.exports = async req => { 233 | console.log(await json(req)) 234 | return 200 235 | } 236 | ``` 237 | 238 | ```js 239 | // package.json 240 | 241 | { 242 | "main": "server.js", 243 | "scripts": { 244 | "start-micro": "micro -p 8080" 245 | } 246 | } 247 | 248 | ``` 249 | 250 | **Schema of the measures** 251 | 252 | Below is the schema of the performance measures that are logged to the console. 253 | 254 | ```js 255 | { 256 | componentName, 257 | mount: { // Mount time 258 | averageTimeSpentMs, 259 | numberOfTimes, 260 | totalTimeSpentMs, 261 | }, 262 | render: { // Render time 263 | averageTimeSpentMs, 264 | numberOfTimes, 265 | totalTimeSpentMs, 266 | }, 267 | update: { // Update time 268 | averageTimeSpentMs, 269 | numberOfTimes, 270 | totalTimeSpentMs, 271 | }, 272 | unmount: { // Unmount time 273 | averageTimeSpentMs, 274 | numberOfTimes, 275 | totalTimeSpentMs, 276 | }, 277 | totalTimeSpent, // Total time taken by the component combining all the phases 278 | percentTimeSpent, // Percent time 279 | numberOfInstances, // Number of instances of the component 280 | 281 | // Time taken in lifecycle hooks 282 | componentWillMount: { 283 | averageTimeSpentMs, 284 | numberOfTimes, 285 | totalTimeSpentMs, 286 | } 287 | componentDidMount: { 288 | averageTimeSpentMs, 289 | numberOfTimes, 290 | totalTimeSpentMs, 291 | } 292 | componentWillReceiveProps: { 293 | averageTimeSpentMs, 294 | numberOfTimes, 295 | totalTimeSpentMs, 296 | }, 297 | shouldComponentUpdate: { 298 | averageTimeSpentMs, 299 | numberOfTimes, 300 | totalTimeSpentMs, 301 | }, 302 | componentWillUpdate: { 303 | averageTimeSpentMs, 304 | numberOfTimes, 305 | totalTimeSpentMs, 306 | }, 307 | componentDidUpdate: { 308 | averageTimeSpentMs, 309 | numberOfTimes, 310 | totalTimeSpentMs, 311 | }, 312 | componentWillUnmount: { 313 | averageTimeSpentMs, 314 | numberOfTimes, 315 | totalTimeSpentMs, 316 | } 317 | } 318 | ``` 319 | 320 | **components** 321 | 322 | You can also inspect the performance of specific components using options through **`components`** property. 323 | 324 | 325 | 326 | Example - 327 | 328 | ```js 329 | const options = { 330 | shouldLog: true, 331 | port: 3000, 332 | components: ['App', 'Main'] // Assuming you've these components in your project 333 | } 334 | 335 | function callback(measures) { 336 | // do something with measures 337 | } 338 | 339 | // assign the observer to the global scope, as the GC will delete it otherwise 340 | window.observer = registerObserver(options, callback) 341 | ``` 342 | 343 | ## Description 344 | 345 | ### Overview section 346 | 347 |

348 | 349 |

350 | 351 | Overview section represents an overview of total time (%) taken by all the components in your application. 352 | 353 | ### Results section 354 | 355 |

356 | 357 |

358 | 359 | * Time taken by all the components - Shows the time taken by all the components (combining all the phases). 360 | 361 | * Time duration for committing changes - Shows the time spent in committing changes. Read more about this [here]() 362 | 363 | * Time duration for committing host effects - Shows the time spent in committing host effects i.e committing when a new tree is inserted (update) and no. of host effects (effect count in commit). 364 | 365 | * Time duration for calling lifecycle methods - Reports the time duration of calling lifecycle hooks and total no of methods called, when a lifecycle hook schedules a cascading update. 366 | 367 | * Total time 368 | 369 | ### Top section 370 | 371 |

372 | 373 |

374 | 375 | **clear** - The clear button clears the measures from the tables and also wipes the results. 376 | 377 | **Reload the inspected window** - This button reloads the inspected window and displays the new measures. 378 | 379 | **Pending events** - This indicates the pending measures (React performance data). 380 | 381 | 382 | ### Components section 383 | 384 |

385 | 386 |

387 | 388 | This section shows the time taken by a component in a phase, number of instances of a component and total time combining all the phases in **ms** and **%** 389 | 390 | ## Phases 391 | 392 | Given below are the different phases for which React measures the performance: 393 | 394 | * **React Tree Reconciliation** - In this phase, React renders the root node and creates a work in progress fiber. If there were some cascading updates while reconciling, it will pause any active measurements and will resumed them in a deferred loop. This is caused when a top-level update interrupts the previous render. If an error was thrown during the render phase then it captures the error by finding the nearest error boundary or it uses the root if there is no error boundary. 395 | 396 | * **Commit changes** - In this phase, the work that was completed is committed. Also, it checks whether the root node has any side-effect. If it has an effect then add it to the list (read more this list data structure [here](https://github.com/nitin42/Making-a-custom-React-renderer/blob/master/part-one.md)) or commit all the side-effects in the tree. If there is a scheduled update in the current commit, then it gives a warning about ***cascading update in lifecycle hook***. During the commit phase, updates are scheduled in the current commit. Also, updates are scheduled if the phase/stage is not [componentWillMount](https://reactjs.org/docs/react-component.html#componentwillmount) or [componentWillReceiveProps](https://reactjs.org/docs/react-component.html#componentwillreceiveprops). 397 | 398 | * **Commit host effects** - Host effects are committed whenever a new tree is inserted. With every new update that is scheduled, total host effects are calculated. This process is done in two phases, the first phase performs all the host node insertions, deletion, update and ref unmounts and the other phase performs all the lifecycle and ref callbacks. 399 | 400 | * **Commit lifecycle** - When the first pass was completed while committing the host effects, the work in progress tree became the current tree. So work in progress is current during **componentDidMount/update**. In this phase, all the lifecycles and ref callbacks are committed. **Committing lifecycles happen as a separate pass so that all the placements, updates and deletions in the entire tree have already been invoked**. 401 | 402 | ## Implementation 403 | 404 | In previous version of this devtool, performance metrics were being queried instead of listening for an event type. This required to comment the line inside the `react-dom` package (`react-dom.development.js`) so that these metrics can be captured by this tool. 405 | 406 | ### Trade-offs 407 | * Need to update the commonjs react-dom development bundle (commenting the line) 408 | * No way of sending the measures from the app frame to the console 409 | * Need to query measures rather than listening to an event once 410 | * No control on how to inspect the measures for a particular use case (for eg - log only the render and update performance of a component) 411 | 412 | But now, with the help of [Performance Observer](https://developer.mozilla.org/en-US/docs/Web/API/PerformanceObserver) API, an observer can be registered to listen to an event of a particular type and get the entries (performance measures). `react-perf-devtool` provides an API on top of the performance observer, a function that registers an observer. 413 | 414 | ```js 415 | const { registerObserver } = require('react-perf-devtool') 416 | 417 | // assign the observer to the global scope, as the GC will delete it otherwise 418 | window.observer = registerObserver() 419 | ``` 420 | 421 | This observer listens to the React performance measurement event. 422 | It hooks an object containing information about the events and performance measures of React components to the window object which can then be accessed inside the inspected window using eval(). 423 | 424 | With every re-render, this object is updated with new measures and events count. The extension takes care of clearing up the memory and also the cache. 425 | 426 | An `option` object and an optional `callback` can also be passed to `registerObserver`. The `option` object is useful when performance measures are to be logged to a console. The `callback` receives parsed and aggregated results (metrics) as its argument which can then be used for analyses. 427 | ### Benefits 428 | 429 | Calculating and aggregating the results happens inside the app frame and not in the devtool. It has its own benefits. 430 | * These measures can be send to a server for analyses 431 | * Measures can be logged to a console 432 | * Particular measures can be inspected in the console with the help of configuration object (not done with the API for it yet) 433 | * This also gives control to the developer on how to manage and inspect the measures apart from using the extension 434 | 435 | ## Todos / Ideas / Improvements 436 | 437 | - [x] New UI for devtool 438 | - [x] Make the implementation of measures generator more concrete 439 | - [ ] Add support for older versions of React 440 | - [x] Make the tool more comprehensible 441 | 442 | ## Contributing 443 | 444 | [Read the contributing guide](./CONTRIBUTING.md) 445 | 446 | ## License 447 | 448 | MIT 449 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/commitChanges.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Committing changes time duration Committing changes with and without warning for cascading updates in earlier commits 1`] = ` 4 | Object { 5 | "Committing Changes": Object { 6 | "timeSpent": Array [ 7 | 6.21, 8 | 1.14, 9 | ], 10 | }, 11 | } 12 | `; 13 | 14 | exports[`Committing changes time duration Returns total time taken in committing changes 1`] = `7.35`; 15 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/hostEffects.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Committing Host Effects Sanity 1`] = ` 4 | Object { 5 | "Committing Host Effects": Object { 6 | "timeSpent": Array [ 7 | 1.14, 8 | 0.09, 9 | ], 10 | "totalEffects": Array [ 11 | 7, 12 | 1, 13 | ], 14 | }, 15 | } 16 | `; 17 | 18 | exports[`Committing Host Effects Total effects 1`] = `8`; 19 | 20 | exports[`Committing Host Effects Total time taken in committing host effects 1`] = `1.23`; 21 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/lifecycle.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Calling Lifecycle Methods Sanity 1`] = ` 4 | Object { 5 | "Calling Lifecycle Methods": Object { 6 | "timeSpent": Array [ 7 | 4.38, 8 | 0.89, 9 | ], 10 | "totalMethods": Array [ 11 | 6, 12 | 1, 13 | ], 14 | }, 15 | } 16 | `; 17 | 18 | exports[`Calling Lifecycle Methods Total effects 1`] = `7`; 19 | 20 | exports[`Calling Lifecycle Methods Total time taken in calling lifecycle methods 1`] = `5.27`; 21 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/mapData.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Map Data from React Performance Data Returns an object containing information about the component measure 1`] = ` 4 | Array [ 5 | Object { 6 | "componentDidMount": Object { 7 | "averageTimeSpentMs": "0.92", 8 | "numberOfTimes": 1, 9 | "totalTimeSpentMs": 0.92, 10 | }, 11 | "componentDidUpdate": Object { 12 | "averageTimeSpentMs": "-", 13 | "numberOfTimes": 0, 14 | "totalTimeSpentMs": 0, 15 | }, 16 | "componentName": "App", 17 | "componentWillMount": Object { 18 | "averageTimeSpentMs": "3.02", 19 | "numberOfTimes": 1, 20 | "totalTimeSpentMs": 3.02, 21 | }, 22 | "componentWillReceiveProps": Object { 23 | "averageTimeSpentMs": "-", 24 | "numberOfTimes": 0, 25 | "totalTimeSpentMs": 0, 26 | }, 27 | "componentWillUnmount": Object { 28 | "averageTimeSpentMs": "-", 29 | "numberOfTimes": 0, 30 | "totalTimeSpentMs": 0, 31 | }, 32 | "componentWillUpdate": Object { 33 | "averageTimeSpentMs": "-", 34 | "numberOfTimes": 0, 35 | "totalTimeSpentMs": 0, 36 | }, 37 | "mount": Object { 38 | "averageTimeSpentMs": "36.88", 39 | "numberOfTimes": 1, 40 | "totalTimeSpentMs": 36.88, 41 | }, 42 | "numberOfInstances": 1, 43 | "percentTimeSpent": "35%", 44 | "render": Object { 45 | "averageTimeSpentMs": "-", 46 | "numberOfTimes": 0, 47 | "totalTimeSpentMs": 0, 48 | }, 49 | "shouldComponentUpdate": Object { 50 | "averageTimeSpentMs": "-", 51 | "numberOfTimes": 0, 52 | "totalTimeSpentMs": 0, 53 | }, 54 | "totalTimeSpent": 40.82000000000001, 55 | "unmount": Object { 56 | "averageTimeSpentMs": "-", 57 | "numberOfTimes": 0, 58 | "totalTimeSpentMs": 0, 59 | }, 60 | "update": Object { 61 | "averageTimeSpentMs": "-", 62 | "numberOfTimes": 0, 63 | "totalTimeSpentMs": 0, 64 | }, 65 | }, 66 | Object { 67 | "componentDidMount": Object { 68 | "averageTimeSpentMs": "0.81", 69 | "numberOfTimes": 1, 70 | "totalTimeSpentMs": 0.81, 71 | }, 72 | "componentDidUpdate": Object { 73 | "averageTimeSpentMs": "-", 74 | "numberOfTimes": 0, 75 | "totalTimeSpentMs": 0, 76 | }, 77 | "componentName": "Main", 78 | "componentWillMount": Object { 79 | "averageTimeSpentMs": "0.88", 80 | "numberOfTimes": 1, 81 | "totalTimeSpentMs": 0.88, 82 | }, 83 | "componentWillReceiveProps": Object { 84 | "averageTimeSpentMs": "-", 85 | "numberOfTimes": 0, 86 | "totalTimeSpentMs": 0, 87 | }, 88 | "componentWillUnmount": Object { 89 | "averageTimeSpentMs": "-", 90 | "numberOfTimes": 0, 91 | "totalTimeSpentMs": 0, 92 | }, 93 | "componentWillUpdate": Object { 94 | "averageTimeSpentMs": "-", 95 | "numberOfTimes": 0, 96 | "totalTimeSpentMs": 0, 97 | }, 98 | "mount": Object { 99 | "averageTimeSpentMs": "31.35", 100 | "numberOfTimes": 1, 101 | "totalTimeSpentMs": 31.35, 102 | }, 103 | "numberOfInstances": 1, 104 | "percentTimeSpent": "29%", 105 | "render": Object { 106 | "averageTimeSpentMs": "-", 107 | "numberOfTimes": 0, 108 | "totalTimeSpentMs": 0, 109 | }, 110 | "shouldComponentUpdate": Object { 111 | "averageTimeSpentMs": "-", 112 | "numberOfTimes": 0, 113 | "totalTimeSpentMs": 0, 114 | }, 115 | "totalTimeSpent": 33.040000000000006, 116 | "unmount": Object { 117 | "averageTimeSpentMs": "-", 118 | "numberOfTimes": 0, 119 | "totalTimeSpentMs": 0, 120 | }, 121 | "update": Object { 122 | "averageTimeSpentMs": "-", 123 | "numberOfTimes": 0, 124 | "totalTimeSpentMs": 0, 125 | }, 126 | }, 127 | Object { 128 | "componentDidMount": Object { 129 | "averageTimeSpentMs": "0.46", 130 | "numberOfTimes": 1, 131 | "totalTimeSpentMs": 0.46, 132 | }, 133 | "componentDidUpdate": Object { 134 | "averageTimeSpentMs": "0.81", 135 | "numberOfTimes": 1, 136 | "totalTimeSpentMs": 0.81, 137 | }, 138 | "componentName": "Introduction", 139 | "componentWillMount": Object { 140 | "averageTimeSpentMs": "0.84", 141 | "numberOfTimes": 1, 142 | "totalTimeSpentMs": 0.84, 143 | }, 144 | "componentWillReceiveProps": Object { 145 | "averageTimeSpentMs": "-", 146 | "numberOfTimes": 0, 147 | "totalTimeSpentMs": 0, 148 | }, 149 | "componentWillUnmount": Object { 150 | "averageTimeSpentMs": "-", 151 | "numberOfTimes": 0, 152 | "totalTimeSpentMs": 0, 153 | }, 154 | "componentWillUpdate": Object { 155 | "averageTimeSpentMs": "-", 156 | "numberOfTimes": 0, 157 | "totalTimeSpentMs": 0, 158 | }, 159 | "mount": Object { 160 | "averageTimeSpentMs": "11.33", 161 | "numberOfTimes": 1, 162 | "totalTimeSpentMs": 11.33, 163 | }, 164 | "numberOfInstances": 1, 165 | "percentTimeSpent": "14%", 166 | "render": Object { 167 | "averageTimeSpentMs": "-", 168 | "numberOfTimes": 0, 169 | "totalTimeSpentMs": 0, 170 | }, 171 | "shouldComponentUpdate": Object { 172 | "averageTimeSpentMs": "-", 173 | "numberOfTimes": 0, 174 | "totalTimeSpentMs": 0, 175 | }, 176 | "totalTimeSpent": 16.27, 177 | "unmount": Object { 178 | "averageTimeSpentMs": "-", 179 | "numberOfTimes": 0, 180 | "totalTimeSpentMs": 0, 181 | }, 182 | "update": Object { 183 | "averageTimeSpentMs": "2.83", 184 | "numberOfTimes": 1, 185 | "totalTimeSpentMs": 2.83, 186 | }, 187 | }, 188 | Object { 189 | "componentDidMount": Object { 190 | "averageTimeSpentMs": "0.08", 191 | "numberOfTimes": 1, 192 | "totalTimeSpentMs": 0.08, 193 | }, 194 | "componentDidUpdate": Object { 195 | "averageTimeSpentMs": "-", 196 | "numberOfTimes": 0, 197 | "totalTimeSpentMs": 0, 198 | }, 199 | "componentName": "Projects", 200 | "componentWillMount": Object { 201 | "averageTimeSpentMs": "1.85", 202 | "numberOfTimes": 1, 203 | "totalTimeSpentMs": 1.85, 204 | }, 205 | "componentWillReceiveProps": Object { 206 | "averageTimeSpentMs": "-", 207 | "numberOfTimes": 0, 208 | "totalTimeSpentMs": 0, 209 | }, 210 | "componentWillUnmount": Object { 211 | "averageTimeSpentMs": "-", 212 | "numberOfTimes": 0, 213 | "totalTimeSpentMs": 0, 214 | }, 215 | "componentWillUpdate": Object { 216 | "averageTimeSpentMs": "-", 217 | "numberOfTimes": 0, 218 | "totalTimeSpentMs": 0, 219 | }, 220 | "mount": Object { 221 | "averageTimeSpentMs": "9.44", 222 | "numberOfTimes": 1, 223 | "totalTimeSpentMs": 9.44, 224 | }, 225 | "numberOfInstances": 1, 226 | "percentTimeSpent": "13%", 227 | "render": Object { 228 | "averageTimeSpentMs": "-", 229 | "numberOfTimes": 0, 230 | "totalTimeSpentMs": 0, 231 | }, 232 | "shouldComponentUpdate": Object { 233 | "averageTimeSpentMs": "-", 234 | "numberOfTimes": 0, 235 | "totalTimeSpentMs": 0, 236 | }, 237 | "totalTimeSpent": 14.399999999999999, 238 | "unmount": Object { 239 | "averageTimeSpentMs": "-", 240 | "numberOfTimes": 0, 241 | "totalTimeSpentMs": 0, 242 | }, 243 | "update": Object { 244 | "averageTimeSpentMs": "3.03", 245 | "numberOfTimes": 1, 246 | "totalTimeSpentMs": 3.03, 247 | }, 248 | }, 249 | Object { 250 | "componentDidMount": Object { 251 | "averageTimeSpentMs": "0.83", 252 | "numberOfTimes": 1, 253 | "totalTimeSpentMs": 0.83, 254 | }, 255 | "componentDidUpdate": Object { 256 | "averageTimeSpentMs": "-", 257 | "numberOfTimes": 0, 258 | "totalTimeSpentMs": 0, 259 | }, 260 | "componentName": "Posts", 261 | "componentWillMount": Object { 262 | "averageTimeSpentMs": "1.11", 263 | "numberOfTimes": 1, 264 | "totalTimeSpentMs": 1.11, 265 | }, 266 | "componentWillReceiveProps": Object { 267 | "averageTimeSpentMs": "-", 268 | "numberOfTimes": 0, 269 | "totalTimeSpentMs": 0, 270 | }, 271 | "componentWillUnmount": Object { 272 | "averageTimeSpentMs": "-", 273 | "numberOfTimes": 0, 274 | "totalTimeSpentMs": 0, 275 | }, 276 | "componentWillUpdate": Object { 277 | "averageTimeSpentMs": "-", 278 | "numberOfTimes": 0, 279 | "totalTimeSpentMs": 0, 280 | }, 281 | "mount": Object { 282 | "averageTimeSpentMs": "4.05", 283 | "numberOfTimes": 1, 284 | "totalTimeSpentMs": 4.05, 285 | }, 286 | "numberOfInstances": 1, 287 | "percentTimeSpent": "5%", 288 | "render": Object { 289 | "averageTimeSpentMs": "-", 290 | "numberOfTimes": 0, 291 | "totalTimeSpentMs": 0, 292 | }, 293 | "shouldComponentUpdate": Object { 294 | "averageTimeSpentMs": "-", 295 | "numberOfTimes": 0, 296 | "totalTimeSpentMs": 0, 297 | }, 298 | "totalTimeSpent": 5.99, 299 | "unmount": Object { 300 | "averageTimeSpentMs": "-", 301 | "numberOfTimes": 0, 302 | "totalTimeSpentMs": 0, 303 | }, 304 | "update": Object { 305 | "averageTimeSpentMs": "-", 306 | "numberOfTimes": 0, 307 | "totalTimeSpentMs": 0, 308 | }, 309 | }, 310 | Object { 311 | "componentDidMount": Object { 312 | "averageTimeSpentMs": "0.55", 313 | "numberOfTimes": 1, 314 | "totalTimeSpentMs": 0.55, 315 | }, 316 | "componentDidUpdate": Object { 317 | "averageTimeSpentMs": "-", 318 | "numberOfTimes": 0, 319 | "totalTimeSpentMs": 0, 320 | }, 321 | "componentName": "Message", 322 | "componentWillMount": Object { 323 | "averageTimeSpentMs": "0.67", 324 | "numberOfTimes": 1, 325 | "totalTimeSpentMs": 0.67, 326 | }, 327 | "componentWillReceiveProps": Object { 328 | "averageTimeSpentMs": "-", 329 | "numberOfTimes": 0, 330 | "totalTimeSpentMs": 0, 331 | }, 332 | "componentWillUnmount": Object { 333 | "averageTimeSpentMs": "-", 334 | "numberOfTimes": 0, 335 | "totalTimeSpentMs": 0, 336 | }, 337 | "componentWillUpdate": Object { 338 | "averageTimeSpentMs": "-", 339 | "numberOfTimes": 0, 340 | "totalTimeSpentMs": 0, 341 | }, 342 | "mount": Object { 343 | "averageTimeSpentMs": "3.41", 344 | "numberOfTimes": 1, 345 | "totalTimeSpentMs": 3.41, 346 | }, 347 | "numberOfInstances": 1, 348 | "percentTimeSpent": "4%", 349 | "render": Object { 350 | "averageTimeSpentMs": "-", 351 | "numberOfTimes": 0, 352 | "totalTimeSpentMs": 0, 353 | }, 354 | "shouldComponentUpdate": Object { 355 | "averageTimeSpentMs": "-", 356 | "numberOfTimes": 0, 357 | "totalTimeSpentMs": 0, 358 | }, 359 | "totalTimeSpent": 4.63, 360 | "unmount": Object { 361 | "averageTimeSpentMs": "-", 362 | "numberOfTimes": 0, 363 | "totalTimeSpentMs": 0, 364 | }, 365 | "update": Object { 366 | "averageTimeSpentMs": "-", 367 | "numberOfTimes": 0, 368 | "totalTimeSpentMs": 0, 369 | }, 370 | }, 371 | ] 372 | `; 373 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/reactPerformanceData.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`React Performance Data Parse component name and phase name 1`] = ` 4 | Object { 5 | "App": Object { 6 | "componentDidMount": Object { 7 | "timeSpent": Array [ 8 | 0.9200000000000159, 9 | ], 10 | }, 11 | "componentDidUpdate": Object { 12 | "timeSpent": Array [], 13 | }, 14 | "componentWillMount": Object { 15 | "timeSpent": Array [ 16 | 3.0249999999999773, 17 | ], 18 | }, 19 | "componentWillReceiveProps": Object { 20 | "timeSpent": Array [], 21 | }, 22 | "componentWillUnmount": Object { 23 | "timeSpent": Array [], 24 | }, 25 | "componentWillUpdate": Object { 26 | "timeSpent": Array [], 27 | }, 28 | "mount": Object { 29 | "timeSpent": Array [ 30 | 36.875, 31 | ], 32 | }, 33 | "render": Object { 34 | "timeSpent": Array [], 35 | }, 36 | "shouldComponentUpdate": Object { 37 | "timeSpent": Array [], 38 | }, 39 | "unmount": Object { 40 | "timeSpent": Array [], 41 | }, 42 | "update": Object { 43 | "timeSpent": Array [], 44 | }, 45 | }, 46 | "Introduction": Object { 47 | "componentDidMount": Object { 48 | "timeSpent": Array [ 49 | 0.4600000000000364, 50 | ], 51 | }, 52 | "componentDidUpdate": Object { 53 | "timeSpent": Array [ 54 | 0.8100000000000023, 55 | ], 56 | }, 57 | "componentWillMount": Object { 58 | "timeSpent": Array [ 59 | 0.8350000000000364, 60 | ], 61 | }, 62 | "componentWillReceiveProps": Object { 63 | "timeSpent": Array [], 64 | }, 65 | "componentWillUnmount": Object { 66 | "timeSpent": Array [], 67 | }, 68 | "componentWillUpdate": Object { 69 | "timeSpent": Array [], 70 | }, 71 | "mount": Object { 72 | "timeSpent": Array [ 73 | 11.33499999999998, 74 | ], 75 | }, 76 | "render": Object { 77 | "timeSpent": Array [], 78 | }, 79 | "shouldComponentUpdate": Object { 80 | "timeSpent": Array [], 81 | }, 82 | "unmount": Object { 83 | "timeSpent": Array [], 84 | }, 85 | "update": Object { 86 | "timeSpent": Array [ 87 | 2.8349999999999795, 88 | ], 89 | }, 90 | }, 91 | "Main": Object { 92 | "componentDidMount": Object { 93 | "timeSpent": Array [ 94 | 0.8099999999999454, 95 | ], 96 | }, 97 | "componentDidUpdate": Object { 98 | "timeSpent": Array [], 99 | }, 100 | "componentWillMount": Object { 101 | "timeSpent": Array [ 102 | 0.875, 103 | ], 104 | }, 105 | "componentWillReceiveProps": Object { 106 | "timeSpent": Array [], 107 | }, 108 | "componentWillUnmount": Object { 109 | "timeSpent": Array [], 110 | }, 111 | "componentWillUpdate": Object { 112 | "timeSpent": Array [], 113 | }, 114 | "mount": Object { 115 | "timeSpent": Array [ 116 | 31.350000000000023, 117 | ], 118 | }, 119 | "render": Object { 120 | "timeSpent": Array [], 121 | }, 122 | "shouldComponentUpdate": Object { 123 | "timeSpent": Array [], 124 | }, 125 | "unmount": Object { 126 | "timeSpent": Array [], 127 | }, 128 | "update": Object { 129 | "timeSpent": Array [], 130 | }, 131 | }, 132 | "Message": Object { 133 | "componentDidMount": Object { 134 | "timeSpent": Array [ 135 | 0.55499999999995, 136 | ], 137 | }, 138 | "componentDidUpdate": Object { 139 | "timeSpent": Array [], 140 | }, 141 | "componentWillMount": Object { 142 | "timeSpent": Array [ 143 | 0.6700000000000159, 144 | ], 145 | }, 146 | "componentWillReceiveProps": Object { 147 | "timeSpent": Array [], 148 | }, 149 | "componentWillUnmount": Object { 150 | "timeSpent": Array [], 151 | }, 152 | "componentWillUpdate": Object { 153 | "timeSpent": Array [], 154 | }, 155 | "mount": Object { 156 | "timeSpent": Array [ 157 | 3.4149999999999636, 158 | ], 159 | }, 160 | "render": Object { 161 | "timeSpent": Array [], 162 | }, 163 | "shouldComponentUpdate": Object { 164 | "timeSpent": Array [], 165 | }, 166 | "unmount": Object { 167 | "timeSpent": Array [], 168 | }, 169 | "update": Object { 170 | "timeSpent": Array [], 171 | }, 172 | }, 173 | "Posts": Object { 174 | "componentDidMount": Object { 175 | "timeSpent": Array [ 176 | 0.8250000000000455, 177 | ], 178 | }, 179 | "componentDidUpdate": Object { 180 | "timeSpent": Array [], 181 | }, 182 | "componentWillMount": Object { 183 | "timeSpent": Array [ 184 | 1.1050000000000182, 185 | ], 186 | }, 187 | "componentWillReceiveProps": Object { 188 | "timeSpent": Array [], 189 | }, 190 | "componentWillUnmount": Object { 191 | "timeSpent": Array [], 192 | }, 193 | "componentWillUpdate": Object { 194 | "timeSpent": Array [], 195 | }, 196 | "mount": Object { 197 | "timeSpent": Array [ 198 | 4.045000000000016, 199 | ], 200 | }, 201 | "render": Object { 202 | "timeSpent": Array [], 203 | }, 204 | "shouldComponentUpdate": Object { 205 | "timeSpent": Array [], 206 | }, 207 | "unmount": Object { 208 | "timeSpent": Array [], 209 | }, 210 | "update": Object { 211 | "timeSpent": Array [], 212 | }, 213 | }, 214 | "Projects": Object { 215 | "componentDidMount": Object { 216 | "timeSpent": Array [ 217 | 0.07999999999998408, 218 | ], 219 | }, 220 | "componentDidUpdate": Object { 221 | "timeSpent": Array [], 222 | }, 223 | "componentWillMount": Object { 224 | "timeSpent": Array [ 225 | 1.8450000000000273, 226 | ], 227 | }, 228 | "componentWillReceiveProps": Object { 229 | "timeSpent": Array [], 230 | }, 231 | "componentWillUnmount": Object { 232 | "timeSpent": Array [], 233 | }, 234 | "componentWillUpdate": Object { 235 | "timeSpent": Array [], 236 | }, 237 | "mount": Object { 238 | "timeSpent": Array [ 239 | 9.435000000000002, 240 | ], 241 | }, 242 | "render": Object { 243 | "timeSpent": Array [], 244 | }, 245 | "shouldComponentUpdate": Object { 246 | "timeSpent": Array [], 247 | }, 248 | "unmount": Object { 249 | "timeSpent": Array [], 250 | }, 251 | "update": Object { 252 | "timeSpent": Array [ 253 | 3.025000000000034, 254 | ], 255 | }, 256 | }, 257 | } 258 | `; 259 | -------------------------------------------------------------------------------- /__tests__/commitChanges.test.js: -------------------------------------------------------------------------------- 1 | var MEASURES = require('../samples/measures') 2 | 3 | var { getCommitChangesTime } = require('../src/shared/commitChanges') 4 | var { getTotalTime } = require('../src/shared/totalTime') 5 | 6 | describe('Committing changes time duration', () => { 7 | test('Committing changes with and without warning for cascading updates in earlier commits', () => { 8 | expect(getCommitChangesTime(MEASURES)).toMatchSnapshot() 9 | }) 10 | test('Returns total time taken in committing changes', () => { 11 | expect(getTotalTime(getCommitChangesTime(MEASURES))).toMatchSnapshot() 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /__tests__/hostEffects.test.js: -------------------------------------------------------------------------------- 1 | var MEASURES = require('../samples/measures') 2 | 3 | var { 4 | getCommitHostEffectsTime, 5 | getTotalEffects 6 | } = require('../src/shared/hostEffects') 7 | var { getTotalTime } = require('../src/shared/totalTime') 8 | 9 | describe('Committing Host Effects', () => { 10 | test('Sanity', () => { 11 | expect(getCommitHostEffectsTime(MEASURES)).toMatchSnapshot() 12 | }) 13 | 14 | test('Total time taken in committing host effects', () => { 15 | expect(getTotalTime(getCommitHostEffectsTime(MEASURES))).toMatchSnapshot() 16 | }) 17 | 18 | test('Total effects', () => { 19 | expect( 20 | getTotalEffects(getCommitHostEffectsTime(MEASURES)) 21 | ).toMatchSnapshot() 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /__tests__/lifecycle.test.js: -------------------------------------------------------------------------------- 1 | var MEASURES = require('../samples/measures') 2 | 3 | var { getLifecycleTime, getTotalMethods } = require('../src/shared/lifecycle') 4 | var { getTotalTime } = require('../src/shared/totalTime') 5 | 6 | describe('Calling Lifecycle Methods', () => { 7 | test('Sanity', () => { 8 | expect(getLifecycleTime(MEASURES)).toMatchSnapshot() 9 | }) 10 | 11 | test('Total time taken in calling lifecycle methods', () => { 12 | expect(getTotalTime(getLifecycleTime(MEASURES))).toMatchSnapshot() 13 | }) 14 | 15 | test('Total effects', () => { 16 | expect(getTotalMethods(getLifecycleTime(MEASURES))).toMatchSnapshot() 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /__tests__/mapData.test.js: -------------------------------------------------------------------------------- 1 | var { generateDataFromMeasures } = require('../src/shared/generate') 2 | var { getReactPerformanceData } = require('../src/shared/parse') 3 | var MEASURES = require('../samples/measures') 4 | 5 | describe('Map Data from React Performance Data', () => { 6 | test('Returns an object containing information about the component measure', () => { 7 | expect( 8 | generateDataFromMeasures(getReactPerformanceData(MEASURES)) 9 | ).toMatchSnapshot() 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /__tests__/measures.test.js: -------------------------------------------------------------------------------- 1 | var { getComponentAndPhaseName } = require('../src/shared/parseMeasures') 2 | 3 | describe('Measure names', () => { 4 | test('Component name and phase name', () => { 5 | expect( 6 | getComponentAndPhaseName({ 7 | name: '⚛ App [mount]' 8 | }) 9 | ).toEqual({ componentName: 'App', phase: '[mount]' }) 10 | }) 11 | 12 | test('Component name and lifecycle hook without warning for cascading updates', () => { 13 | expect( 14 | getComponentAndPhaseName({ 15 | name: '⚛ App.componentWillMount' 16 | }) 17 | ).toEqual({ componentName: 'App', phase: 'componentWillMount' }) 18 | }) 19 | 20 | test('Component name and lifecycle hook with warning for cascading updates', () => { 21 | expect( 22 | getComponentAndPhaseName({ 23 | name: '⛔ App.componentDidMount Warning: Scheduled a cascading update' 24 | }) 25 | ).toEqual({ componentName: 'App', phase: 'componentDidMount' }) 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /__tests__/reactPerformanceData.test.js: -------------------------------------------------------------------------------- 1 | var { getReactPerformanceData } = require('../src/shared/parse') 2 | var MEASURES = require('../samples/measures') 3 | 4 | describe('React Performance Data', () => { 5 | test('Parse component name and phase name', () => { 6 | expect(getReactPerformanceData(MEASURES)).toMatchSnapshot() 7 | }) 8 | }) 9 | -------------------------------------------------------------------------------- /__tests__/registerObserver.test.js: -------------------------------------------------------------------------------- 1 | var { 2 | registerObserver, 3 | getMeasuresByComponentNames 4 | } = require('../src/npm/hook') 5 | 6 | describe('`registerObserver`', () => { 7 | let observer 8 | let callback 9 | let params 10 | let observerReference 11 | describe('with `window.PerformanceObserver`', () => { 12 | beforeEach(() => { 13 | observerReference = { 14 | observe: jest.fn() 15 | } 16 | window.PerformanceObserver = jest.fn(() => observerReference) 17 | callback = jest.fn() 18 | observer = registerObserver(params, callback) 19 | }) 20 | it('should return `observer`', () => { 21 | expect(observer).toEqual(observerReference) 22 | }) 23 | it('should call `observer.observe`', () => { 24 | expect(observerReference.observe).toHaveBeenCalledTimes(1) 25 | }) 26 | describe('without `params`', () => { 27 | beforeEach(() => { 28 | params = undefined 29 | }) 30 | it('should call `observer.observe` with `entryTypes`', () => { 31 | expect(observerReference.observe).toHaveBeenCalledWith({ 32 | entryTypes: ['measure'] 33 | }) 34 | }) 35 | }) 36 | }) 37 | describe('without `window.PerformanceObserver`', () => { 38 | beforeEach(() => { 39 | window.PerformanceObserver = undefined 40 | callback = jest.fn() 41 | params = undefined 42 | observer = registerObserver(params, callback) 43 | }) 44 | it('should return `undefined`', () => { 45 | expect(observer).toEqual(undefined) 46 | }) 47 | }) 48 | }) 49 | 50 | describe('utils', () => { 51 | let componentNames 52 | let measures 53 | let requiredMeasures 54 | describe('getMeasuresByComponentNames', () => { 55 | describe('with matching `componentNames`', () => { 56 | beforeEach(() => { 57 | componentNames = ['foo'] 58 | measures = [ 59 | { 60 | componentName: 'foo' 61 | } 62 | ] 63 | requiredMeasures = getMeasuresByComponentNames(componentNames, measures) 64 | }) 65 | it('should return a list of required measure', () => { 66 | expect(requiredMeasures).toEqual([ 67 | { 68 | componentName: 'foo' 69 | } 70 | ]) 71 | }) 72 | }) 73 | describe('without matching `componentNames`', () => { 74 | beforeEach(() => { 75 | componentNames = ['foo'] 76 | measures = [ 77 | { 78 | componentName: 'bar' 79 | } 80 | ] 81 | requiredMeasures = getMeasuresByComponentNames(componentNames, measures) 82 | }) 83 | it('should return a list of empty measure', () => { 84 | expect(requiredMeasures).toEqual([]) 85 | }) 86 | }) 87 | }) 88 | }) 89 | -------------------------------------------------------------------------------- /art/Demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitin42/react-perf-devtool/3f6d159c62b68f253ebc74b9bfc2d485c196ed63/art/Demo.gif -------------------------------------------------------------------------------- /art/RPLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitin42/react-perf-devtool/3f6d159c62b68f253ebc74b9bfc2d485c196ed63/art/RPLogo.png -------------------------------------------------------------------------------- /art/Tool.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitin42/react-perf-devtool/3f6d159c62b68f253ebc74b9bfc2d485c196ed63/art/Tool.png -------------------------------------------------------------------------------- /art/tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitin42/react-perf-devtool/3f6d159c62b68f253ebc74b9bfc2d485c196ed63/art/tab.png -------------------------------------------------------------------------------- /extension/dependencies/react-dom.js: -------------------------------------------------------------------------------- 1 | /** @license React v16.2.0 2 | * react-dom.production.min.js 3 | * 4 | * Copyright (c) 2013-present, Facebook, Inc. 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | /* 10 | Modernizr 3.0.0pre (Custom Build) | MIT 11 | */ 12 | 'use strict';(function(na,l){"object"===typeof exports&&"undefined"!==typeof module?module.exports=l(require("react")):"function"===typeof define&&define.amd?define(["react"],l):na.ReactDOM=l(na.React)})(this,function(na){function l(a){for(var b=arguments.length-1,c="Minified React error #"+a+"; visit http://facebook.github.io/react/docs/error-decoder.html?invariant\x3d"+a,d=0;dthis.eventPool.length&&this.eventPool.push(a)}function md(a){a.eventPool=[];a.getPooled=ef;a.release=ff}function nd(a,b,c,d){return n.call(this,a,b,c,d)}function od(a,b,c,d){return n.call(this,a,b,c,d)}function gf(){var a=window.opera;return"object"===typeof a&&"function"===typeof a.version&&12>=parseInt(a.version(),10)}function pd(a,b){switch(a){case "topKeyUp":return-1!==hf.indexOf(b.keyCode);case "topKeyDown":return 229!==b.keyCode;case "topKeyPress":case "topMouseDown":case "topBlur":return!0; 24 | default:return!1}}function qd(a){a=a.detail;return"object"===typeof a&&"data"in a?a.data:null}function jf(a,b){switch(a){case "topCompositionEnd":return qd(b);case "topKeyPress":if(32!==b.which)return null;rd=!0;return sd;case "topTextInput":return a=b.data,a===sd&&rd?null:a;default:return null}}function kf(a,b){if(za)return"topCompositionEnd"===a||!bc&&pd(a,b)?(a=kd(),H._root=null,H._startText=null,H._fallbackText=null,za=!1,a):null;switch(a){case "topPaste":return null;case "topKeyPress":if(!(b.ctrlKey|| 25 | b.altKey||b.metaKey)||b.ctrlKey&&b.altKey){if(b.char&&1qb.length&&qb.push(a)}}}function rb(a,b){var c={};c[a.toLowerCase()]=b.toLowerCase();c["Webkit"+a]="webkit"+b;c["Moz"+a]="moz"+b;c["ms"+a]="MS"+b;c["O"+a]="o"+b.toLowerCase();return c}function sb(a){if(kc[a])return kc[a];if(!U[a])return a;var b=U[a],c;for(c in b)if(b.hasOwnProperty(c)&&c in Jd)return kc[a]=b[c];return""}function Kd(a){Object.prototype.hasOwnProperty.call(a,tb)||(a[tb]=zf++,Ld[a[tb]]= 36 | {});return Ld[a[tb]]}function Md(a,b){return a===b?0!==a||0!==b||1/a===1/b:a!==a&&b!==b}function Nd(a,b){return a&&b?a===b?!0:Od(a)?!1:Od(b)?Nd(a,b.parentNode):"contains"in a?a.contains(b):a.compareDocumentPosition?!!(a.compareDocumentPosition(b)&16):!1:!1}function Pd(a){for(;a&&a.firstChild;)a=a.firstChild;return a}function Qd(a,b){var c=Pd(a);a=0;for(var d;c;){if(3===c.nodeType){d=a+c.textContent.length;if(a<=b&&d>=b)return{node:c,offset:b-a};a=d}a:{for(;c;){if(c.nextSibling){c=c.nextSibling;break a}c= 37 | c.parentNode}c=void 0}c=Pd(c)}}function lc(a){var b=a&&a.nodeName&&a.nodeName.toLowerCase();return b&&("input"===b&&"text"===a.type||"textarea"===b||"true"===a.contentEditable)}function Rd(a,b){if(mc||null==X||X!==nc())return null;var c=X;"selectionStart"in c&&lc(c)?c={start:c.selectionStart,end:c.selectionEnd}:window.getSelection?(c=window.getSelection(),c={anchorNode:c.anchorNode,anchorOffset:c.anchorOffset,focusNode:c.focusNode,focusOffset:c.focusOffset}):c=void 0;return Sa&&oc(Sa,c)?null:(Sa= 38 | c,a=n.getPooled(Sd.select,pc,a,b),a.type="select",a.target=X,ya(a),a)}function Td(a,b,c,d){return n.call(this,a,b,c,d)}function Ud(a,b,c,d){return n.call(this,a,b,c,d)}function Vd(a,b,c,d){return n.call(this,a,b,c,d)}function ub(a){var b=a.keyCode;"charCode"in a?(a=a.charCode,0===a&&13===b&&(a=13)):a=b;return 32<=a||13===a?a:0}function Wd(a,b,c,d){return n.call(this,a,b,c,d)}function Xd(a,b,c,d){return n.call(this,a,b,c,d)}function Yd(a,b,c,d){return n.call(this,a,b,c,d)}function Zd(a,b,c,d){return n.call(this, 39 | a,b,c,d)}function $d(a,b,c,d){return n.call(this,a,b,c,d)}function I(a,b){0>ra||(a.current=vb[ra],vb[ra]=null,ra--)}function M(a,b,c){ra++;vb[ra]=a.current;a.current=b}function Ta(a){return Ua(a)?wb:ia.current}function Va(a,b){var c=a.type.contextTypes;if(!c)return ja;var d=a.stateNode;if(d&&d.__reactInternalMemoizedUnmaskedChildContext===b)return d.__reactInternalMemoizedMaskedChildContext;var e={},f;for(f in c)e[f]=b[f];d&&(a=a.stateNode,a.__reactInternalMemoizedUnmaskedChildContext=b,a.__reactInternalMemoizedMaskedChildContext= 40 | e);return e}function Ua(a){return 2===a.tag&&null!=a.type.childContextTypes}function ae(a){Ua(a)&&(I(J,a),I(ia,a))}function be(a,b,c){null!=ia.cursor?l("168"):void 0;M(ia,b,a);M(J,c,a)}function ce(a,b){var c=a.stateNode,d=a.type.childContextTypes;if("function"!==typeof c.getChildContext)return b;c=c.getChildContext();for(var e in c)e in d?void 0:l("108",Pa(a)||"Unknown",e);return C({},b,c)}function xb(a){if(!Ua(a))return!1;var b=a.stateNode;b=b&&b.__reactInternalMemoizedMergedChildContext||ja;wb= 41 | ia.current;M(ia,b,a);M(J,J.current,a);return!0}function de(a,b){var c=a.stateNode;c?void 0:l("169");if(b){var d=ce(a,wb);c.__reactInternalMemoizedMergedChildContext=d;I(J,a);I(ia,a);M(ia,d,a)}else I(J,a);M(J,b,a)}function Q(a,b,c){this.tag=a;this.key=b;this.stateNode=this.type=null;this.sibling=this.child=this["return"]=null;this.index=0;this.memoizedState=this.updateQueue=this.memoizedProps=this.pendingProps=this.ref=null;this.internalContextTag=c;this.effectTag=0;this.lastEffect=this.firstEffect= 42 | this.nextEffect=null;this.expirationTime=0;this.alternate=null}function yb(a,b,c){var d=a.alternate;null===d?(d=new Q(a.tag,a.key,a.internalContextTag),d.type=a.type,d.stateNode=a.stateNode,d.alternate=a,a.alternate=d):(d.effectTag=0,d.nextEffect=null,d.firstEffect=null,d.lastEffect=null);d.expirationTime=c;d.pendingProps=b;d.child=a.child;d.memoizedProps=a.memoizedProps;d.memoizedState=a.memoizedState;d.updateQueue=a.updateQueue;d.sibling=a.sibling;d.index=a.index;d.ref=a.ref;return d}function qc(a, 43 | b,c){var d=void 0,e=a.type,f=a.key;"function"===typeof e?(d=e.prototype&&e.prototype.isReactComponent?new Q(2,f,b):new Q(0,f,b),d.type=e,d.pendingProps=a.props):"string"===typeof e?(d=new Q(5,f,b),d.type=e,d.pendingProps=a.props):"object"===typeof e&&null!==e&&"number"===typeof e.tag?(d=e,d.pendingProps=a.props):l("130",null==e?e:typeof e,"");d.expirationTime=c;return d}function zb(a,b,c,d){b=new Q(10,d,b);b.pendingProps=a;b.expirationTime=c;return b}function rc(a,b,c){b=new Q(6,null,b);b.pendingProps= 44 | a;b.expirationTime=c;return b}function sc(a,b,c){b=new Q(7,a.key,b);b.type=a.handler;b.pendingProps=a;b.expirationTime=c;return b}function tc(a,b,c){a=new Q(9,null,b);a.expirationTime=c;return a}function uc(a,b,c){b=new Q(4,a.key,b);b.pendingProps=a.children||[];b.expirationTime=c;b.stateNode={containerInfo:a.containerInfo,pendingChildren:null,implementation:a.implementation};return b}function ee(a){return function(b){try{return a(b)}catch(c){}}}function Af(a){if("undefined"===typeof __REACT_DEVTOOLS_GLOBAL_HOOK__)return!1; 45 | var b=__REACT_DEVTOOLS_GLOBAL_HOOK__;if(b.isDisabled||!b.supportsFiber)return!0;try{var c=b.inject(a);vc=ee(function(a){return b.onCommitFiberRoot(c,a)});wc=ee(function(a){return b.onCommitFiberUnmount(c,a)})}catch(d){}return!0}function fe(a){"function"===typeof vc&&vc(a)}function ge(a){"function"===typeof wc&&wc(a)}function he(a){return{baseState:a,expirationTime:0,first:null,last:null,callbackList:null,hasForceUpdate:!1,isInitialized:!1}}function Ab(a,b){null===a.last?a.first=a.last=b:(a.last.next= 46 | b,a.last=b);if(0===a.expirationTime||a.expirationTime>b.expirationTime)a.expirationTime=b.expirationTime}function Bb(a,b){var c=a.alternate,d=a.updateQueue;null===d&&(d=a.updateQueue=he(null));null!==c?(a=c.updateQueue,null===a&&(a=c.updateQueue=he(null))):a=null;a=a!==d?a:null;null===a?Ab(d,b):null===d.last||null===a.last?(Ab(d,b),Ab(a,b)):(Ab(d,b),a.last=b)}function ie(a,b,c,d){a=a.partialState;return"function"===typeof a?a.call(b,c,d):a}function xc(a,b,c,d,e,f){null!==a&&a.updateQueue===c&&(c= 47 | b.updateQueue={baseState:c.baseState,expirationTime:c.expirationTime,first:c.first,last:c.last,isInitialized:c.isInitialized,callbackList:null,hasForceUpdate:!1});c.expirationTime=0;c.isInitialized?a=c.baseState:(a=c.baseState=b.memoizedState,c.isInitialized=!0);for(var g=!0,h=c.first,k=!1;null!==h;){var l=h.expirationTime;if(l>f){var D=c.expirationTime;if(0===D||D>l)c.expirationTime=l;k||(k=!0,c.baseState=a)}else{k||(c.first=h.next,null===c.first&&(c.last=null));if(h.isReplace)a=ie(h,d,a,e),g=!0; 48 | else if(l=ie(h,d,a,e))a=g?C({},a,l):C(a,l),g=!1;h.isForced&&(c.hasForceUpdate=!0);null!==h.callback&&(l=c.callbackList,null===l&&(l=c.callbackList=[]),l.push(h))}h=h.next}null!==c.callbackList?b.effectTag|=32:null!==c.first||c.hasForceUpdate||(b.updateQueue=null);k||(c.baseState=a);return a}function je(a,b){var c=a.callbackList;if(null!==c)for(a.callbackList=null,a=0;ax?(k=p,p=null):k=p.sibling;var l=L(e,p,h[x],z);if(null===l){null===p&&(p=k);break}a&&p&&null===l.alternate&& 57 | b(e,p);g=f(l,g,x);null===q?t=l:q.sibling=l;q=l;p=k}if(x===h.length)return c(e,p),t;if(null===p){for(;xx?(k=p,p=null):k=p.sibling;var La=L(e,p,m.value,z);if(null===La){p||(p=k);break}a&&p&&null===La.alternate&&b(e,p);g=f(La,g,x);null===q?t=La:q.sibling=La;q=La;p=k}if(m.done)return c(e,p),t;if(null===p){for(;!m.done;x++,m=h.next())m=K(e,m.value,z),null!==m&&(g=f(m,g,x),null===q?t=m:q.sibling=m,q=m);return t}for(p=d(e,p);!m.done;x++,m=h.next())if(m=R(p,e,x,m.value,z),null!==m){if(a&&null!==m.alternate)p["delete"](null===m.key?x:m.key); 59 | g=f(m,g,x);null===q?t=m:q.sibling=m;q=m}a&&p.forEach(function(a){return b(e,a)});return t}return function(a,d,f,h){"object"===typeof f&&null!==f&&f.type===sa&&null===f.key&&(f=f.props.children);var k="object"===typeof f&&null!==f;if(k)switch(f.$$typeof){case Db:a:{var q=f.key;for(k=d;null!==k;){if(k.key===q)if(10===k.tag?f.type===sa:k.type===f.type){c(a,k.sibling);d=e(k,f.type===sa?f.props.children:f.props,h);d.ref=Xa(k,f);d["return"]=a;a=d;break a}else{c(a,k);break}else b(a,k);k=k.sibling}f.type=== 60 | sa?(d=zb(f.props.children,a.internalContextTag,h,f.key),d["return"]=a,a=d):(h=qc(f,a.internalContextTag,h),h.ref=Xa(d,f),h["return"]=a,a=h)}return g(a);case Eb:a:{for(k=f.key;null!==d;){if(d.key===k)if(7===d.tag){c(a,d.sibling);d=e(d,f,h);d["return"]=a;a=d;break a}else{c(a,d);break}else b(a,d);d=d.sibling}d=sc(f,a.internalContextTag,h);d["return"]=a;a=d}return g(a);case Fb:a:{if(null!==d)if(9===d.tag){c(a,d.sibling);d=e(d,null,h);d.type=f.value;d["return"]=a;a=d;break a}else c(a,d);d=tc(f,a.internalContextTag, 61 | h);d.type=f.value;d["return"]=a;a=d}return g(a);case Ya:a:{for(k=f.key;null!==d;){if(d.key===k)if(4===d.tag&&d.stateNode.containerInfo===f.containerInfo&&d.stateNode.implementation===f.implementation){c(a,d.sibling);d=e(d,f.children||[],h);d["return"]=a;a=d;break a}else{c(a,d);break}else b(a,d);d=d.sibling}d=uc(f,a.internalContextTag,h);d["return"]=a;a=d}return g(a)}if("string"===typeof f||"number"===typeof f)return f=""+f,null!==d&&6===d.tag?(c(a,d.sibling),d=e(d,f,h)):(c(a,d),d=rc(f,a.internalContextTag, 62 | h)),d["return"]=a,a=d,g(a);if(Gb(f))return n(a,d,f,h);if(Wa(f))return r(a,d,f,h);k&&Cb(a,f);if("undefined"===typeof f)switch(a.tag){case 2:case 1:h=a.type,l("152",h.displayName||h.name||"Component")}return c(a,d)}}function Bf(a,b,c){var d=3c||d.hasOverloadedBooleanValue&&!1===c?oe(a,b):d.mustUseProperty?a[d.propertyName]=c:(b=d.attributeName,(e=d.attributeNamespace)?a.setAttributeNS(e,b,""+c):d.hasBooleanValue||d.hasOverloadedBooleanValue&&!0===c?a.setAttribute(b,""):a.setAttribute(b,""+c))}else Ac(a,b,Xc(b,c)?c:null)}function Ac(a,b,c){Cf(b)&&(null==c?a.removeAttribute(b): 64 | a.setAttribute(b,""+c))}function oe(a,b){var c=Ub(b);c?(b=c.mutationMethod)?b(a,void 0):c.mustUseProperty?a[c.propertyName]=c.hasBooleanValue?!1:"":a.removeAttribute(c.attributeName):a.removeAttribute(b)}function Bc(a,b){var c=b.value,d=b.checked;return C({type:void 0,step:void 0,min:void 0,max:void 0},b,{defaultChecked:void 0,defaultValue:void 0,value:null!=c?c:a._wrapperState.initialValue,checked:null!=d?d:a._wrapperState.initialChecked})}function pe(a,b){var c=b.defaultValue;a._wrapperState={initialChecked:null!= 65 | b.checked?b.checked:b.defaultChecked,initialValue:null!=b.value?b.value:c,controlled:"checkbox"===b.type||"radio"===b.type?null!=b.checked:null!=b.value}}function qe(a,b){b=b.checked;null!=b&&zc(a,"checked",b)}function Cc(a,b){qe(a,b);var c=b.value;if(null!=c)if(0===c&&""===a.value)a.value="0";else if("number"===b.type){if(b=parseFloat(a.value)||0,c!=b||c==b&&a.value!=c)a.value=""+c}else a.value!==""+c&&(a.value=""+c);else null==b.value&&null!=b.defaultValue&&a.defaultValue!==""+b.defaultValue&&(a.defaultValue= 66 | ""+b.defaultValue),null==b.checked&&null!=b.defaultChecked&&(a.defaultChecked=!!b.defaultChecked)}function re(a,b){switch(b.type){case "submit":case "reset":break;case "color":case "date":case "datetime":case "datetime-local":case "month":case "time":case "week":a.value="";a.value=a.defaultValue;break;default:a.value=a.value}b=a.name;""!==b&&(a.name="");a.defaultChecked=!a.defaultChecked;a.defaultChecked=!a.defaultChecked;""!==b&&(a.name=b)}function Ef(a){var b="";na.Children.forEach(a,function(a){null== 67 | a||"string"!==typeof a&&"number"!==typeof a||(b+=a)});return b}function Dc(a,b){a=C({children:void 0},b);if(b=Ef(b.children))a.children=b;return a}function ka(a,b,c,d){a=a.options;if(b){b={};for(var e=0;e=b.length?void 0:l("93"),b=b[0]),c=""+b),null==c&&(c=""));a._wrapperState={initialValue:""+ 69 | c}}function ue(a,b){var c=b.value;null!=c&&(c=""+c,c!==a.value&&(a.value=c),null==b.defaultValue&&(a.defaultValue=c));null!=b.defaultValue&&(a.defaultValue=b.defaultValue)}function ve(a){switch(a){case "svg":return"http://www.w3.org/2000/svg";case "math":return"http://www.w3.org/1998/Math/MathML";default:return"http://www.w3.org/1999/xhtml"}}function Fc(a,b){return null==a||"http://www.w3.org/1999/xhtml"===a?ve(b):"http://www.w3.org/2000/svg"===a&&"foreignObject"===b?"http://www.w3.org/1999/xhtml": 70 | a}function we(a,b,c){a=a.style;for(var d in b)if(b.hasOwnProperty(d)){c=0===d.indexOf("--");var e=d;var f=b[d];e=null==f||"boolean"===typeof f||""===f?"":c||"number"!==typeof f||0===f||Za.hasOwnProperty(e)&&Za[e]?(""+f).trim():f+"px";"float"===d&&(d="cssFloat");c?a.setProperty(d,e):a[d]=e}}function Gc(a,b,c){b&&(Ff[a]&&(null!=b.children||null!=b.dangerouslySetInnerHTML?l("137",a,c()):void 0),null!=b.dangerouslySetInnerHTML&&(null!=b.children?l("60"):void 0,"object"===typeof b.dangerouslySetInnerHTML&& 71 | "__html"in b.dangerouslySetInnerHTML?void 0:l("61")),null!=b.style&&"object"!==typeof b.style?l("62",c()):void 0)}function Hc(a,b){if(-1===a.indexOf("-"))return"string"===typeof b.is;switch(a){case "annotation-xml":case "color-profile":case "font-face":case "font-face-src":case "font-face-uri":case "font-face-format":case "font-face-name":case "missing-glyph":return!1;default:return!0}}function Y(a,b){a=9===a.nodeType||11===a.nodeType?a:a.ownerDocument;var c=Kd(a);b=kb[b];for(var d=0;d=g.hasBooleanValue+g.hasNumericValue+g.hasOverloadedBooleanValue?void 0:l("50",f);e.hasOwnProperty(f)&&(g.attributeName=e[f]);d.hasOwnProperty(f)&&(g.attributeNamespace=d[f]);a.hasOwnProperty(f)&&(g.mutationMethod=a[f]);ib[f]=g}}},ib={},aa=Ie,Ib=aa.MUST_USE_PROPERTY,w=aa.HAS_BOOLEAN_VALUE,Je=aa.HAS_NUMERIC_VALUE,Jb=aa.HAS_POSITIVE_NUMERIC_VALUE,Ke=aa.HAS_OVERLOADED_BOOLEAN_VALUE, 87 | Kb=aa.HAS_STRING_BOOLEAN_VALUE,Hf={Properties:{allowFullScreen:w,async:w,autoFocus:w,autoPlay:w,capture:Ke,checked:Ib|w,cols:Jb,contentEditable:Kb,controls:w,"default":w,defer:w,disabled:w,download:Ke,draggable:Kb,formNoValidate:w,hidden:w,loop:w,multiple:Ib|w,muted:Ib|w,noValidate:w,open:w,playsInline:w,readOnly:w,required:w,reversed:w,rows:Jb,rowSpan:Je,scoped:w,seamless:w,selected:Ib|w,size:Jb,start:Je,span:Jb,spellCheck:Kb,style:0,tabIndex:0,itemScope:w,acceptCharset:0,className:0,htmlFor:0,httpEquiv:0, 88 | value:Kb},DOMAttributeNames:{acceptCharset:"accept-charset",className:"class",htmlFor:"for",httpEquiv:"http-equiv"},DOMMutationMethods:{value:function(a,b){if(null==b)return a.removeAttribute("value");"number"!==a.type||!1===a.hasAttribute("value")?a.setAttribute("value",""+b):a.validity&&!a.validity.badInput&&a.ownerDocument.activeElement!==a&&a.setAttribute("value",""+b)}}},Kc=aa.HAS_STRING_BOOLEAN_VALUE,Lc={Properties:{autoReverse:Kc,externalResourcesRequired:Kc,preserveAlpha:Kc},DOMAttributeNames:{autoReverse:"autoReverse", 89 | externalResourcesRequired:"externalResourcesRequired",preserveAlpha:"preserveAlpha"},DOMAttributeNamespaces:{xlinkActuate:"http://www.w3.org/1999/xlink",xlinkArcrole:"http://www.w3.org/1999/xlink",xlinkHref:"http://www.w3.org/1999/xlink",xlinkRole:"http://www.w3.org/1999/xlink",xlinkShow:"http://www.w3.org/1999/xlink",xlinkTitle:"http://www.w3.org/1999/xlink",xlinkType:"http://www.w3.org/1999/xlink",xmlBase:"http://www.w3.org/XML/1998/namespace",xmlLang:"http://www.w3.org/XML/1998/namespace",xmlSpace:"http://www.w3.org/XML/1998/namespace"}}, 90 | If=/[\-\:]([a-z])/g,Jf=function(a){return a[1].toUpperCase()};"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode x-height xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xmlns:xlink xml:lang xml:space".split(" ").forEach(function(a){var b= 91 | a.replace(If,Jf);Lc.Properties[b]=0;Lc.DOMAttributeNames[b]=a});aa.injectDOMPropertyConfig(Hf);aa.injectDOMPropertyConfig(Lc);var y={_caughtError:null,_hasCaughtError:!1,_rethrowError:null,_hasRethrowError:!1,injection:{injectErrorUtils:function(a){"function"!==typeof a.invokeGuardedCallback?l("197"):void 0;Le=a.invokeGuardedCallback}},invokeGuardedCallback:function(a,b,c,d,e,f,g,h,k){Le.apply(y,arguments)},invokeGuardedCallbackAndCatchFirstError:function(a,b,c,d,e,f,g,h,k){y.invokeGuardedCallback.apply(this, 92 | arguments);if(y.hasCaughtError()){var l=y.clearCaughtError();y._hasRethrowError||(y._hasRethrowError=!0,y._rethrowError=l)}},rethrowCaughtError:function(){return Kf.apply(y,arguments)},hasCaughtError:function(){return y._hasCaughtError},clearCaughtError:function(){if(y._hasCaughtError){var a=y._caughtError;y._caughtError=null;y._hasCaughtError=!1;return a}l("198")}},Le=function(a,b,c,d,e,f,g,h,k){y._hasCaughtError=!1;y._caughtError=null;var l=Array.prototype.slice.call(arguments,3);try{b.apply(c, 93 | l)}catch(D){y._caughtError=D,y._hasCaughtError=!0}},Kf=function(){if(y._hasRethrowError){var a=y._rethrowError;y._rethrowError=null;y._hasRethrowError=!1;throw a;}},jb=null,ba={},oa=[],Vb={},ca={},kb={},Lf=Object.freeze({plugins:oa,eventNameDispatchConfigs:Vb,registrationNameModules:ca,registrationNameDependencies:kb,possibleRegistrationNames:null,injectEventPluginOrder:ad,injectEventPluginsByName:bd}),ta=function(){};ta.thatReturns=lb;ta.thatReturnsFalse=lb(!1);ta.thatReturnsTrue=lb(!0);ta.thatReturnsNull= 94 | lb(null);ta.thatReturnsThis=function(){return this};ta.thatReturnsArgument=function(a){return a};var G=ta,Xb=null,vd=null,dd=null,pa=null,Me=function(a,b){if(a){var c=a._dispatchListeners,d=a._dispatchInstances;if(Array.isArray(c))for(var e=0;e=ab),sd=String.fromCharCode(32), 100 | V={beforeInput:{phasedRegistrationNames:{bubbled:"onBeforeInput",captured:"onBeforeInputCapture"},dependencies:["topCompositionEnd","topKeyPress","topTextInput","topPaste"]},compositionEnd:{phasedRegistrationNames:{bubbled:"onCompositionEnd",captured:"onCompositionEndCapture"},dependencies:"topBlur topCompositionEnd topKeyDown topKeyPress topKeyUp topMouseDown".split(" ")},compositionStart:{phasedRegistrationNames:{bubbled:"onCompositionStart",captured:"onCompositionStartCapture"},dependencies:"topBlur topCompositionStart topKeyDown topKeyPress topKeyUp topMouseDown".split(" ")}, 101 | compositionUpdate:{phasedRegistrationNames:{bubbled:"onCompositionUpdate",captured:"onCompositionUpdateCapture"},dependencies:"topBlur topCompositionUpdate topKeyDown topKeyPress topKeyUp topMouseDown".split(" ")}},rd=!1,za=!1,Qf={eventTypes:V,extractEvents:function(a,b,c,d){var e;if(bc)b:{switch(a){case "topCompositionStart":var f=V.compositionStart;break b;case "topCompositionEnd":f=V.compositionEnd;break b;case "topCompositionUpdate":f=V.compositionUpdate;break b}f=void 0}else za?pd(a,c)&&(f=V.compositionEnd): 102 | "topKeyDown"===a&&229===c.keyCode&&(f=V.compositionStart);f?(td&&(za||f!==V.compositionStart?f===V.compositionEnd&&za&&(e=kd()):(H._root=d,H._startText=ld(),za=!0)),f=nd.getPooled(f,b,c,d),e?f.data=e:(e=qd(c),null!==e&&(f.data=e)),ya(f),e=f):e=null;(a=Pf?jf(a,c):kf(a,c))?(b=od.getPooled(V.beforeInput,b,c,d),b.data=a,ya(b)):b=null;return[e,b]}},mb=null,Ga=null,fa=null,Qe={injectFiberControlledHostComponent:function(a){mb=a}},Rf=Object.freeze({injection:Qe,enqueueStateRestore:wd,restoreStateIfNeeded:xd}), 103 | ec=function(a,b){return a(b)},dc=!1,lf={color:!0,date:!0,datetime:!0,"datetime-local":!0,email:!0,month:!0,number:!0,password:!0,range:!0,search:!0,tel:!0,text:!0,time:!0,url:!0,week:!0},zd;P.canUseDOM&&(zd=document.implementation&&document.implementation.hasFeature&&!0!==document.implementation.hasFeature("",""));var Dd={change:{phasedRegistrationNames:{bubbled:"onChange",captured:"onChangeCapture"},dependencies:"topBlur topChange topClick topFocus topInput topKeyDown topKeyUp topSelectionChange".split(" ")}}, 104 | Ha=null,Oa=null,Nc=!1;P.canUseDOM&&(Nc=gc("input")&&(!document.documentMode||9=document.documentMode,Sd={select:{phasedRegistrationNames:{bubbled:"onSelect",captured:"onSelectCapture"},dependencies:"topBlur topContextMenu topFocus topKeyDown topKeyUp topMouseDown topMouseUp topSelectionChange".split(" ")}}, 115 | X=null,pc=null,Sa=null,mc=!1,Xf={eventTypes:Sd,extractEvents:function(a,b,c,d){var e=d.window===d?d.document:9===d.nodeType?d:d.ownerDocument,f;if(!(f=!e)){a:{e=Kd(e);f=kb.onSelect;for(var g=0;gc)return D(a,b);switch(b.tag){case 0:null!== 133 | a?l("155"):void 0;var d=b.type,e=b.pendingProps,q=Ta(b);q=Va(b,q);d=d(e,q);b.effectTag|=1;"object"===typeof d&&null!==d&&"function"===typeof d.render?(b.tag=2,e=xb(b),x(b,d),z(b,c),b=h(a,b,!0,e)):(b.tag=1,f(a,b,d),b.memoizedProps=e,b=b.child);return b;case 1:a:{e=b.type;c=b.pendingProps;d=b.memoizedProps;if(J.current)null===c&&(c=d);else if(null===c||d===c){b=m(a,b);break a}d=Ta(b);d=Va(b,d);e=e(c,d);b.effectTag|=1;f(a,b,e);b.memoizedProps=c;b=b.child}return b;case 2:return e=xb(b),d=void 0,null=== 134 | a?b.stateNode?l("153"):(t(b,b.pendingProps),z(b,c),d=!0):d=yc(a,b,c),h(a,b,d,e);case 3:return k(b),e=b.updateQueue,null!==e?(d=b.memoizedState,e=xc(a,b,e,null,null,c),d===e?(w(),b=m(a,b)):(d=e.element,q=b.stateNode,(null===a||null===a.child)&&q.hydrate&&r(b)?(b.effectTag|=2,b.child=Mb(b,null,d,c)):(w(),f(a,b,d)),b.memoizedState=e,b=b.child)):(w(),b=m(a,b)),b;case 5:L(b);null===a&&y(b);e=b.type;var p=b.memoizedProps;d=b.pendingProps;null===d&&(d=p,null===d?l("154"):void 0);q=null!==a?a.memoizedProps: 135 | null;J.current||null!==d&&p!==d?(p=d.children,A(e,d)?p=null:q&&A(e,q)&&(b.effectTag|=16),g(a,b),2147483647!==c&&!v&&n(e,d)?(b.expirationTime=2147483647,b=null):(f(a,b,p),b.memoizedProps=d,b=b.child)):b=m(a,b);return b;case 6:return null===a&&y(b),a=b.pendingProps,null===a&&(a=b.memoizedProps),b.memoizedProps=a,null;case 8:b.tag=7;case 7:e=b.pendingProps;if(J.current)null===e&&(e=a&&a.memoizedProps,null===e?l("154"):void 0);else if(null===e||b.memoizedProps===e)e=b.memoizedProps;d=e.children;b.stateNode= 136 | null===a?Mb(b,b.stateNode,d,c):db(b,b.stateNode,d,c);b.memoizedProps=e;return b.stateNode;case 9:return null;case 4:a:{R(b,b.stateNode.containerInfo);e=b.pendingProps;if(J.current)null===e&&(e=a&&a.memoizedProps,null==e?l("154"):void 0);else if(null===e||b.memoizedProps===e){b=m(a,b);break a}null===a?b.child=db(b,null,e,c):f(a,b,e);b.memoizedProps=e;b=b.child}return b;case 10:a:{c=b.pendingProps;if(J.current)null===c&&(c=b.memoizedProps);else if(null===c||b.memoizedProps===c){b=m(a,b);break a}f(a, 137 | b,c);b.memoizedProps=c;b=b.child}return b;default:l("156")}},beginFailedWork:function(a,b,c){switch(b.tag){case 2:xb(b);break;case 3:k(b);break;default:l("157")}b.effectTag|=64;null===a?b.child=null:b.child!==a.child&&(b.child=a.child);if(0===b.expirationTime||b.expirationTime>c)return D(a,b);b.firstEffect=null;b.lastEffect=null;b.child=null===a?Mb(b,null,null,c):db(b,a.child,null,c);2===b.tag&&(a=b.stateNode,b.memoizedProps=a.props,b.memoizedState=a.state);return b.child}}},cg=function(a,b,c){function d(a){a.effectTag|= 138 | 4}var e=a.createInstance,f=a.createTextInstance,g=a.appendInitialChild,h=a.finalizeInitialChildren,k=a.prepareUpdate,m=a.persistence,D=b.getRootHostContainer,A=b.popHostContext,v=b.getHostContext,n=b.popHostContainer,L=c.prepareToHydrateHostInstance,R=c.prepareToHydrateHostTextInstance,r=c.popHydrationState,w=void 0,y=void 0,x=void 0;a.mutation?(w=function(a){},y=function(a,b,c,e,f,g,h){(b.updateQueue=c)&&d(b)},x=function(a,b,c,e){c!==e&&d(b)}):m?l("235"):l("236");return{completeWork:function(a,b, 139 | c){var t=b.pendingProps;if(null===t)t=b.memoizedProps;else if(2147483647!==b.expirationTime||2147483647===c)b.pendingProps=null;switch(b.tag){case 1:return null;case 2:return ae(b),null;case 3:n(b);I(J,b);I(ia,b);t=b.stateNode;t.pendingContext&&(t.context=t.pendingContext,t.pendingContext=null);if(null===a||null===a.child)r(b),b.effectTag&=-3;w(b);return null;case 5:A(b);c=D();var z=b.type;if(null!==a&&null!=b.stateNode){var m=a.memoizedProps,K=b.stateNode,yc=v();K=k(K,z,m,t,c,yc);y(a,b,K,z,m,t,c); 140 | a.ref!==b.ref&&(b.effectTag|=128)}else{if(!t)return null===b.stateNode?l("166"):void 0,null;a=v();if(r(b))L(b,c,a)&&d(b);else{a=e(z,t,c,a,b);a:for(m=b.child;null!==m;){if(5===m.tag||6===m.tag)g(a,m.stateNode);else if(4!==m.tag&&null!==m.child){m.child["return"]=m;m=m.child;continue}if(m===b)break;for(;null===m.sibling;){if(null===m["return"]||m["return"]===b)break a;m=m["return"]}m.sibling["return"]=m["return"];m=m.sibling}h(a,z,t,c)&&d(b);b.stateNode=a}null!==b.ref&&(b.effectTag|=128)}return null; 141 | case 6:if(a&&null!=b.stateNode)x(a,b,a.memoizedProps,t);else{if("string"!==typeof t)return null===b.stateNode?l("166"):void 0,null;a=D();c=v();r(b)?R(b)&&d(b):b.stateNode=f(t,a,c,b)}return null;case 7:(t=b.memoizedProps)?void 0:l("165");b.tag=8;z=[];a:for((m=b.stateNode)&&(m["return"]=b);null!==m;){if(5===m.tag||6===m.tag||4===m.tag)l("247");else if(9===m.tag)z.push(m.type);else if(null!==m.child){m.child["return"]=m;m=m.child;continue}for(;null===m.sibling;){if(null===m["return"]||m["return"]=== 142 | b)break a;m=m["return"]}m.sibling["return"]=m["return"];m=m.sibling}m=t.handler;t=m(t.props,z);b.child=db(b,null!==a?a.child:null,t,c);return b.child;case 8:return b.tag=7,null;case 9:return null;case 10:return null;case 4:return n(b),w(b),null;case 0:l("167");default:l("156")}}}},dg=function(a,b){function c(a){var c=a.ref;if(null!==c)try{c(null)}catch(z){b(a,z)}}function d(a){"function"===typeof ge&&ge(a);switch(a.tag){case 2:c(a);var d=a.stateNode;if("function"===typeof d.componentWillUnmount)try{d.props= 143 | a.memoizedProps,d.state=a.memoizedState,d.componentWillUnmount()}catch(z){b(a,z)}break;case 5:c(a);break;case 7:e(a.stateNode);break;case 4:k&&g(a)}}function e(a){for(var b=a;;)if(d(b),null===b.child||k&&4===b.tag){if(b===a)break;for(;null===b.sibling;){if(null===b["return"]||b["return"]===a)return;b=b["return"]}b.sibling["return"]=b["return"];b=b.sibling}else b.child["return"]=b,b=b.child}function f(a){return 5===a.tag||3===a.tag||4===a.tag}function g(a){for(var b=a,c=!1,f=void 0,g=void 0;;){if(!c){c= 144 | b["return"];a:for(;;){null===c?l("160"):void 0;switch(c.tag){case 5:f=c.stateNode;g=!1;break a;case 3:f=c.stateNode.containerInfo;g=!0;break a;case 4:f=c.stateNode.containerInfo;g=!0;break a}c=c["return"]}c=!0}if(5===b.tag||6===b.tag)e(b),g?y(f,b.stateNode):w(f,b.stateNode);else if(4===b.tag?f=b.stateNode.containerInfo:d(b),null!==b.child){b.child["return"]=b;b=b.child;continue}if(b===a)break;for(;null===b.sibling;){if(null===b["return"]||b["return"]===a)return;b=b["return"];4===b.tag&&(c=!1)}b.sibling["return"]= 145 | b["return"];b=b.sibling}}var h=a.getPublicInstance,k=a.mutation;a=a.persistence;k||(a?l("235"):l("236"));var m=k.commitMount,D=k.commitUpdate,A=k.resetTextContent,v=k.commitTextUpdate,n=k.appendChild,L=k.appendChildToContainer,R=k.insertBefore,r=k.insertInContainerBefore,w=k.removeChild,y=k.removeChildFromContainer;return{commitResetTextContent:function(a){A(a.stateNode)},commitPlacement:function(a){a:{for(var b=a["return"];null!==b;){if(f(b)){var c=b;break a}b=b["return"]}l("160");c=void 0}var d= 146 | b=void 0;switch(c.tag){case 5:b=c.stateNode;d=!1;break;case 3:b=c.stateNode.containerInfo;d=!0;break;case 4:b=c.stateNode.containerInfo;d=!0;break;default:l("161")}c.effectTag&16&&(A(b),c.effectTag&=-17);a:b:for(c=a;;){for(;null===c.sibling;){if(null===c["return"]||f(c["return"])){c=null;break a}c=c["return"]}c.sibling["return"]=c["return"];for(c=c.sibling;5!==c.tag&&6!==c.tag;){if(c.effectTag&2)continue b;if(null===c.child||4===c.tag)continue b;else c.child["return"]=c,c=c.child}if(!(c.effectTag& 147 | 2)){c=c.stateNode;break a}}for(var e=a;;){if(5===e.tag||6===e.tag)c?d?r(b,e.stateNode,c):R(b,e.stateNode,c):d?L(b,e.stateNode):n(b,e.stateNode);else if(4!==e.tag&&null!==e.child){e.child["return"]=e;e=e.child;continue}if(e===a)break;for(;null===e.sibling;){if(null===e["return"]||e["return"]===a)return;e=e["return"]}e.sibling["return"]=e["return"];e=e.sibling}},commitDeletion:function(a){g(a);a["return"]=null;a.child=null;a.alternate&&(a.alternate.child=null,a.alternate["return"]=null)},commitWork:function(a, 148 | b){switch(b.tag){case 2:break;case 5:var c=b.stateNode;if(null!=c){var d=b.memoizedProps;a=null!==a?a.memoizedProps:d;var e=b.type,f=b.updateQueue;b.updateQueue=null;null!==f&&D(c,f,e,a,d,b)}break;case 6:null===b.stateNode?l("162"):void 0;c=b.memoizedProps;v(b.stateNode,null!==a?a.memoizedProps:c,c);break;case 3:break;default:l("163")}},commitLifeCycles:function(a,b){switch(b.tag){case 2:var c=b.stateNode;if(b.effectTag&4)if(null===a)c.props=b.memoizedProps,c.state=b.memoizedState,c.componentDidMount(); 149 | else{var d=a.memoizedProps;a=a.memoizedState;c.props=b.memoizedProps;c.state=b.memoizedState;c.componentDidUpdate(d,a)}b=b.updateQueue;null!==b&&je(b,c);break;case 3:c=b.updateQueue;null!==c&&je(c,null!==b.child?b.child.stateNode:null);break;case 5:c=b.stateNode;null===a&&b.effectTag&4&&m(c,b.type,b.memoizedProps,b);break;case 6:break;case 4:break;default:l("163")}},commitAttachRef:function(a){var b=a.ref;if(null!==b){var c=a.stateNode;switch(a.tag){case 5:b(h(c));break;default:b(c)}}},commitDetachRef:function(a){a= 150 | a.ref;null!==a&&a(null)}}},la={},eg=function(a){function b(a){a===la?l("174"):void 0;return a}var c=a.getChildHostContext,d=a.getRootHostContext,e={current:la},f={current:la},g={current:la};return{getHostContext:function(){return b(e.current)},getRootHostContainer:function(){return b(g.current)},popHostContainer:function(a){I(e,a);I(f,a);I(g,a)},popHostContext:function(a){f.current===a&&(I(e,a),I(f,a))},pushHostContainer:function(a,b){M(g,b,a);b=d(b);M(f,a,a);M(e,b,a)},pushHostContext:function(a){var d= 151 | b(g.current),h=b(e.current);d=c(h,a.type,d);h!==d&&(M(f,a,a),M(e,d,a))},resetHostContainer:function(){e.current=la;g.current=la}}},fg=function(a){function b(a,b){var c=new Q(5,null,0);c.type="DELETED";c.stateNode=b;c["return"]=a;c.effectTag=8;null!==a.lastEffect?(a.lastEffect.nextEffect=c,a.lastEffect=c):a.firstEffect=a.lastEffect=c}function c(a,b){switch(a.tag){case 5:return b=f(b,a.type,a.pendingProps),null!==b?(a.stateNode=b,!0):!1;case 6:return b=g(b,a.pendingProps),null!==b?(a.stateNode=b,!0): 152 | !1;default:return!1}}function d(a){for(a=a["return"];null!==a&&5!==a.tag&&3!==a.tag;)a=a["return"];A=a}var e=a.shouldSetTextContent;a=a.hydration;if(!a)return{enterHydrationState:function(){return!1},resetHydrationState:function(){},tryToClaimNextHydratableInstance:function(){},prepareToHydrateHostInstance:function(){l("175")},prepareToHydrateHostTextInstance:function(){l("176")},popHydrationState:function(a){return!1}};var f=a.canHydrateInstance,g=a.canHydrateTextInstance,h=a.getNextHydratableSibling, 153 | k=a.getFirstHydratableChild,m=a.hydrateInstance,D=a.hydrateTextInstance,A=null,v=null,n=!1;return{enterHydrationState:function(a){v=k(a.stateNode.containerInfo);A=a;return n=!0},resetHydrationState:function(){v=A=null;n=!1},tryToClaimNextHydratableInstance:function(a){if(n){var d=v;if(d){if(!c(a,d)){d=h(d);if(!d||!c(a,d)){a.effectTag|=2;n=!1;A=a;return}b(A,v)}A=a;v=k(d)}else a.effectTag|=2,n=!1,A=a}},prepareToHydrateHostInstance:function(a,b,c){b=m(a.stateNode,a.type,a.memoizedProps,b,c,a);a.updateQueue= 154 | b;return null!==b?!0:!1},prepareToHydrateHostTextInstance:function(a){return D(a.stateNode,a.memoizedProps,a)},popHydrationState:function(a){if(a!==A)return!1;if(!n)return d(a),n=!0,!1;var c=a.type;if(5!==a.tag||"head"!==c&&"body"!==c&&!e(c,a.memoizedProps))for(c=v;c;)b(a,c),c=h(c);d(a);v=A?h(a.stateNode):null;return!0}}},gg=function(a){function b(a){X=Ba=!0;var b=a.stateNode;b.current===a?l("177"):void 0;b.isReadyForCommit=!1;bb.current=null;if(1g.expirationTime)&&(f=g.expirationTime),g=g.sibling;e.expirationTime=f}if(null!== 158 | b)return b;null!==c&&(null===c.firstEffect&&(c.firstEffect=a.firstEffect),null!==a.lastEffect&&(null!==c.lastEffect&&(c.lastEffect.nextEffect=a.firstEffect),c.lastEffect=a.lastEffect),1a))if(F<=ha)for(;null!==B;)B=k(B)?e(B):d(B);else for(;null!==B&&!z();)B=k(B)?e(B):d(B)}else if(!(0===F||F>a))if(F<=ha)for(;null!==B;)B=d(B);else for(;null!==B&&!z();)B=d(B)}function g(a,b){Ba?l("243"):void 0;Ba=!0;a.isReadyForCommit=!1;if(a!==Ja||b!==F||null===B){for(;-1b)a.expirationTime=b;null!==a.alternate&&(0===a.alternate.expirationTime||a.alternate.expirationTime>b)&&(a.alternate.expirationTime=b);if(null=== 164 | a["return"])if(3===a.tag){c=a.stateNode;!Ba&&c===Ja&&bGa&&l("185");if(null===d.nextScheduledRoot)d.remainingExpirationTime=e,null===N?(Ka=N=d,d.nextScheduledRoot=d):(N=N.nextScheduledRoot=d,N.nextScheduledRoot=Ka);else{var f=d.remainingExpirationTime;if(0===f||eZ)return;xa(la)}var b=ba()-qa;Z=a;la=wa(H,{timeout:10*(a-2)-b})}function E(){var a=0,b=null;if(null!==N)for(var c=N,d=Ka;null!==d;){var e=d.remainingExpirationTime;if(0===e){null===c||null===N?l("244"):void 0;if(d===d.nextScheduledRoot){Ka=N=d.nextScheduledRoot=null;break}else if(d===Ka)Ka=e=d.nextScheduledRoot,N.nextScheduledRoot=e,d.nextScheduledRoot=null;else if(d===N){N=c;N.nextScheduledRoot=Ka;d.nextScheduledRoot=null;break}else c.nextScheduledRoot=d.nextScheduledRoot, 166 | d.nextScheduledRoot=null;d=c.nextScheduledRoot}else{if(0===a||eHa?!1:oa=!0}function G(a){null===Ea?l("246"):void 0;Ea.remainingExpirationTime=0;da||(da=!0,pa=a)}var q=eg(a),p=fg(a),I=q.popHostContainer,O=q.popHostContext,P=q.resetHostContainer,M=bg(a,q,p,v,A),Q=M.beginWork,T=M.beginFailedWork, 168 | Y=cg(a,q,p).completeWork;q=dg(a,h);var aa=q.commitResetTextContent,V=q.commitPlacement,na=q.commitDeletion,ca=q.commitWork,sa=q.commitLifeCycles,ta=q.commitAttachRef,va=q.commitDetachRef,ba=a.now,wa=a.scheduleDeferredCallback,xa=a.cancelDeferredCallback,ya=a.useSyncScheduling,za=a.prepareForCommit,Aa=a.resetAfterCommit,qa=ba(),ha=2,Ca=0,Ba=!1,B=null,Ja=null,F=0,u=null,S=null,Ia=null,ua=null,ma=null,U=!1,X=!1,ka=!1,Ka=null,N=null,Z=0,la=-1,Na=!1,Ea=null,Fa=0,oa=!1,da=!1,pa=null,W=null,Da=!1,ea=!1, 169 | Ga=1E3,fa=0,Ha=1;return{computeAsyncExpiration:n,computeExpirationForFiber:A,scheduleWork:v,batchedUpdates:function(a,b){var c=Da;Da=!0;try{return a(b)}finally{(Da=c)||Na||x(1,null)}},unbatchedUpdates:function(a){if(Da&&!ea){ea=!0;try{return a()}finally{ea=!1}}return a()},flushSync:function(a){var b=Da;Da=!0;try{a:{var c=Ca;Ca=1;try{var d=a();break a}finally{Ca=c}d=void 0}return d}finally{Da=b,Na?l("187"):void 0,x(1,null)}},deferredUpdates:function(a){var b=Ca;Ca=n();try{return a()}finally{Ca=b}}}}, 170 | Te=function(a){function b(a){a=wf(a);return null===a?null:a.stateNode}var c=a.getPublicInstance;a=gg(a);var d=a.computeAsyncExpiration,e=a.computeExpirationForFiber,f=a.scheduleWork;return{createContainer:function(a,b){var c=new Q(3,null,0);a={current:c,containerInfo:a,pendingChildren:null,remainingExpirationTime:0,isReadyForCommit:!1,finishedWork:null,context:null,pendingContext:null,hydrate:b,nextScheduledRoot:null};return c.stateNode=a},updateContainer:function(a,b,c,m){var g=b.current;if(c){c= 171 | c._reactInternalFiber;var h;b:{2===Qa(c)&&2===c.tag?void 0:l("170");for(h=c;3!==h.tag;){if(Ua(h)){h=h.stateNode.__reactInternalMemoizedMergedChildContext;break b}(h=h["return"])?void 0:l("171")}h=h.stateNode.context}c=Ua(c)?ce(c,h):h}else c=ja;null===b.context?b.context=c:b.pendingContext=c;b=m;b=void 0===b?null:b;m=null!=a&&null!=a.type&&null!=a.type.prototype&&!0===a.type.prototype.unstable_isAsyncReactComponent?d():e(g);Bb(g,{expirationTime:m,partialState:{element:a},callback:b,isReplace:!1,isForced:!1, 172 | nextCallback:null,next:null});f(g,m)},batchedUpdates:a.batchedUpdates,unbatchedUpdates:a.unbatchedUpdates,deferredUpdates:a.deferredUpdates,flushSync:a.flushSync,getPublicRootInstance:function(a){a=a.current;if(!a.child)return null;switch(a.child.tag){case 5:return c(a.child.stateNode);default:return a.child.stateNode}},findHostInstance:b,findHostInstanceWithNoPortals:function(a){a=xf(a);return null===a?null:a.stateNode},injectIntoDevTools:function(a){var c=a.findFiberByHostInstance;return Af(C({}, 173 | a,{findHostInstanceByFiber:function(a){return b(a)},findFiberByHostInstance:function(a){return c?c(a):null}}))}}},Ue=Object.freeze({default:Te}),Sc=Ue&&Te||Ue,hg=Sc["default"]?Sc["default"]:Sc,Ve="object"===typeof performance&&"function"===typeof performance.now,Nb=void 0;Nb=Ve?function(){return performance.now()}:function(){return Date.now()};var Ob=void 0,Pb=void 0;if(P.canUseDOM)if("function"!==typeof requestIdleCallback||"function"!==typeof cancelIdleCallback){var Qb=null,Rb=!1,eb=-1,fb=!1,gb= 174 | 0,Sb=33,hb=33;var Tc=Ve?{didTimeout:!1,timeRemaining:function(){var a=gb-performance.now();return 0=gb-a)if(-1!==eb&&eb<=a)Tc.didTimeout=!0;else{fb||(fb=!0,requestAnimationFrame(Xe));return}else Tc.didTimeout=!1;eb=-1;a=Qb;Qb=null;null!==a&&a(Tc)}},!1); 175 | var Xe=function(a){fb=!1;var b=a-gb+hb;bb&&(b=8),hb=bd&&(e=d,d=a,a=e);e=Qd(c,a);var f=Qd(c,d);if(e&&f&&(1!==b.rangeCount||b.anchorNode!==e.node||b.anchorOffset!==e.offset||b.focusNode!==f.node|| 185 | b.focusOffset!==f.offset)){var g=document.createRange();g.setStart(e.node,e.offset);b.removeAllRanges();a>d?(b.addRange(g),b.extend(f.node,f.offset)):(g.setEnd(f.node,f.offset),b.addRange(g))}}b=[];for(a=c;a=a.parentNode;)1===a.nodeType&&b.push({element:a,left:a.scrollLeft,top:a.scrollTop});try{c.focus()}catch(h){}for(c=0;cu.length&&u.push(a)}function t(a,b,c,d){var f=typeof a;if("undefined"===f||"boolean"===f)a=null;var l=!1;if(null===a)l=!0;else switch(f){case "string":case "number":l=!0;break;case "object":switch(a.$$typeof){case r:case P:case Q:case R:l=!0}}if(l)return c(d,a,""===b?"."+D(a,0):b),1;l=0;b=""===b?".":b+":";if(Array.isArray(a))for(var e= 13 | 0;ea;a++)b["_"+String.fromCharCode(a)]=a;if("0123456789"!==Object.getOwnPropertyNames(b).map(function(a){return b[a]}).join(""))return!1;var c={};"abcdefghijklmnopqrst".split("").forEach(function(a){c[a]=a});return"abcdefghijklmnopqrst"!==Object.keys(Object.assign({},c)).join("")?!1:!0}catch(d){return!1}}()?Object.assign: 16 | function(a,b){if(null===a||void 0===a)throw new TypeError("Object.assign cannot be called with null or undefined");var c=Object(a);for(var d,f=1;f 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /extension/icon/RP128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitin42/react-perf-devtool/3f6d159c62b68f253ebc74b9bfc2d485c196ed63/extension/icon/RP128.png -------------------------------------------------------------------------------- /extension/icon/RP16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitin42/react-perf-devtool/3f6d159c62b68f253ebc74b9bfc2d485c196ed63/extension/icon/RP16.png -------------------------------------------------------------------------------- /extension/icon/RP48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitin42/react-perf-devtool/3f6d159c62b68f253ebc74b9bfc2d485c196ed63/extension/icon/RP48.png -------------------------------------------------------------------------------- /extension/load.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /extension/load.js: -------------------------------------------------------------------------------- 1 | require.config({ 2 | paths: { 3 | react: './dependencies/react', 4 | 'react-dom': './dependencies/react-dom', 5 | Chart: './dependencies/Chart' 6 | } 7 | }) 8 | 9 | requirejs( 10 | ['react', 'react-dom', 'Chart', '../build/ReactPerfDevtool'], 11 | function(React, ReactDOM, Chart, { ReactPerfDevtool }) { 12 | const root = document.getElementById('root') 13 | 14 | ReactDOM.render( 15 | React.createElement(ReactPerfDevtool, { Graphics: Chart }), 16 | root 17 | ) 18 | } 19 | ) 20 | -------------------------------------------------------------------------------- /extension/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | 4 | "name": "React Performance Devtool", 5 | "short_name": "react-perf-devtool", 6 | "description": 7 | "A devtool extension for inspecting the performance of React components.", 8 | "version": "5.5", 9 | "devtools_page": "devtools.html", 10 | 11 | "icons": { 12 | "16": "./icon/RP16.png", 13 | "48": "./icon/RP48.png", 14 | "128": "./icon/RP128.png" 15 | }, 16 | 17 | "permissions": ["file:///*", "http://*/*", "https://*/*"], 18 | 19 | "content_security_policy": 20 | "script-src 'self' 'unsafe-eval'; object-src 'self'" 21 | } 22 | -------------------------------------------------------------------------------- /extension/panel.js: -------------------------------------------------------------------------------- 1 | chrome.devtools.panels.create( 2 | 'React Performance', 3 | './icon/RP48.png', 4 | 'load.html', 5 | function(panel) { 6 | console.log('Started!') 7 | } 8 | ) 9 | -------------------------------------------------------------------------------- /extension/styles/index.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-family: "Metrophobic", Georgia, Serif; 3 | } 4 | 5 | table { 6 | font-family: "Metrophobic", Georgia, Serif; 7 | border-collapse: collapse; 8 | width: 100%; 9 | } 10 | 11 | tr td:last-child {text-align:right} 12 | td, th { 13 | font-size: 15px; 14 | border-bottom: 1px solid #dddddd; 15 | text-align: left; 16 | padding: 8px; 17 | } 18 | 19 | #measures tr:nth-child(even){ 20 | background-color: rgba(100, 100, 100, 0.2); 21 | } 22 | 23 | button { 24 | margin: 5px; 25 | padding: 6px; 26 | border: 0.5px solid rgba(100, 100, 100, 0.2); 27 | font-size: 10px; 28 | font-weight: bolder; 29 | } 30 | 31 | .overview { 32 | margin-top: 30px; 33 | margin-bottom: 10px; 34 | padding-bottom: 10px; 35 | border-bottom: 1px solid #f3f3f3; 36 | } 37 | 38 | .results { 39 | margin-top: 30px; 40 | margin-bottom: 10px; 41 | padding-bottom: 10px; 42 | border-bottom: 1px solid #f3f3f3; 43 | } 44 | 45 | .loader-container{ 46 | margin-top: 5%; 47 | display: flex; 48 | justify-content: center; 49 | } 50 | 51 | .loader { 52 | border: 5px solid #f3f3f3; 53 | border-radius: 50%; 54 | border-top: 5px solid #3F51B5; 55 | width: 50px; 56 | height: 50px; 57 | -webkit-animation: spin 2s linear infinite; /* Safari */ 58 | animation: spin 2s linear infinite; 59 | } 60 | 61 | .dark-loader { 62 | border: 5px solid black; 63 | border-radius: 50%; 64 | border-top: 5px solid #f3f3f3; 65 | width: 50px; 66 | height: 50px; 67 | -webkit-animation: spin 2s linear infinite; /* Safari */ 68 | animation: spin 2s linear infinite; 69 | } 70 | 71 | /* Safari */ 72 | @-webkit-keyframes spin { 73 | 0% { -webkit-transform: rotate(0deg); } 74 | 100% { -webkit-transform: rotate(360deg); } 75 | } 76 | 77 | @keyframes spin { 78 | 0% { transform: rotate(0deg); } 79 | 100% { transform: rotate(360deg); } 80 | } 81 | 82 | .loading-text{ 83 | text-align:center; 84 | color:#3F51B5; 85 | font-size:17px; 86 | } 87 | 88 | .dark-loading-text { 89 | text-align:center; 90 | color:#f3f3f3; 91 | font-size:17px; 92 | } 93 | 94 | .circle-background, 95 | .circle-progress { 96 | fill: none; 97 | } 98 | 99 | .circle-background { 100 | stroke: #ddd; 101 | } 102 | 103 | .circle-progress { 104 | stroke: #3F51B5; 105 | stroke-linecap: round; 106 | stroke-linejoin: round; 107 | } 108 | 109 | .circle-black-progress { 110 | stroke: #ddd; 111 | stroke-linecap: round; 112 | stroke-linejoin: round; 113 | } 114 | 115 | .circle-black-background { 116 | stroke: black; 117 | } 118 | 119 | .circle-text { 120 | font-size: 1.4em; 121 | fill: #3F51B5; 122 | } 123 | 124 | .circle-black-text { 125 | font-size: 1.4em; 126 | fill: #ddd; 127 | } 128 | 129 | .ctime li{ 130 | display: inline-block; 131 | margin-bottom: 0 !important; 132 | } 133 | 134 | .btn { 135 | background-color: #3F51B5; 136 | color: white; 137 | font-size: 14px; 138 | border-radius: 4%; 139 | cursor: pointer; 140 | } 141 | 142 | .dark-btn { 143 | background-color: black; 144 | color: white; 145 | font-size: 14px; 146 | border-radius: 4%; 147 | cursor: pointer; 148 | border-color: #3b3c3d; 149 | } 150 | 151 | .component-progress-container{ 152 | margin-bottom: 0 !important; 153 | } 154 | 155 | .container{ 156 | width: 60%; 157 | } 158 | 159 | .component-result { 160 | margin-top: 2%; 161 | margin-bottom: 2%; 162 | } 163 | 164 | .component-text{ 165 | margin-top: 30px; 166 | margin-bottom: 10px; 167 | padding-bottom: 10px; 168 | border-bottom: 1px solid #f3f3f3; 169 | } 170 | 171 | .events { 172 | font-size: 20px; 173 | padding: 8px; 174 | } 175 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | interface IOptions { 2 | shouldLog?: boolean; 3 | port?: number; 4 | components?: string[]; 5 | } 6 | 7 | interface IMeasureDetail { 8 | averageTimeSpentMs: string; 9 | numberOfTimes: number; 10 | totalTimeSpentMs: number; 11 | } 12 | 13 | interface IMeasure { 14 | componentName: string; // Name of the component 15 | totalTimeSpent: number; // Total time taken by the component combining all the phases 16 | percentTimeSpent: string; // Percent time 17 | numberOfInstances: number; // Number of instances of the component 18 | mount: IMeasureDetail; // Mount time 19 | render: IMeasureDetail; // Render time 20 | update: IMeasureDetail; // Update time 21 | unmount: IMeasureDetail; // Unmount time 22 | 23 | // Time taken in lifecycle hooks 24 | componentWillMount: IMeasureDetail; 25 | componentDidMount: IMeasureDetail; 26 | componentWillReceiveProps: IMeasureDetail; 27 | shouldComponentUpdate: IMeasureDetail; 28 | componentWillUpdate: IMeasureDetail; 29 | componentDidUpdate: IMeasureDetail; 30 | componentWillUnmount: IMeasureDetail; 31 | } 32 | 33 | declare module 'react-perf-devtool' { 34 | function registerObserver( 35 | options?: IOptions, 36 | callback?: (measures: Array) => void 37 | ): void 38 | } -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/devtool-hook.js') 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-perf-devtool", 3 | "version": "3.1.8", 4 | "description": "A devtool for inspecting the performance of React Components", 5 | "main": "index.js", 6 | "types": "index.d.ts", 7 | "files": ["src", "lib"], 8 | "author": "Nitin Tulswani", 9 | "license": "MIT", 10 | "peerDependencies": { 11 | "react": "^16.2.0", 12 | "react-dom": "^16.2.0" 13 | }, 14 | "devDependencies": { 15 | "babel-core": "^6.26.0", 16 | "babel-jest": "^21.2.0", 17 | "babel-loader": "^7.1.2", 18 | "babel-plugin-transform-class-properties": "^6.24.1", 19 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 20 | "babel-preset-env": "^1.6.1", 21 | "babel-preset-react": "^6.24.1", 22 | "husky": "^0.14.3", 23 | "jest": "^21.2.1", 24 | "lint-staged": "^6.0.0", 25 | "prettier": "^1.9.2", 26 | "react": "^16.2.0", 27 | "react-dom": "^16.2.0", 28 | "react-test-renderer": "^16.2.0", 29 | "uglifyjs-webpack-plugin": "^1.1.2", 30 | "webpack": "^4.0.1", 31 | "webpack-cli": "^2.0.9" 32 | }, 33 | "scripts": { 34 | "test": "jest", 35 | "build:hook": 36 | "rm -rf ./extension/build && NODE_ENV=production ./node_modules/.bin/webpack-cli --config ./webpack/webpack.config.hook.js --progress", 37 | "build:watch": 38 | "rm -rf ./extension/build && NODE_ENV=production ./node_modules/.bin/webpack-cli --watch --config ./webpack/webpack.config.js --progress", 39 | "build": 40 | "rm -rf ./extension/build && NODE_ENV=production ./node_modules/.bin/webpack-cli --config ./webpack/webpack.config.js --progress", 41 | "precommit": "lint-staged", 42 | "format": 43 | "find src -name '*.js' | xargs ./node_modules/.bin/prettier --write --no-semi --single-quote", 44 | "format:nowrite": 45 | "find src -name '*.js' | xargs ./node_modules/.bin/prettier --no-semi --single-quote --list-different --loglevel error", 46 | "generate": 47 | "yarn build && rm -rf ./extension/extension.zip && zip -r ./extension/extension.zip ./extension", 48 | "build:extension": "yarn format && yarn test && yarn generate" 49 | }, 50 | "lint-staged": { 51 | "*.{js,json}": [ 52 | "./node_modules/.bin/prettier --write --no-semi --single-quote", 53 | "git add" 54 | ] 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /samples/measures.js: -------------------------------------------------------------------------------- 1 | const MEASURES = [ 2 | { 3 | name: 4 | '⛔ (React Tree Reconciliation) Warning: There were cascading updates', 5 | entryType: 'measure', 6 | startTime: 414.90500000000003, 7 | duration: 54.74000000000001 8 | }, 9 | { 10 | name: '⚛ App [mount]', 11 | entryType: 'measure', 12 | startTime: 417.13500000000005, 13 | duration: 36.875 14 | }, 15 | { 16 | name: '⚛ App.componentWillMount', 17 | entryType: 'measure', 18 | startTime: 418.055, 19 | duration: 3.0249999999999773 20 | }, 21 | { 22 | name: '⚛ Main [mount]', 23 | entryType: 'measure', 24 | startTime: 422.55, 25 | duration: 31.350000000000023 26 | }, 27 | { 28 | name: '⚛ Main.componentWillMount', 29 | entryType: 'measure', 30 | startTime: 423.15000000000003, 31 | duration: 0.875 32 | }, 33 | { 34 | name: '⚛ Introduction [mount]', 35 | entryType: 'measure', 36 | startTime: 425.5450000000001, 37 | duration: 11.33499999999998 38 | }, 39 | { 40 | name: '⚛ Introduction.componentWillMount', 41 | entryType: 'measure', 42 | startTime: 426.005, 43 | duration: 0.8350000000000364 44 | }, 45 | { 46 | name: '⚛ Projects [mount]', 47 | entryType: 'measure', 48 | startTime: 436.91, 49 | duration: 9.435000000000002 50 | }, 51 | { 52 | name: '⚛ Projects.componentWillMount', 53 | entryType: 'measure', 54 | startTime: 437.595, 55 | duration: 1.8450000000000273 56 | }, 57 | { 58 | name: '⚛ Posts [mount]', 59 | entryType: 'measure', 60 | startTime: 446.365, 61 | duration: 4.045000000000016 62 | }, 63 | { 64 | name: '⚛ Posts.componentWillMount', 65 | entryType: 'measure', 66 | startTime: 446.975, 67 | duration: 1.1050000000000182 68 | }, 69 | { 70 | name: '⚛ Message [mount]', 71 | entryType: 'measure', 72 | startTime: 450.43000000000006, 73 | duration: 3.4149999999999636 74 | }, 75 | { 76 | name: '⚛ Message.componentWillMount', 77 | entryType: 'measure', 78 | startTime: 450.85, 79 | duration: 0.6700000000000159 80 | }, 81 | { 82 | name: 83 | '⛔ (Committing Changes) Warning: Lifecycle hook scheduled a cascading update', 84 | entryType: 'measure', 85 | startTime: 454.31000000000006, 86 | duration: 6.214999999999975 87 | }, 88 | { 89 | name: '⚛ (Committing Host Effects: 7 Total)', 90 | entryType: 'measure', 91 | startTime: 454.61500000000007, 92 | duration: 1.1399999999999295 93 | }, 94 | { 95 | name: '⚛ (Calling Lifecycle Methods: 6 Total)', 96 | entryType: 'measure', 97 | startTime: 456.03000000000003, 98 | duration: 4.3799999999999955 99 | }, 100 | { 101 | name: 102 | '⛔ Introduction.componentDidMount Warning: Scheduled a cascading update', 103 | entryType: 'measure', 104 | startTime: 456.47, 105 | duration: 0.4600000000000364 106 | }, 107 | { 108 | name: '⛔ Projects.componentDidMount Warning: Scheduled a cascading update', 109 | entryType: 'measure', 110 | startTime: 456.95500000000004, 111 | duration: 0.07999999999998408 112 | }, 113 | { 114 | name: '⚛ Posts.componentDidMount', 115 | entryType: 'measure', 116 | startTime: 457.05, 117 | duration: 0.8250000000000455 118 | }, 119 | { 120 | name: '⚛ Message.componentDidMount', 121 | entryType: 'measure', 122 | startTime: 457.9200000000001, 123 | duration: 0.55499999999995 124 | }, 125 | { 126 | name: '⚛ Main.componentDidMount', 127 | entryType: 'measure', 128 | startTime: 458.49500000000006, 129 | duration: 0.8099999999999454 130 | }, 131 | { 132 | name: '⚛ App.componentDidMount', 133 | entryType: 'measure', 134 | startTime: 459.33000000000004, 135 | duration: 0.9200000000000159 136 | }, 137 | { 138 | name: '⚛ Introduction [update]', 139 | entryType: 'measure', 140 | startTime: 462.17500000000007, 141 | duration: 2.8349999999999795 142 | }, 143 | { 144 | name: '⚛ Projects [update]', 145 | entryType: 'measure', 146 | startTime: 465.035, 147 | duration: 3.025000000000034 148 | }, 149 | { 150 | name: 151 | '⛔ (Committing Changes) Warning: Caused by a cascading update in earlier commit', 152 | entryType: 'measure', 153 | startTime: 468.31000000000006, 154 | duration: 1.1399999999999864 155 | }, 156 | { 157 | name: '⚛ (Committing Host Effects: 1 Total)', 158 | entryType: 'measure', 159 | startTime: 468.425, 160 | duration: 0.08999999999997499 161 | }, 162 | { 163 | name: '⚛ (Calling Lifecycle Methods: 1 Total)', 164 | entryType: 'measure', 165 | startTime: 468.535, 166 | duration: 0.8899999999999864 167 | }, 168 | { 169 | name: '⚛ Introduction.componentDidUpdate', 170 | entryType: 'measure', 171 | startTime: 468.57500000000005, 172 | duration: 0.8100000000000023 173 | }, 174 | { 175 | name: '⚛ (React Tree Reconciliation)', 176 | entryType: 'measure', 177 | startTime: 634.6050000000001, 178 | duration: 0.15999999999985448 179 | } 180 | ] 181 | 182 | module.exports = MEASURES 183 | -------------------------------------------------------------------------------- /src/extension/components/Buttons.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export function Buttons({ theme, clear, reload }) { 4 | return ( 5 | 6 | 9 | 15 | 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /src/extension/components/ComponentTime.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { ProgressLoader } from './ProgressLoader' 4 | 5 | const theme = require('../theme') 6 | 7 | // Align the overview pane 8 | const calign = { 9 | margin: '0', 10 | paddingLeft: '10', 11 | paddingRight: '10', 12 | paddingTop: '10', 13 | textAlign: 'center', 14 | cursor: 'pointer', 15 | fontFamily: 'Metrophobic, Georgia, Serif', 16 | fontSize: '13px' 17 | } 18 | 19 | // Style links for components 20 | const setHrefStyles = theme => 21 | theme === 'dark' 22 | ? { textDecoration: 'none', color: '#eff1f4' } 23 | : { textDecoration: 'none', color: 'black' } 24 | 25 | export function ComponentTime(props) { 26 | return ( 27 | 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /src/extension/components/ErrorComponent.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export function ErrorComponent() { 4 | return ( 5 |
6 | An error occurred while collecting the measures. This is possibly due to 7 |
    8 |
  • 9 | absence of register observer hook in your project.{' '} 10 | 15 | See the detailed documentation 16 | {' '} 17 | on how to register a top level observer in your React application. 18 |
  • 19 |
    20 |
  • your project is not using React.
  • 21 |
22 |

23 | If above solutions don't work, then try reloading the plugin or close 24 | and reopen the inspected window. 25 |

26 |
27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /src/extension/components/Graphics.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export function Graphics(props) { 4 | return ( 5 | 6 |

Overall time taken

7 | 8 |

9 | {' '} 10 | Total time taken combining all the phases -{' '} 11 | {props.totalTime} ms{' '} 12 |

13 |
14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /src/extension/components/Measures.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { ProgressLoader } from './ProgressLoader' 4 | 5 | const theme = require('../theme') 6 | 7 | /** 8 | This component renders the data (measures of each component) 9 | */ 10 | export function Measures(props) { 11 | return ( 12 |
13 |

Components

14 | {props.measures.map((measure, index) => { 15 | return ( 16 |
21 |

{measure.componentName}

22 | 23 | 24 | 25 | 28 | 29 | 30 | 31 | 34 | 35 | 36 | 37 | 40 | 41 | 42 | 43 | 46 | 47 | 48 | 49 | 52 | 53 | 54 | 55 | 58 | 59 | 60 | 61 | 64 | 65 | 66 | 67 | 70 | 71 | 72 | 73 | 76 | 77 | 78 | 79 | 84 | 85 | 86 | 87 | 92 | 93 | 94 | 95 | 100 | 101 | 102 | 103 | 106 | 107 | 108 | 109 | 114 | 115 |
Total time (ms) 26 | {Number(measure.totalTimeSpent.toFixed(2))} 27 |
Instances 32 | {measure.numberOfInstances} 33 |
Total time (%) 38 | {measure.percentTimeSpent} 39 |
Mount (ms) 44 | {measure.mount.totalTimeSpentMs} 45 |
Update (ms) 50 | {measure.update.totalTimeSpentMs} 51 |
Render (ms) 56 | {measure.render.totalTimeSpentMs} 57 |
Unmount (ms) 62 | {measure.unmount.totalTimeSpentMs} 63 |
componentWillMount (ms) 68 | {measure.componentWillMount.totalTimeSpentMs} 69 |
componentDidMount (ms) 74 | {measure.componentDidMount.totalTimeSpentMs} 75 |
componentWillReceiveProps (ms) 80 | 81 | {measure.componentWillReceiveProps.totalTimeSpentMs} 82 | 83 |
shouldComponentUpdate (ms) 88 | 89 | {measure.shouldComponentUpdate.totalTimeSpentMs} 90 | 91 |
componentWillUpdate (ms) 96 | 97 | {measure.componentWillUpdate.totalTimeSpentMs} 98 | 99 |
componentDidUpdate (ms) 104 | {measure.componentDidUpdate.totalTimeSpentMs} 105 |
componentWillUnmount (ms) 110 | 111 | {measure.componentWillUnmount.totalTimeSpentMs} 112 | 113 |
116 |
117 | ) 118 | })} 119 |
120 | ) 121 | } 122 | -------------------------------------------------------------------------------- /src/extension/components/Metrics.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { ComponentTime } from './ComponentTime' 4 | 5 | /** 6 | This component contains the performance measures of React components in Circular loader . 7 | */ 8 | export function Metrics(props) { 9 | return ( 10 | 11 |
12 |

Overview

13 |
    14 | {props.measures.map((measure, index) => { 15 | return ( 16 |
  • 17 | 22 |
  • 23 | ) 24 | })} 25 |
26 |
27 |
28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /src/extension/components/ProgressLoader.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const theme = require('../theme') 4 | 5 | // inspired from https://codepen.io/bbrady/pen/ozrjKE 6 | 7 | export function ProgressLoader(props) { 8 | // Size of the enclosing square 9 | const sqSize = props.sqSize 10 | // SVG centers the stroke width on the radius, subtract out so circle fits in square 11 | const radius = (props.sqSize - props.strokeWidth) / 2 12 | // Enclose cicle in a circumscribing square 13 | const viewBox = `0 0 ${sqSize} ${sqSize}` 14 | 15 | const strokeWidth = props.strokeWidth 16 | // Arc length at 100% coverage is the circle circumference 17 | const dashArray = radius * Math.PI * 2 18 | // Scale 100% coverage overlay with the actual percent 19 | const dashOffset = dashArray - dashArray * props.percentage / 100 20 | 21 | const percentage = props.percentage 22 | 23 | return ( 24 | 25 | 34 | 49 | 56 | {`${percentage} %`} 57 | 58 | 59 | ) 60 | } 61 | -------------------------------------------------------------------------------- /src/extension/components/ReactPerfDevtool.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { Metrics } from './Metrics' 4 | import { ErrorComponent } from './ErrorComponent' 5 | import { Measures } from './Measures' 6 | import { Buttons } from './Buttons' 7 | import { Stats } from './Stats' 8 | 9 | import { computeTotalTime, getResults } from '../util' 10 | 11 | const theme = require('../theme') 12 | 13 | // Stores the measures 14 | let store = [] 15 | 16 | // These fields are evaluated in the inspectedWindow to get information about measures. 17 | let queries = { 18 | measuresLength: 'JSON.stringify(__REACT_PERF_DEVTOOL_GLOBAL_STORE__.length)', 19 | measures: 'JSON.stringify(__REACT_PERF_DEVTOOL_GLOBAL_STORE__.measures)', 20 | rawMeasures: 21 | 'JSON.stringify(__REACT_PERF_DEVTOOL_GLOBAL_STORE__.rawMeasures)', 22 | getTimeoutValue: 23 | 'JSON.stringify(__REACT_PERF_DEVTOOL_GLOBAL_STORE__.timeout)', 24 | clear: `__REACT_PERF_DEVTOOL_GLOBAL_STORE__ = { 25 | length: 0, 26 | measures: [], 27 | rawMeasures: [], 28 | timeout: __REACT_PERF_DEVTOOL_GLOBAL_STORE__.timeout 29 | }` 30 | } 31 | 32 | const sleep = timeout => new Promise(resolve => setTimeout(resolve, timeout)) 33 | 34 | /** 35 | This is the main component that renders the table, containing information about 36 | the component mount/render/update/unmount time and also lifecycle time. 37 | It also renders the total time taken while committing the changes, host effects 38 | and calling all the lifecycle methods. 39 | */ 40 | export class ReactPerfDevtool extends React.Component { 41 | timer = null 42 | evaluate = chrome.devtools.inspectedWindow.eval 43 | panelStyles = { 44 | color: theme === 'dark' ? 'white' : 'black', 45 | fontFamily: 'Metrophobic, Georgia, Serif', 46 | fontSize: '15px' 47 | } 48 | 49 | constructor(props) { 50 | super(props) 51 | this.state = { 52 | perfData: [], // Stores the parsed performance measures 53 | totalTime: 0, // Total time taken combining all the phases. 54 | pendingEvents: 0, // Pending event count. 55 | rawMeasures: [], // Raw measures output. It is used for rendering the overall results. 56 | loading: false, // To show the loading output while collecting the results. 57 | hasError: false, // Track errors, occurred when collecting the measures. 58 | showChart: false 59 | } 60 | } 61 | 62 | componentDidMount() { 63 | // Show the loader while the measures get resolved. 64 | // showChart, when set to true, render the canvas required for Chart.js. 65 | this.setState({ loading: true, showChart: true }) 66 | 67 | // Defer the initialization of the extension until the application loads. Why ? 68 | // Because some of the applications are huge in size and may take a lot of time to load. 69 | // If the extension initialization process kicks-in before the app loads, we are trapped inside the error state. 70 | // With this, users can configure a timeout value for initialization of the extension using the observer hook like this `registerObserver({ timeout: 4000 })` 71 | // Default value for the timeout is 2 sec. 72 | 73 | // We need to resolve the promise after one sec. to make sure we have the updated __REACT_PERF_DEVTOOL_GLOBAL_STORE__ object otherwise we might end up with null 74 | sleep(1000).then(res => { 75 | this.evaluate(queries['getTimeoutValue'], (timeout, err) => { 76 | if (err) { 77 | this.setErrorState() 78 | return 79 | } 80 | 81 | // This is also backward compatible with older versions of observer hook (for versions <= @3.0.8) 82 | this.timer = setInterval( 83 | () => this.getMeasuresLength(), 84 | timeout !== undefined ? JSON.parse(timeout) : 2000 85 | ) 86 | }) 87 | }) 88 | } 89 | 90 | componentWillUnmount() { 91 | this.timer && clearInterval(this.timer) 92 | } 93 | 94 | reloadInspectedWindow = () => chrome.devtools.inspectedWindow.reload() 95 | 96 | chartOptions = ({ 97 | totalTime, 98 | commitChanges, 99 | hostEffects, 100 | lifecycleTime, 101 | totalComponents, 102 | totalEffects, 103 | totalLifecycleMethods 104 | }) => ({ 105 | type: 'doughnut', 106 | data: { 107 | datasets: [ 108 | { 109 | data: [totalTime, commitChanges, hostEffects, lifecycleTime], 110 | backgroundColor: ['#ff9999', '#99ffdd', '#d98cb3', '#ffff4d'] 111 | } 112 | ], 113 | 114 | // These labels appear in the legend and in the tooltips when hovering different arcs 115 | labels: [ 116 | `${totalComponents} ${ 117 | totalComponents === 0 || totalComponents === 1 118 | ? 'component' 119 | : 'components' 120 | } (ms)`, 121 | 'Committing changes (ms)', 122 | `Committing ${totalEffects} host ${ 123 | totalEffects === 0 || totalEffects === 1 ? 'effect' : 'effects' 124 | } (ms)`, 125 | `Calling ${totalLifecycleMethods} ${ 126 | totalLifecycleMethods === 0 || totalLifecycleMethods === 1 127 | ? 'lifecycle hook' 128 | : 'lifecycle hooks' 129 | } (ms)` 130 | ] 131 | }, 132 | options: { 133 | responsive: false, 134 | maintainAspectRatio: false 135 | } 136 | }) 137 | 138 | getChartData = props => { 139 | const totalEffects = getResults(props.rawMeasures).totalEffects 140 | const hostEffects = getResults(props.rawMeasures).hostEffectsTime 141 | const commitChanges = getResults(props.rawMeasures).commitChangesTime 142 | const totalLifecycleMethods = getResults(props.rawMeasures) 143 | .totalLifecycleMethods 144 | const lifecycleTime = getResults(props.rawMeasures).lifecycleTime 145 | const totalTime = computeTotalTime( 146 | props.rawMeasures, 147 | props.totalTime 148 | ).toFixed(2) 149 | 150 | return { 151 | totalEffects, 152 | hostEffects, 153 | commitChanges, 154 | totalLifecycleMethods, 155 | lifecycleTime, 156 | totalTime, 157 | totalComponents: props.totalComponents 158 | } 159 | } 160 | 161 | drawChart = props => { 162 | const contex = document.getElementById('myChart') 163 | const Chart = this.props.Graphics 164 | 165 | const statsChart = new Chart( 166 | contex, 167 | this.chartOptions(this.getChartData(props)) 168 | ) 169 | } 170 | 171 | setErrorState = () => this.setState({ hasError: true, loading: false }) 172 | 173 | getMeasuresLength = () => { 174 | this.evaluate(queries['measuresLength'], (count, err) => { 175 | if (err) { 176 | this.setErrorState() 177 | return 178 | } 179 | 180 | // Update the event count. 181 | this.updateEventsCount(JSON.parse(count)) 182 | }) 183 | } 184 | 185 | updateEventsCount = count => { 186 | this.setState({ 187 | pendingEvents: count, 188 | loading: false 189 | }) 190 | 191 | // Render the measures if the store is empty. 192 | this.shouldRenderMeasures() 193 | } 194 | 195 | shouldRenderMeasures = () => { 196 | if (this.state.perfData.length === 0) { 197 | // Get the performance measures. 198 | this.getMeasures() 199 | } 200 | } 201 | 202 | getMeasures = () => { 203 | // Returns the performance entries which are not parsed and not aggregated 204 | // These measures are required for creating the chart. 205 | this.getRawMeasures() 206 | 207 | this.evaluate(queries['measures'], (measures, err) => { 208 | if (err) { 209 | this.setErrorState() 210 | return 211 | } 212 | 213 | this.updateMeasures(JSON.parse(measures)) 214 | }) 215 | } 216 | 217 | getRawMeasures = () => { 218 | this.evaluate(queries['rawMeasures'], (measures, err) => { 219 | if (err) { 220 | this.setErrorState() 221 | return 222 | } 223 | 224 | this.setState({ 225 | rawMeasures: JSON.parse(measures) 226 | }) 227 | }) 228 | } 229 | 230 | updateMeasures = measures => { 231 | store = store.concat(measures) 232 | 233 | this.drawChart({ 234 | totalTime: store 235 | .reduce((acc, comp) => acc + comp.totalTimeSpent, 0) 236 | .toFixed(2), 237 | rawMeasures: this.state.rawMeasures, 238 | totalComponents: store.length 239 | }) 240 | 241 | this.setState({ 242 | perfData: store, 243 | totalTime: store 244 | .reduce((acc, comp) => acc + comp.totalTimeSpent, 0) 245 | .toFixed(2) 246 | }) 247 | 248 | // Clear the shared state, so that new measures can be appended (and they don't override) 249 | this.clearMeasures() 250 | } 251 | 252 | clearMeasures = () => this.evaluate(queries['clear']) 253 | 254 | // Clear the panel content. 255 | clear = () => { 256 | store = [] 257 | 258 | this.setState({ 259 | perfData: store, 260 | totalTime: 0, 261 | rawMeasures: [], 262 | pendingEvents: 0, 263 | showChart: false 264 | }) 265 | 266 | this.clearMeasures() 267 | 268 | // Makes sure that we are not batching calls for example - setting chart state here false and batching drawChart may result in an error because the canvas may not render. 269 | this.timer && clearInterval(this.timer) 270 | } 271 | 272 | browserReload = () => 273 | typeof window !== undefined ? window.location.reload() : null 274 | 275 | // Reload. 276 | reload = () => { 277 | this.clear() 278 | this.browserReload() 279 | 280 | // This avoids a flash when the inspected window is reloaded. 281 | this.setState({ loading: true }) 282 | 283 | this.reloadInspectedWindow() 284 | } 285 | 286 | render() { 287 | if (this.state.loading) { 288 | return ( 289 |
290 |
291 |
292 |
293 |

296 | Collecting React performance measures... 297 |

298 |
299 | ) 300 | } 301 | 302 | return ( 303 |
304 |
305 | 306 | 307 | Pending events: {this.state.pendingEvents} 308 | 309 |
310 | {this.state.hasError ? ( 311 | 312 | ) : ( 313 | 314 | 315 | 319 | 320 | 321 | )} 322 |
323 | ) 324 | } 325 | } 326 | -------------------------------------------------------------------------------- /src/extension/components/Stats.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Graphics } from './Graphics' 3 | 4 | export function Stats({ showChart, totalTime }) { 5 | return ( 6 |
7 |

Stats

8 | {showChart ? : null} 9 |
10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /src/extension/theme.js: -------------------------------------------------------------------------------- 1 | module.exports = chrome.devtools.panels.themeName 2 | -------------------------------------------------------------------------------- /src/extension/util.js: -------------------------------------------------------------------------------- 1 | import { getCommitChangesTime } from '../shared/commitChanges' 2 | import { 3 | getCommitHostEffectsTime, 4 | getTotalEffects 5 | } from '../shared/hostEffects' 6 | import { getLifecycleTime, getTotalMethods } from '../shared/lifecycle' 7 | 8 | import { getTotalTime } from '../shared/totalTime' 9 | 10 | // Compute the total time 11 | export function computeTotalTime(measures, componentTotalTime) { 12 | let total = 0 13 | 14 | { 15 | total = total || 0 16 | total += getTotalTime(getCommitChangesTime(measures)) 17 | total += getTotalTime(getCommitHostEffectsTime(measures)) 18 | total += getTotalTime(getLifecycleTime(measures)) 19 | total += Number(componentTotalTime) 20 | } 21 | 22 | return total 23 | } 24 | 25 | // Compute the results (host effects, lifecycle and committing change time) 26 | export function getResults(measures) { 27 | return { 28 | commitChangesTime: Number( 29 | getTotalTime(getCommitChangesTime(measures)).toFixed(2) 30 | ), 31 | totalEffects: Number(getTotalEffects(getCommitHostEffectsTime(measures))), 32 | hostEffectsTime: Number( 33 | getTotalTime(getCommitHostEffectsTime(measures)).toFixed(2) 34 | ), 35 | totalLifecycleMethods: Number(getTotalMethods(getLifecycleTime(measures))), 36 | lifecycleTime: Number(getTotalTime(getLifecycleTime(measures)).toFixed(2)) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/hook.js: -------------------------------------------------------------------------------- 1 | import { registerObserver } from './npm/hook' 2 | 3 | export { registerObserver } 4 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { ReactPerfDevtool } from './extension/components/ReactPerfDevtool' 2 | import { registerObserver } from './npm/hook' 3 | 4 | export { ReactPerfDevtool, registerObserver } 5 | -------------------------------------------------------------------------------- /src/npm/hook.js: -------------------------------------------------------------------------------- 1 | import { getReactPerformanceData } from '../shared/parse' 2 | import { generateDataFromMeasures } from '../shared/generate' 3 | 4 | /** 5 | This registers an observer that listens to the React performance measurement event. 6 | It hooks an object containing information about the events and performance measures of React components to the 7 | global state (window object) which can then be accessed inside the inspected window using eval(). 8 | 9 | With every re-render, this object is updated with new measures and events count. 10 | The extension takes care of clearing up the memory (required to store this object) and also the cache. 11 | 12 | Calculating and aggregating the results happens inside the app frame and not in the devtool. It has its own benefits. 13 | * These measures can be send to a server for analyses 14 | * Measures can be logged to a console 15 | * Particular measures can be inspected in the console with the help of configuration object (not done with the API for it yet) 16 | * This also gives control to the developer on how to manage and inspect the measures apart from using the extension 17 | 18 | Trade-offs of previous version: 19 | * Need to update the commonjs react-dom development bundle (commenting the line) 20 | * No way of sending the measures from the app frame to the console 21 | * Need to query measures rather than listening to an event once 22 | * No control on how to inspect the measures for a particular use case (for eg - render and update performance of a component) 23 | 24 | Options, passed to listener: 25 | * shouldLog (log to console) 26 | * port (port number to send the data to console) 27 | * components (array of components to measure) 28 | 29 | Callback (optional): A callback can also be passed. The callback receives the parsed and aggregated results of the performance measures. 30 | 31 | NOTE: This should only be used in development mode. 32 | */ 33 | const registerObserver = (params = { shouldLog: false }, callback) => { 34 | if (window.PerformanceObserver) { 35 | const { shouldLog, timeout = 2000 } = params 36 | const observer = new window.PerformanceObserver(list => { 37 | const entries = list.getEntries() 38 | 39 | const generatedMeasures = generateDataFromMeasures( 40 | getReactPerformanceData(entries) 41 | ) 42 | const measures = 43 | typeof components !== 'undefined' && Array.isArray(components) 44 | ? getMeasuresByComponentNames(components, generatedMeasures) 45 | : generatedMeasures 46 | 47 | if (typeof callback === 'function') callback(measures) 48 | window.__REACT_PERF_DEVTOOL_GLOBAL_STORE__ = { 49 | measures, 50 | timeout, 51 | length: entries.length, 52 | rawMeasures: entries 53 | } 54 | if (shouldLog) logToConsole(params, measures) 55 | }) 56 | observer.observe({ entryTypes: ['measure'] }) 57 | return observer 58 | } 59 | return undefined 60 | } 61 | 62 | /** 63 | This function logs the measures to the console. Requires a server running on a specified port. Default port number is 8080. 64 | */ 65 | const logToConsole = ({ port }, measures) => { 66 | measures.forEach( 67 | ({ 68 | componentName, 69 | mount, 70 | render, 71 | update, 72 | unmount, 73 | totalTimeSpent, 74 | percentTimeSpent, 75 | numberOfInstances, 76 | componentWillMount, 77 | componentDidMount, 78 | componentWillReceiveProps, 79 | shouldComponentUpdate, 80 | componentWillUpdate, 81 | componentDidUpdate, 82 | componentWillUnmount 83 | }) => { 84 | // The time is in millisecond (ms) 85 | const data = { 86 | component: componentName, 87 | mount, 88 | render, 89 | update, 90 | unmount, 91 | totalTimeSpent, 92 | percentTimeSpent, 93 | numberOfInstances, 94 | componentWillMount, 95 | componentDidMount, 96 | componentWillReceiveProps, 97 | shouldComponentUpdate, 98 | componentWillUpdate, 99 | componentDidUpdate, 100 | componentWillUnmount 101 | } 102 | 103 | send(data, port) 104 | } 105 | ) 106 | } 107 | 108 | // Send the data to a specified port 109 | const send = (data, port) => { 110 | const normalizedPort = 111 | port !== undefined && typeof port === 'number' ? port : 8080 112 | try { 113 | window.navigator.sendBeacon( 114 | `http://127.0.0.1:${normalizedPort}`, 115 | JSON.stringify(data, null, 2) 116 | ) 117 | } catch (err) { 118 | console.error(`Failed to send data to port ${normalizedPort}`) 119 | } 120 | } 121 | 122 | const getMeasuresByComponentNames = (componentNames, measures) => 123 | measures.filter(measure => 124 | componentNames.some(componentName => 125 | new Regex(componentName).test(measure.componentName) 126 | ) 127 | ) 128 | 129 | export { registerObserver, getMeasuresByComponentNames } 130 | -------------------------------------------------------------------------------- /src/shared/commitChanges.js: -------------------------------------------------------------------------------- 1 | import { add } from './math' 2 | 3 | const COMMITTING_CHANGES = 'Committing Changes' 4 | const LIFE_CYCLE_NOTICE = '⚛' 5 | const RECONCILIATION_NOTICE = '⛔' 6 | 7 | const getMeasurmentsByDelimiter = (measurments, delimiter) => 8 | measurments 9 | .filter( 10 | measurment => 11 | measurment.name.includes(delimiter) && 12 | measurment.name 13 | .split(delimiter) 14 | .join('') 15 | .includes(COMMITTING_CHANGES) 16 | ) 17 | .map(measurment => Number(measurment.duration.toFixed(2))) 18 | 19 | const getCommitChangesTime = measurements => ({ 20 | [COMMITTING_CHANGES]: { 21 | timeSpent: [ 22 | ...getMeasurmentsByDelimiter(measurements, LIFE_CYCLE_NOTICE), 23 | ...getMeasurmentsByDelimiter(measurements, RECONCILIATION_NOTICE) 24 | ] 25 | } 26 | }) 27 | 28 | export { getCommitChangesTime } 29 | -------------------------------------------------------------------------------- /src/shared/generate.js: -------------------------------------------------------------------------------- 1 | import { add, average, percent } from './math' 2 | 3 | // Get the total time taken combining all the phases 4 | const getSummarisedTotalTime = components => 5 | components.reduce((acc, component) => (acc += component.totalTime), 0) 6 | 7 | // Plot the timings (average time in ms, component instances, total time in ms) 8 | const plotTimings = nums => ({ 9 | averageTimeSpentMs: average(nums), 10 | numberOfTimes: nums.length, 11 | totalTimeSpentMs: add(nums) 12 | }) 13 | 14 | // Create a schema for each component 15 | const createSchema = (store, component, totalTime) => ({ 16 | componentName: component.name, 17 | totalTimeSpent: component.totalTime, 18 | numberOfInstances: 19 | store[component.name].mount.timeSpent.length - 20 | store[component.name].unmount.timeSpent.length, 21 | percentTimeSpent: percent(component.totalTime / totalTime), 22 | render: plotTimings(store[component.name].render.timeSpent), 23 | mount: plotTimings(store[component.name].mount.timeSpent), 24 | update: plotTimings(store[component.name].update.timeSpent), 25 | unmount: plotTimings(store[component.name].unmount.timeSpent), 26 | componentWillMount: plotTimings( 27 | store[component.name].componentWillMount.timeSpent 28 | ), 29 | componentDidMount: plotTimings( 30 | store[component.name].componentDidMount.timeSpent 31 | ), 32 | componentWillReceiveProps: plotTimings( 33 | store[component.name].componentWillReceiveProps.timeSpent 34 | ), 35 | shouldComponentUpdate: plotTimings( 36 | store[component.name].shouldComponentUpdate.timeSpent 37 | ), 38 | componentWillUpdate: plotTimings( 39 | store[component.name].componentWillUpdate.timeSpent 40 | ), 41 | componentDidUpdate: plotTimings( 42 | store[component.name].componentDidUpdate.timeSpent 43 | ), 44 | componentWillUnmount: plotTimings( 45 | store[component.name].componentWillUnmount.timeSpent 46 | ) 47 | }) 48 | 49 | const getTotalComponentTimeSpent = componentPhases => { 50 | const phases = [ 51 | 'mount', 52 | 'unmount', 53 | 'update', 54 | 'render', 55 | 'componentWillMount', 56 | 'componentDidMount', 57 | 'componentWillReceiveProps', 58 | 'shouldComponentUpdate', 59 | 'componentWillUpdate', 60 | 'componentDidUpdate', 61 | 'componentWillUnmount' 62 | ] 63 | return phases.reduce( 64 | (totalTimeSpent, phase) => 65 | (totalTimeSpent += add(componentPhases[phase].timeSpent)), 66 | 0 67 | ) 68 | } 69 | 70 | const generateDataFromMeasures = store => { 71 | const componentsWithTotalTime = Object.keys(store).map(componentName => ({ 72 | name: componentName, 73 | totalTime: getTotalComponentTimeSpent(store[componentName]) 74 | })) 75 | const totalTime = getSummarisedTotalTime(componentsWithTotalTime) 76 | return componentsWithTotalTime 77 | .map(componentWithTotalTime => 78 | createSchema(store, componentWithTotalTime, totalTime) 79 | ) 80 | .sort((a, b) => b.totalTimeSpent - a.totalTimeSpent) 81 | } 82 | 83 | export { generateDataFromMeasures } 84 | -------------------------------------------------------------------------------- /src/shared/hostEffects.js: -------------------------------------------------------------------------------- 1 | import { add } from './math' 2 | 3 | const COMMITTING_HOST_EFFECTS = 'Committing Host Effects' 4 | 5 | // Update the store with the time spent while committing host effects 6 | const getCommitHostEffectsTime = measures => { 7 | const measurementsByMatchingName = measures.filter( 8 | measure => 9 | measure.name.includes('⚛') && 10 | measure.name 11 | .split('⚛ ') 12 | .join('') 13 | .includes(COMMITTING_HOST_EFFECTS) 14 | ) 15 | 16 | return { 17 | [COMMITTING_HOST_EFFECTS]: { 18 | totalEffects: measurementsByMatchingName.map( 19 | measurementByMatchingName => { 20 | const measurementName = measurementByMatchingName.name 21 | .split('⚛ ') 22 | .join('') 23 | const effectValue = measurementName.split(':')[1].split(' ')[1] 24 | return Number(effectValue) 25 | } 26 | ), 27 | timeSpent: measurementsByMatchingName.map(measurementByMatchingName => { 28 | const durationValue = measurementByMatchingName.duration.toFixed(2) 29 | return Number(durationValue) 30 | }) 31 | } 32 | } 33 | } 34 | 35 | // Get total number of host effects 36 | const getTotalEffects = store => 37 | Number(add(store[COMMITTING_HOST_EFFECTS].totalEffects)) 38 | 39 | export { getTotalEffects, getCommitHostEffectsTime } 40 | -------------------------------------------------------------------------------- /src/shared/lifecycle.js: -------------------------------------------------------------------------------- 1 | import { add } from './math' 2 | 3 | const CALLING_LIFECYCLE_METHODS = 'Calling Lifecycle Methods' 4 | const getLifecycleTime = measures => { 5 | const measurementsByName = measures.filter( 6 | measure => 7 | measure.name.includes('⚛') && 8 | measure.name 9 | .split('⚛ ') 10 | .join('') 11 | .includes(CALLING_LIFECYCLE_METHODS) 12 | ) 13 | return { 14 | [CALLING_LIFECYCLE_METHODS]: { 15 | totalMethods: measurementsByName.map(measurementByName => { 16 | const methodValue = measurementByName.name.split(':')[1].split(' ')[1] 17 | return Number(methodValue) 18 | }), 19 | timeSpent: measurementsByName.map(measurementByName => 20 | Number(measurementByName.duration.toFixed(2)) 21 | ) 22 | } 23 | } 24 | } 25 | const getTotalMethods = store => 26 | Number(add(store[CALLING_LIFECYCLE_METHODS].totalMethods)) 27 | 28 | export { getTotalMethods, getLifecycleTime } 29 | -------------------------------------------------------------------------------- /src/shared/math.js: -------------------------------------------------------------------------------- 1 | function add(nums) { 2 | return Number(nums.reduce((acc, v) => (acc += v), 0).toFixed(2)) 3 | } 4 | 5 | function average(nums) { 6 | if (nums.length === 0) { 7 | return '-' 8 | } 9 | return (nums.reduce((acc, v) => (acc += v), 0) / nums.length).toFixed(2) 10 | } 11 | 12 | function percent(num) { 13 | return Math.round(num * 100) + '%' 14 | } 15 | 16 | export { add, average, percent } 17 | -------------------------------------------------------------------------------- /src/shared/parse.js: -------------------------------------------------------------------------------- 1 | import { getComponentAndPhaseName } from './parseMeasures' 2 | 3 | // Schema for storing the time duration of each phase of a React component 4 | const createSchema = () => ({ 5 | // Phases 6 | mount: { 7 | timeSpent: [] 8 | }, 9 | unmount: { 10 | timeSpent: [] 11 | }, 12 | update: { 13 | timeSpent: [] 14 | }, 15 | render: { 16 | timeSpent: [] 17 | }, 18 | 19 | // Lifecycle hooks 20 | componentWillMount: { 21 | timeSpent: [] 22 | }, 23 | componentDidMount: { 24 | timeSpent: [] 25 | }, 26 | componentWillReceiveProps: { 27 | timeSpent: [] 28 | }, 29 | shouldComponentUpdate: { 30 | timeSpent: [] 31 | }, 32 | componentWillUpdate: { 33 | timeSpent: [] 34 | }, 35 | componentDidUpdate: { 36 | timeSpent: [] 37 | }, 38 | componentWillUnmount: { 39 | timeSpent: [] 40 | } 41 | }) 42 | 43 | // Update the time duration of each phase 44 | const updateTime = (store, componentName, phase, measure) => { 45 | if (phase === '[mount]') { 46 | store[componentName].mount.timeSpent.push(measure.duration) 47 | } 48 | 49 | if (phase === '[unmount]') { 50 | store[componentName].unmount.timeSpent.push(measure.duration) 51 | } 52 | 53 | if (phase === '[update]') { 54 | store[componentName].update.timeSpent.push(measure.duration) 55 | } 56 | 57 | if (phase === '[render]') { 58 | store[componentName].render.timeSpent.push(measure.duration) 59 | } 60 | 61 | if (phase === 'componentWillMount') { 62 | store[componentName].componentWillMount.timeSpent.push(measure.duration) 63 | } 64 | 65 | if (phase === 'componentWillUnmount') { 66 | store[componentName].componentWillUnmount.timeSpent.push(measure.duration) 67 | } 68 | 69 | if (phase === 'componentDidMount') { 70 | store[componentName].componentDidMount.timeSpent.push(measure.duration) 71 | } 72 | 73 | if (phase === 'componentWillReceiveProps') { 74 | store[componentName].componentWillReceiveProps.timeSpent.push( 75 | measure.duration 76 | ) 77 | } 78 | 79 | if (phase === 'shouldComponentUpdate') { 80 | store[componentName].shouldComponentUpdate.timeSpent.push(measure.duration) 81 | } 82 | 83 | if (phase === 'componentWillUpdate') { 84 | store[componentName].componentWillUpdate.timeSpent.push(measure.duration) 85 | } 86 | 87 | if (phase === 'componentDidUpdate') { 88 | store[componentName].componentDidUpdate.timeSpent.push(measure.duration) 89 | } 90 | } 91 | 92 | // Get data from the performance measures 93 | const getReactPerformanceData = measures => { 94 | const store = {} 95 | measures 96 | .filter(measure => getComponentAndPhaseName(measure) !== null) 97 | .forEach(measure => { 98 | const { componentName, phase } = getComponentAndPhaseName(measure) 99 | if (!store[componentName]) { 100 | store[componentName] = createSchema() 101 | } 102 | 103 | updateTime(store, componentName, phase, measure) 104 | }) 105 | return store 106 | } 107 | 108 | export { getReactPerformanceData } 109 | -------------------------------------------------------------------------------- /src/shared/parseMeasures.js: -------------------------------------------------------------------------------- 1 | /** 2 | This function returns the component name and phase name from the measure name (returned from React performance data). 3 | This may break if React changes the mark name (measure name). 4 | */ 5 | const getComponentAndPhaseName = measure => { 6 | if (measure.name.includes('⚛')) { 7 | let index = measure.name.split('⚛ ').join('') 8 | 9 | // "App [mount]" 10 | if (/\[\w+\]/.test(index)) { 11 | const [componentName, phase] = index.split(' ') 12 | return { 13 | componentName, 14 | phase 15 | } 16 | } else if (/\w+\.\w+/.test(index)) { 17 | // App.componentWillMount 18 | const [componentName, lifecycle] = index.split('.') 19 | return { 20 | componentName, 21 | phase: lifecycle 22 | } 23 | } else { 24 | return null 25 | } 26 | } else if (measure.name.includes('⛔')) { 27 | let index = measure.name.split('⛔ ').join('') 28 | 29 | if (/\w+\.\w+/.test(index)) { 30 | return { 31 | componentName: index.split('.')[0], 32 | phase: index.split('.')[1].split(' Warning:')[0] 33 | } 34 | } else { 35 | return null 36 | } 37 | } 38 | 39 | return null 40 | } 41 | 42 | export { getComponentAndPhaseName } 43 | -------------------------------------------------------------------------------- /src/shared/totalTime.js: -------------------------------------------------------------------------------- 1 | import { add } from './math' 2 | 3 | // Computes the total time of each measure (commit changes, host effects and lifecycle methods) 4 | const getTotalTime = store => { 5 | let totalTime = 0 6 | 7 | for (const measure in store) { 8 | totalTime = totalTime || 0 9 | totalTime += add(store[measure].timeSpent) 10 | } 11 | 12 | return totalTime 13 | } 14 | 15 | export { getTotalTime } 16 | -------------------------------------------------------------------------------- /webpack/webpack.config.hook.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | const UglifyJSPlugin = require('uglifyjs-webpack-plugin') 4 | 5 | const getMode = () => 6 | process.env.NODE_ENV === 'production' ? 'production' : 'development' 7 | 8 | const output = () => ({ 9 | filename: 'devtool-hook.js', 10 | path: path.resolve(__dirname, '../lib'), 11 | publicPath: '/', 12 | libraryTarget: 'umd' 13 | }) 14 | 15 | const externals = () => ({ 16 | react: 'react' 17 | }) 18 | 19 | const jsLoader = () => ({ 20 | test: /\.js$/, 21 | include: [ 22 | path.resolve(__dirname, '../src/npm'), 23 | path.resolve(__dirname, '../src/shared') 24 | ], 25 | exclude: [ 26 | path.resolve(__dirname, '../src/extension'), 27 | 'node_modules', 28 | 'samples', 29 | 'art', 30 | 'extension' 31 | ], 32 | use: 'babel-loader' 33 | }) 34 | 35 | const plugins = () => [ 36 | new webpack.LoaderOptionsPlugin({ 37 | minimize: true, 38 | debug: false 39 | }), 40 | new webpack.DefinePlugin({ 41 | 'process.env.NODE_ENV': JSON.stringify('production') 42 | }), 43 | new webpack.optimize.ModuleConcatenationPlugin(), 44 | new UglifyJSPlugin() 45 | ] 46 | 47 | module.exports = { 48 | entry: path.resolve(__dirname, '../src/hook.js'), 49 | output: output(), 50 | target: 'web', 51 | mode: getMode(), 52 | externals: externals(), 53 | module: { 54 | rules: [jsLoader()] 55 | }, 56 | plugins: plugins() 57 | } 58 | -------------------------------------------------------------------------------- /webpack/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | const UglifyJSPlugin = require('uglifyjs-webpack-plugin') 4 | 5 | const getMode = () => 6 | process.env.NODE_ENV === 'production' ? 'production' : 'development' 7 | 8 | const output = () => ({ 9 | filename: 'ReactPerfDevtool.js', 10 | path: path.resolve(__dirname, '../extension/build'), 11 | publicPath: '/', 12 | libraryTarget: 'umd' 13 | }) 14 | 15 | const externals = () => ({ 16 | react: 'react' 17 | }) 18 | 19 | const jsLoader = () => ({ 20 | test: /\.js$/, 21 | include: path.resolve(__dirname, '../src'), 22 | exclude: ['node_modules'], 23 | use: 'babel-loader' 24 | }) 25 | 26 | const plugins = () => [ 27 | new webpack.LoaderOptionsPlugin({ 28 | minimize: true, 29 | debug: false 30 | }), 31 | new webpack.DefinePlugin({ 32 | 'process.env.NODE_ENV': JSON.stringify('production') 33 | }), 34 | new webpack.optimize.ModuleConcatenationPlugin(), 35 | new UglifyJSPlugin() 36 | ] 37 | 38 | module.exports = { 39 | entry: path.resolve(__dirname, '../src/index.js'), 40 | output: output(), 41 | target: 'web', 42 | mode: getMode(), 43 | externals: externals(), 44 | module: { 45 | rules: [jsLoader()] 46 | }, 47 | plugins: plugins() 48 | } 49 | --------------------------------------------------------------------------------