mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Merge remote-tracking branch 'upstream/master' into act-async
This commit is contained in:
+2
-1
@@ -6,6 +6,7 @@ shells/browser/firefox/build
|
||||
shells/browser/shared/build
|
||||
shells/dev/dist
|
||||
vendor
|
||||
*.js.snap
|
||||
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
yarn.lock
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
},
|
||||
"globals": {
|
||||
"__DEV__": "readonly",
|
||||
"jasmine": "readonly"
|
||||
"jasmine": "readonly",
|
||||
"spyOn": "readonly"
|
||||
}
|
||||
}
|
||||
|
||||
+35
-154
@@ -14,9 +14,9 @@ The old DevTools also rendered the entire application tree in the form of a larg
|
||||
|
||||
Every React commit that changes the tree in a way DevTools cares about results in an "_operations_" message being sent across the bridge. These messages are lightweight patches that describe the changes that were made. (We don't resend the full tree structure like in legacy DevTools.)
|
||||
|
||||
The payload for each message is a typed array. The first two entries are numbers that identify which renderer and root the update belongs to (for multi-root support). Then the strings are encoded in a string table. The rest of the array depends on the operations being made to the tree.
|
||||
The payload for each message is a typed array. The first two entries are numbers that identify which renderer and root the update belongs to (for multi-root support). Then the strings are encoded in a [string table](#string-table). The rest of the array depends on the operations being made to the tree.
|
||||
|
||||
No updates are required for many commits because we only send the following bits of information: element type, id, parent id, owner id, name, and key. Additional information (e.g. props, state) requires a separate "_inspectElement_" message.
|
||||
No updates are required for most commits because we only send the following bits of information: element type, id, parent id, owner id, name, and key. Additional information (e.g. props, state) requires a separate ["_inspectElement_" message](#inspecting-an-element).
|
||||
|
||||
#### String table
|
||||
|
||||
@@ -211,13 +211,39 @@ while (index !== currentWeight) {
|
||||
}
|
||||
```
|
||||
|
||||
## Inspecting an element
|
||||
|
||||
When an element is mounted in the tree, DevTools sends a minimal amount of information about it across the bridge. This information includes its display name, type, and key- but does _not_ include things like props or state. (These values are often expensive to serialize and change frequently, which would add a significant amount of load to the bridge.)
|
||||
|
||||
Instead DevTools lazily requests additional information about an element only when it is selected in the "Components" tab. At that point, the frontend requests this information by sending a special "_inspectElement_" message containing the id of the element being inspected. The backend then responds with an "_inspectedElement_" message containing the additional details.
|
||||
|
||||
### Polling strategy
|
||||
|
||||
Elements can update frequently, especially in response to things like scrolling events. Since props and state can be large, we avoid sending this information across the bridge every time the selected element is updated. Instead, the frontend polls the backend for updates about once a second. The backend tracks when the element was last "inspected" and sends a special no-op response if it has not re-rendered since then.
|
||||
|
||||
### Inspecting hooks
|
||||
|
||||
Hooks present a unique challenge for the DevTools because of the concept of _custom_ hooks. (A custom hook is essentially any function that calls at least one of the built-in hooks. By convention custom hooks also have names that begin with "use".)
|
||||
|
||||
So how does DevTools identify custom functions called from within third party components? It does this by temporarily overriding React's built-in hooks and shallow rendering the component in question. Whenever one of the (overridden) built-in hooks are called, it parses the call stack to spot potential custom hooks (functions between the component itself and the built-in hook). This approach enables it to build a tree structure describing all of the calls to both the built-in _and_ custom hooks, along with the values passed to those hooks. (If you're interested in learning more about this, [here is the source code](https://github.com/bvaughn/react-devtools-experimental/blob/master/src/backend/ReactDebugHooks.js).)
|
||||
|
||||
> **Note**: DevTools obtains hooks info by re-rendering a component. Breakpoints and console logs will be invoked during this additional (shallow) render.
|
||||
|
||||
### Performance implications
|
||||
|
||||
To mitigate the performance impact of re-rendering a component, DevTools does the following:
|
||||
* Only function components that use _at least one hook_ are rendered. (Props and state can be analyzed without rendering.)
|
||||
* Rendering is always shallow.
|
||||
* Rendering is throttled to occur, at most, once per second.
|
||||
* Rendering is skipped if the component has not updated since the last time its properties were inspected.
|
||||
|
||||
## Profiler
|
||||
|
||||
The Profiler UI is a powerful tool for identifying and fixing performance problems. The primary goal of the new profiler is to minimize its impact (CPU usage) while profiling is active. This can be accomplished by:
|
||||
* Minimizing bridge traffic.
|
||||
* Making expensive computations lazy.
|
||||
|
||||
Profiling information is stored on the backend. The backend push-notifies the frontend of when profiling starts or stops by sending a "_profilingStatus_" message. (The frontend also asks for the current status after mounting by sending a "_getProfilingStatus_" message.)
|
||||
The majority of profiling information is stored on the backend. The backend push-notifies the frontend of when profiling starts or stops by sending a "_profilingStatus_" message. (The frontend also asks for the current status after mounting by sending a "_getProfilingStatus_" message.)
|
||||
|
||||
When profiling begins, the frontend takes a snapshot/copy of each root. This snapshot includes the id, name, key, and child IDs for each node in the tree. (This information is already present on the frontend, so it does not require any additional bridge traffic.) While profiling is active, each time React commits– the frontend also stores a copy of the "_operations_" message (described above). Once profiling has finished, the frontend can use the original snapshot along with each of the stored "_operations_" messages to reconstruct the tree for each of the profiled commits.
|
||||
|
||||
@@ -226,161 +252,16 @@ When profiling begins, the backend records the base durations of each fiber curr
|
||||
* Which elements were rendered during that commit.
|
||||
* Which interactions (if any) were part of the commit.
|
||||
|
||||
This information is kept on the backend until requested by the frontend (as described below).
|
||||
This information will eventually be required by the frontend in order to render its profiling graphs, but it will not be sent across the bridge until profiling has completed (to minimize the performance impact of profiling).
|
||||
|
||||
<sup>1</sup> In the future, the backend may also store additional metadata (e.g. which props/states changed between rendered for a given component).
|
||||
|
||||
### Profiling summary
|
||||
### Combining profiling data
|
||||
|
||||
The profiling tab shows information for the currently-selected React root. When profiling completes (or when a new root is selected) the frontend first checks to see if there is any profiling data for the selected root. (Has it cached any "_operations_"?)
|
||||
Once profiling is finished, the frontend requests profiling data from the backend one renderer at a time by sending a "_getProfilingData_" message. The backend responds with a "_profilingData_" message that contains per-root commit timing and duration information. The frontend then combines this information with its own snapshots to form a complete picture of the profiling session. Using this data, charts and graphs are lazily computed (and incrementally cached) on demand, based on which commits and views are selected in the Profiler UI.
|
||||
|
||||
If so, then it sends a "_getProfilingSummary_" message with an id that identifies the root. The backend then returns a "_profilingSummary_" message with the following information:
|
||||
### Importing/exporting data
|
||||
|
||||
* root id (to match request and response)
|
||||
* number of interactions that were traced for this root
|
||||
* the commits (each consisting of a timestamp and duration) that were profiled for the root
|
||||
* tree base durations as of when profiling started
|
||||
Because all of the data is merged in the frontend after a profiling session is completed, it can be exported and imported (as JSON), enabling profiling sessions to be shared between users.
|
||||
|
||||
This is the minimal information required to render the main ["commit selector"](https://reactjs.org/blog/2018/09/10/introducing-the-react-profiler.html#browsing-commits).
|
||||
|
||||
Here is an example profile summary:
|
||||
```js
|
||||
{
|
||||
rootID: 1,
|
||||
interactionCount: 2,
|
||||
|
||||
// Commit durations
|
||||
commitDurations: [
|
||||
10, // first commit took 10ms
|
||||
13, // second commit took 13ms
|
||||
5, // third commit took 5ms
|
||||
]
|
||||
|
||||
// Commit times (relative to when profiling started)
|
||||
commitTimes: [
|
||||
210, // first commit started 210ms after profiling began
|
||||
284, // second commit started 284ms after profiling began
|
||||
303, // third commit started 303ms after profiling began
|
||||
],
|
||||
|
||||
// Tuples of fiber id and initial tree base duration
|
||||
initialTreeBaseDurations: [
|
||||
1, // fiber id
|
||||
11, // tree base duration when profiling started
|
||||
|
||||
2, // fiber id
|
||||
12, // tree base duration when profiling started
|
||||
|
||||
3, // fiber id
|
||||
8, // tree base duration when profiling started
|
||||
]
|
||||
]
|
||||
```
|
||||
|
||||
Additional information (e.g. which components were part of a specific commit, which interactions were logged) are lazily requested by the frontend as a user interacts with the Profiler UI.
|
||||
|
||||
### Commit details
|
||||
|
||||
When a commit is selected in the profiling view, the frontend needs to reconstruct the tree at that point in time using the snapshot and the "_operations_" it has cached.
|
||||
|
||||
In addition to this, it also needs to ask the backend for some additional information needed to display the ["flame chart"](https://reactjs.org/blog/2018/09/10/introducing-the-react-profiler.html#flame-chart) and ["ranked chart"](https://reactjs.org/blog/2018/09/10/introducing-the-react-profiler.html#ranked-chart) views. The frontend sends a "_profileCommitDetails_" message specifying which root and commit (index) it is interested in. The backend sends a response to fill in missing details about the commit:
|
||||
|
||||
* root id and commit index (to match request and response)
|
||||
* which elements were rendered during the commit <sup>1</sup> and how long did they take
|
||||
* which interactions were part of the commit
|
||||
|
||||
Here is an example commit in which two elements were rendered and one interaction was traced:
|
||||
|
||||
```js
|
||||
{
|
||||
rootID: 1,
|
||||
commitIndex: 0,
|
||||
interactions: [
|
||||
{
|
||||
id: 8,
|
||||
timestamp: 4,
|
||||
name: "Foo"
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
timestamp: 4,
|
||||
name: "Bar"
|
||||
}
|
||||
],
|
||||
nodes: [
|
||||
{
|
||||
id: 1,
|
||||
baseDuration: 15,
|
||||
actualDuration: 15
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
baseDuration: 11,
|
||||
actualDuration: 11
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<sup>1</sup> Elements in the tree that are not explicitly included in the above response were not rendered during the current commit.
|
||||
|
||||
### Component commits
|
||||
|
||||
When a particular component (fiber) is selected, the frontend polls the backend for the aggregate data required to render the ["component chart"](https://reactjs.org/blog/2018/09/10/introducing-the-react-profiler.html#component-chart) view. The frontend sends a "_profileComponentDetails_" message specifying which root and component (id) it is interested in. The backend sends a response that includes:
|
||||
|
||||
* root and component ids (to match request and response)
|
||||
* which commits was the component rendered in and how long did each take
|
||||
|
||||
Here is an example of a component that committed twice during a profiling session:
|
||||
|
||||
```js
|
||||
{
|
||||
rootID: 1,
|
||||
id: 2,
|
||||
|
||||
// Tuples of commit index and render duration (ms)
|
||||
commits: [
|
||||
0, // index of first
|
||||
11 // duration (ms)
|
||||
|
||||
2, // index of second commit
|
||||
7 // duration (ms)
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Interactions
|
||||
|
||||
The [Interactions chart](https://reactjs.org/blog/2018/09/10/introducing-the-react-profiler.html#interactions) shows a time series for every interaction that was traced in the recent profiler session. The frontend sends a "_profileInteractions_" message specifying which root it would like interaction data for. The backend sends the following response:
|
||||
|
||||
* root id (to match request and response)
|
||||
* interaction metadata
|
||||
|
||||
Here is an example of a profiling session consisting of two interactions:
|
||||
|
||||
```js
|
||||
{
|
||||
rootID: 1,
|
||||
interactions: [
|
||||
{
|
||||
id: 8,
|
||||
name: "Foo",
|
||||
timestamp: 4,
|
||||
commits: [
|
||||
0, // index of first commit
|
||||
2 // index of second commit
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
name: "Bar",
|
||||
timestamp: 4,
|
||||
commits: [
|
||||
0 // index of first commit
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The backend does not need to resend the timestamp for each of the commits because that was already sent as part of the "_profilingSummary_" message.
|
||||
At the moment, screenshots are not included in the exported data (to keep the export filesize small) but this could be changed in the future.
|
||||
+9
-9
@@ -24,6 +24,7 @@
|
||||
"<rootDir>/src/__tests__/setupTests"
|
||||
],
|
||||
"snapshotSerializers": [
|
||||
"<rootDir>/src/__tests__/inspectedElementSerializer",
|
||||
"<rootDir>/src/__tests__/storeSerializer"
|
||||
],
|
||||
"testMatch": [
|
||||
@@ -78,7 +79,7 @@
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"@reach/menu-button": "^0.1.17",
|
||||
"@reach/tooltip": "^0.2.0",
|
||||
"adm-zip": "^0.4.7",
|
||||
"archiver": "^3.0.0",
|
||||
"babel-core": "^7.0.0-bridge",
|
||||
"babel-eslint": "^9.0.0",
|
||||
"babel-jest": "^24.7.1",
|
||||
@@ -89,7 +90,6 @@
|
||||
"cli-spinners": "^1.0.0",
|
||||
"clipboard-js": "^0.3.6",
|
||||
"cross-env": "^5.2.0",
|
||||
"crx": "git+https://github.com/oncletom/crx#ef150e8",
|
||||
"css-loader": "^1.0.1",
|
||||
"error-stack-parser": "^2.0.2",
|
||||
"es6-symbol": "3.0.2",
|
||||
@@ -131,21 +131,21 @@
|
||||
"opener": "^1.5.1",
|
||||
"prettier": "^1.16.4",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^0.0.0-6da04b5d8",
|
||||
"react": "^0.0.0-50b50c26f",
|
||||
"react-color": "^2.11.7",
|
||||
"react-dom": "^0.0.0-6da04b5d8",
|
||||
"react-is": "^0.0.0-6da04b5d8",
|
||||
"react-test-renderer": "^0.0.0-6da04b5d8",
|
||||
"react-dom": "^0.0.0-50b50c26f",
|
||||
"react-is": "^0.0.0-50b50c26f",
|
||||
"react-test-renderer": "^0.0.0-50b50c26f",
|
||||
"react-virtualized-auto-sizer": "^1.0.2",
|
||||
"react-window": "^1.8.0",
|
||||
"request-promise": "^4.2.4",
|
||||
"scheduler": "^0.0.0-6da04b5d8",
|
||||
"rimraf": "^2.6.3",
|
||||
"scheduler": "^0.0.0-50b50c26f",
|
||||
"semver": "^5.5.1",
|
||||
"style-loader": "^0.23.1",
|
||||
"web-ext": "^3.0.0",
|
||||
"webpack": "^4.26.0",
|
||||
"webpack-cli": "^3.1.2",
|
||||
"webpack-dev-server": "^3.3.1",
|
||||
"xml-js": "^1.6.11"
|
||||
"webpack-dev-server": "^3.3.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +1,10 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const chalk = require('chalk');
|
||||
const { execSync } = require('child_process');
|
||||
const { join } = require('path');
|
||||
const rp = require('request-promise');
|
||||
const convert = require('xml-js');
|
||||
const build = require('../shared/build');
|
||||
|
||||
const main = async () => {
|
||||
const manifestVersion = await rp(
|
||||
'https://react-devtools-experimental-chrome.now.sh/updates.xml'
|
||||
)
|
||||
.then(xmlString => {
|
||||
const parsedXML = convert.xml2js(xmlString);
|
||||
const version =
|
||||
parsedXML.elements[0].elements[0].elements[0].attributes.version;
|
||||
const match = /(\d)\.(\d)\.(\d)\.*(\d)*/.exec(version);
|
||||
if (match !== null) {
|
||||
const prerelease = parseInt(match[4], 10) || 0;
|
||||
return `${match[1]}.${match[2]}.${match[3]}.${prerelease + 1}`;
|
||||
}
|
||||
})
|
||||
.catch(error => null);
|
||||
|
||||
await build('chrome', manifestVersion);
|
||||
|
||||
const cwd = join(__dirname, 'build');
|
||||
execSync('crx pack ./unpacked -o ReactDevTools.crx -p ../../../../key.pem', {
|
||||
cwd,
|
||||
});
|
||||
execSync('rm packed.zip', { cwd });
|
||||
await build('chrome');
|
||||
|
||||
console.log(chalk.green('\nThe Chrome extension has been built!'));
|
||||
console.log(chalk.green('You can test this build by running:'));
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
"description": "Adds React debugging tools to the Chrome Developer Tools.",
|
||||
"version": "4.0.0",
|
||||
|
||||
"update_url": "https://react-devtools-experimental-chrome.now.sh/updates.xml",
|
||||
|
||||
"minimum_chrome_version": "49",
|
||||
|
||||
"icons": {
|
||||
@@ -44,8 +42,8 @@
|
||||
"permissions": [
|
||||
"<all_urls>",
|
||||
"background",
|
||||
"downloads",
|
||||
"tabs",
|
||||
"webNavigation",
|
||||
"file:///*",
|
||||
"http://*/*",
|
||||
"https://*/*"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "react-devtools-experimental-chrome",
|
||||
"alias": ["react-devtools-experimental-chrome"],
|
||||
"files": ["index.html", "updates.xml", "ReactDevTools.crx"]
|
||||
"files": ["index.html", "ReactDevTools.zip"]
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<gupdate xmlns='http://www.google.com/update2/response' protocol='2.0'>
|
||||
<app appid='akkpaehpnolchhkkfbedcdcikfdnjobk'>
|
||||
<updatecheck codebase='https://react-devtools-experimental-chrome.now.sh/ReactDevTools.crx' version='4.0.0' />
|
||||
</app>
|
||||
</gupdate>
|
||||
@@ -46,9 +46,9 @@
|
||||
|
||||
"permissions": [
|
||||
"<all_urls>",
|
||||
"downloads",
|
||||
"activeTab",
|
||||
"tabs",
|
||||
"webNavigation",
|
||||
"file:///*",
|
||||
"http://*/*",
|
||||
"https://*/*"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "react-devtools-experimental-firefox",
|
||||
"alias": ["react-devtools-experimental-firefox"],
|
||||
"files": ["index.html", "packed.zip"]
|
||||
"files": ["index.html", "ReactDevTools.zip"]
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const AdmZip = require('adm-zip');
|
||||
const archiver = require('archiver');
|
||||
const { execSync } = require('child_process');
|
||||
const { readFileSync, writeFileSync } = require('fs');
|
||||
const { readFileSync, writeFileSync, createWriteStream } = require('fs');
|
||||
const { copy, ensureDir, move, remove } = require('fs-extra');
|
||||
const { join } = require('path');
|
||||
const { getGitCommit } = require('../../utils');
|
||||
@@ -17,7 +17,7 @@ const preProcess = async (destinationPath, tempPath) => {
|
||||
await ensureDir(tempPath); // Create temp dir for this new build
|
||||
};
|
||||
|
||||
const build = async (tempPath, manifestPath, manifestVersion) => {
|
||||
const build = async (tempPath, manifestPath) => {
|
||||
const binPath = join(tempPath, 'bin');
|
||||
const zipPath = join(tempPath, 'zip');
|
||||
|
||||
@@ -62,23 +62,27 @@ const build = async (tempPath, manifestPath, manifestVersion) => {
|
||||
const commit = getGitCommit();
|
||||
|
||||
const manifest = JSON.parse(readFileSync(copiedManifestPath).toString());
|
||||
manifest.description += `\n\nCreated from revision ${commit}`;
|
||||
if (manifestVersion) {
|
||||
manifest.version = manifestVersion;
|
||||
manifest.version_name = `${manifestVersion} ${commit}`;
|
||||
}
|
||||
manifest.description += `\n\nCreated from revision ${commit} (${new Date().toLocaleDateString()})`;
|
||||
manifest.version_name = `${commit} (${new Date().toLocaleDateString()})`;
|
||||
writeFileSync(copiedManifestPath, JSON.stringify(manifest, null, 2));
|
||||
|
||||
// Pack the extension
|
||||
const zip = new AdmZip();
|
||||
zip.addLocalFolder(zipPath);
|
||||
zip.writeZip(join(tempPath, 'packed.zip'));
|
||||
const archive = archiver('zip', { zlib: { level: 9 } });
|
||||
const zipStream = createWriteStream(join(tempPath, 'ReactDevTools.zip'));
|
||||
await new Promise((resolve, reject) => {
|
||||
archive
|
||||
.directory(zipPath, false)
|
||||
.on('error', err => reject(err))
|
||||
.pipe(zipStream);
|
||||
archive.finalize();
|
||||
zipStream.on('close', () => resolve());
|
||||
});
|
||||
};
|
||||
|
||||
const postProcess = async (tempPath, destinationPath) => {
|
||||
const unpackedSourcePath = join(tempPath, 'zip');
|
||||
const packedSourcePath = join(tempPath, 'packed.zip');
|
||||
const packedDestPath = join(destinationPath, 'packed.zip');
|
||||
const packedSourcePath = join(tempPath, 'ReactDevTools.zip');
|
||||
const packedDestPath = join(destinationPath, 'ReactDevTools.zip');
|
||||
const unpackedDestPath = join(destinationPath, 'unpacked');
|
||||
|
||||
await move(unpackedSourcePath, unpackedDestPath); // Copy built files to destination
|
||||
@@ -86,7 +90,7 @@ const postProcess = async (tempPath, destinationPath) => {
|
||||
await remove(tempPath); // Clean up temp directory and files
|
||||
};
|
||||
|
||||
const main = async (buildId, manifestVersion) => {
|
||||
const main = async buildId => {
|
||||
const root = join(__dirname, '..', buildId);
|
||||
const manifestPath = join(root, 'manifest.json');
|
||||
const destinationPath = join(root, 'build');
|
||||
@@ -94,7 +98,7 @@ const main = async (buildId, manifestVersion) => {
|
||||
try {
|
||||
const tempPath = join(__dirname, 'build', buildId);
|
||||
await preProcess(destinationPath, tempPath);
|
||||
await build(tempPath, manifestPath, manifestVersion);
|
||||
await build(tempPath, manifestPath);
|
||||
|
||||
const builtUnpackedPath = join(destinationPath, 'unpacked');
|
||||
await postProcess(tempPath, destinationPath);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<ol>
|
||||
<li><a href="ReactDevTools.crx">download extension</a></li>
|
||||
<li>Navigate to <a href="chrome://extensions/">chrome://extensions/</a></li>
|
||||
<li><a href="ReactDevTools.zip">download extension</a></li>
|
||||
<li>Double-click to extract</li>
|
||||
<li>Navigate to <code>chrome://extensions/</code></li>
|
||||
<li>Enable "Developer mode"</li>
|
||||
<li>Drag <code>ReactDevTools.crx</code> into Chrome</li>
|
||||
<li>Choose "Add Extension" when prompted</li>
|
||||
<li>Click "LOAD UNPACKED"</li>
|
||||
<li>Select extracted extension folder (<code>ReactDevTools</code>)</li>
|
||||
</ol>
|
||||
@@ -1,7 +1,7 @@
|
||||
<ol>
|
||||
<li><a href="packed.zip">download extension</a></li>
|
||||
<li><a href="ReactDevTools.zip">download extension</a></li>
|
||||
<li>Extract/unzip</li>
|
||||
<li>Visit <code>about:debugging</code></li>
|
||||
<li>Click "Load Temporary Add-on"</li>
|
||||
<li>Select the <code>manifest.json</code></li>
|
||||
<li>Select the <code>manifest.json</code> file inside of the extracted extension folder (<code>ReactDevTools</code>)</li>
|
||||
</ol>
|
||||
@@ -3,7 +3,6 @@
|
||||
const { exec, execSync } = require('child_process');
|
||||
const { readFileSync, writeFileSync } = require('fs');
|
||||
const { join } = require('path');
|
||||
const convert = require('xml-js');
|
||||
|
||||
const main = async buildId => {
|
||||
const root = join(__dirname, '..', buildId);
|
||||
@@ -22,18 +21,6 @@ const main = async buildId => {
|
||||
cwd: root,
|
||||
});
|
||||
|
||||
if (buildId === 'chrome') {
|
||||
const file = readFileSync(join(buildPath, 'unpacked', 'manifest.json'));
|
||||
const json = JSON.parse(file);
|
||||
const { version } = json;
|
||||
|
||||
const xmlString = readFileSync(join(root, 'updates.xml'), 'utf8');
|
||||
const parsedXML = convert.xml2js(xmlString);
|
||||
parsedXML.elements[0].elements[0].elements[0].attributes.version = version;
|
||||
|
||||
writeFileSync(join(buildPath, 'updates.xml'), convert.js2xml(parsedXML));
|
||||
}
|
||||
|
||||
const file = readFileSync(join(root, 'now.json'));
|
||||
const json = JSON.parse(file);
|
||||
const alias = json.alias[0];
|
||||
|
||||
@@ -110,17 +110,6 @@ chrome.runtime.onMessage.addListener((request, sender) => {
|
||||
setIconAndPopup(reactBuildType, sender.tab.id);
|
||||
}
|
||||
|
||||
if (request.exportFile) {
|
||||
let { contents, filename } = request;
|
||||
if (!Array.isArray(contents)) {
|
||||
contents = [contents];
|
||||
}
|
||||
|
||||
const blob = new Blob(contents, { type: 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
chrome.downloads.download({ filename, saveAs: true, url });
|
||||
}
|
||||
|
||||
if (request.captureScreenshot) {
|
||||
const { commitIndex, rootID } = request;
|
||||
try {
|
||||
|
||||
@@ -51,6 +51,8 @@ function createPanelIfReactLoaded() {
|
||||
let bridge = null;
|
||||
let store = null;
|
||||
|
||||
let profilingData = null;
|
||||
|
||||
let componentsPortalContainer = null;
|
||||
let profilerPortalContainer = null;
|
||||
let settingsPortalContainer = null;
|
||||
@@ -85,13 +87,6 @@ function createPanelIfReactLoaded() {
|
||||
localStorage.setItem(LOCAL_STORAGE_SUPPORTS_PROFILING_KEY, 'true');
|
||||
chrome.devtools.inspectedWindow.eval('window.location.reload();');
|
||||
});
|
||||
bridge.addListener('exportFile', ({ contents, filename }) => {
|
||||
chrome.runtime.sendMessage({
|
||||
exportFile: true,
|
||||
contents,
|
||||
filename,
|
||||
});
|
||||
});
|
||||
bridge.addListener('captureScreenshot', ({ commitIndex, rootID }) => {
|
||||
chrome.runtime.sendMessage(
|
||||
{
|
||||
@@ -124,10 +119,10 @@ function createPanelIfReactLoaded() {
|
||||
store = new Store(bridge, {
|
||||
isProfiling,
|
||||
supportsCaptureScreenshots: true,
|
||||
supportsFileDownloads: browserName === 'Chrome',
|
||||
supportsReloadAndProfile: true,
|
||||
supportsProfiling,
|
||||
});
|
||||
store.profilerStore.profilingData = profilingData;
|
||||
|
||||
// Initialize the backend only once the Store has been initialized.
|
||||
// Otherwise the Store may miss important initial tree op codes.
|
||||
@@ -286,18 +281,29 @@ function createPanelIfReactLoaded() {
|
||||
|
||||
chrome.devtools.network.onNavigated.removeListener(checkPageForReact);
|
||||
|
||||
// Shutdown bridge and re-initialize DevTools panel when a new page is loaded.
|
||||
// Shutdown bridge before a new page is loaded.
|
||||
chrome.webNavigation.onBeforeNavigate.addListener(
|
||||
function onBeforeNavigate(details) {
|
||||
// `bridge.shutdown()` will remove all listeners we added, so we don't have to.
|
||||
bridge.shutdown();
|
||||
|
||||
profilingData = store.profilerStore.profilingData;
|
||||
}
|
||||
);
|
||||
|
||||
// Re-initialize DevTools panel when a new page is loaded.
|
||||
chrome.devtools.network.onNavigated.addListener(function onNavigated() {
|
||||
// Re-initialize saved filters on navigation,
|
||||
// since global values stored on window get reset in this case.
|
||||
initializeSavedComponentFilters();
|
||||
|
||||
// `bridge.shutdown()` will remove all listeners we added, so we don't have to.
|
||||
bridge.shutdown();
|
||||
|
||||
// It's easiest to recreate the DevTools panel (to clean up potential stale state).
|
||||
// We can revisit this in the future as a small optimization.
|
||||
flushSync(() => root.unmount(initBridgeAndStore));
|
||||
flushSync(() => {
|
||||
root.unmount(() => {
|
||||
initBridgeAndStore();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
@@ -6,8 +6,6 @@ import React, {
|
||||
lazy,
|
||||
memo,
|
||||
Component,
|
||||
// $FlowFixMe Flow thinks ConcurrentMode is stable
|
||||
unstable_ConcurrentMode as ConcurrentMode,
|
||||
Fragment,
|
||||
// $FlowFixMe Flow doesn't know about the Profiler import yet
|
||||
Profiler,
|
||||
@@ -48,15 +46,13 @@ export default function ElementTypes() {
|
||||
<Context.Consumer>{value => null}</Context.Consumer>
|
||||
</Context.Provider>
|
||||
<StrictMode>
|
||||
<ConcurrentMode>
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<ClassComponent />
|
||||
<FunctionComponent />
|
||||
<MemoFunctionComponent />
|
||||
<ForwardRefComponent />
|
||||
<LazyComponent />
|
||||
</Suspense>
|
||||
</ConcurrentMode>
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<ClassComponent />
|
||||
<FunctionComponent />
|
||||
<MemoFunctionComponent />
|
||||
<ForwardRefComponent />
|
||||
<LazyComponent />
|
||||
</Suspense>
|
||||
</StrictMode>
|
||||
</Fragment>
|
||||
</Profiler>
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
// @flow
|
||||
|
||||
import React, { Fragment, useCallback, useState } from 'react';
|
||||
import {
|
||||
unstable_IdlePriority as IdlePriority,
|
||||
unstable_LowPriority as LowPriority,
|
||||
unstable_runWithPriority as runWithPriority,
|
||||
} from 'scheduler';
|
||||
|
||||
export default function PriorityLevels() {
|
||||
const [defaultPriority, setDefaultPriority] = useState<boolean>(false);
|
||||
const [idlePriority, setIdlePriority] = useState<boolean>(false);
|
||||
const [normalPriority, setLowPriority] = useState<boolean>(false);
|
||||
|
||||
const resetSequence = useCallback(() => {
|
||||
setDefaultPriority(false);
|
||||
setLowPriority(false);
|
||||
setIdlePriority(false);
|
||||
}, []);
|
||||
|
||||
const startSequence = useCallback(() => {
|
||||
setDefaultPriority(true);
|
||||
runWithPriority(LowPriority, () => setLowPriority(true));
|
||||
runWithPriority(IdlePriority, () => setIdlePriority(true));
|
||||
}, []);
|
||||
|
||||
const labels = [];
|
||||
if (defaultPriority) {
|
||||
labels.push('(default priority)');
|
||||
}
|
||||
if (normalPriority) {
|
||||
labels.push('Low Priority');
|
||||
}
|
||||
if (idlePriority) {
|
||||
labels.push('Idle Priority');
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<h1>Priority Levels</h1>
|
||||
<button onClick={resetSequence}>Reset</button>
|
||||
<button onClick={startSequence}>Start sequence</button>
|
||||
<span>{labels.join(', ')}</span>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
+11
-5
@@ -3,28 +3,33 @@
|
||||
// This test harness mounts each test app as a separate root to test multi-root applications.
|
||||
|
||||
import { createElement } from 'react';
|
||||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
import {
|
||||
// $FlowFixMe Flow does not yet know about createRoot()
|
||||
unstable_createRoot as createRoot,
|
||||
} from 'react-dom';
|
||||
import DeeplyNestedComponents from './DeeplyNestedComponents';
|
||||
import EditableProps from './EditableProps';
|
||||
import ElementTypes from './ElementTypes';
|
||||
import InspectableElements from './InspectableElements';
|
||||
import InteractionTracing from './InteractionTracing';
|
||||
import PriorityLevels from './PriorityLevels';
|
||||
import ToDoList from './ToDoList';
|
||||
import Toggle from './Toggle';
|
||||
import SuspenseTree from './SuspenseTree';
|
||||
|
||||
import './styles.css';
|
||||
|
||||
const containers = [];
|
||||
const roots = [];
|
||||
|
||||
function mountHelper(App) {
|
||||
const container = document.createElement('div');
|
||||
|
||||
((document.body: any): HTMLBodyElement).appendChild(container);
|
||||
|
||||
containers.push(container);
|
||||
const root = createRoot(container);
|
||||
root.render(createElement(App));
|
||||
|
||||
render(createElement(App), container);
|
||||
roots.push(root);
|
||||
}
|
||||
|
||||
function mountTestApp() {
|
||||
@@ -33,13 +38,14 @@ function mountTestApp() {
|
||||
mountHelper(InspectableElements);
|
||||
mountHelper(ElementTypes);
|
||||
mountHelper(EditableProps);
|
||||
mountHelper(PriorityLevels);
|
||||
mountHelper(Toggle);
|
||||
mountHelper(SuspenseTree);
|
||||
mountHelper(DeeplyNestedComponents);
|
||||
}
|
||||
|
||||
function unmountTestApp() {
|
||||
containers.forEach(container => unmountComponentAtNode(container));
|
||||
roots.forEach(root => root.unmount());
|
||||
}
|
||||
|
||||
mountTestApp();
|
||||
|
||||
+12
-6
@@ -9,13 +9,19 @@ function getGitCommit() {
|
||||
}
|
||||
|
||||
function getGitHubURL() {
|
||||
// TODO potentially replac this with an fb.me URL (if it can forward the query params)
|
||||
return execSync('git remote get-url origin')
|
||||
// TODO potentially replace this with an fb.me URL (assuming it can forward the query params)
|
||||
const url = execSync('git remote get-url origin')
|
||||
.toString()
|
||||
.trim()
|
||||
.replace(':', '/')
|
||||
.replace('git@', 'https://')
|
||||
.replace('.git', '');
|
||||
.trim();
|
||||
|
||||
if (url.startsWith('https://')) {
|
||||
return url.replace('.git', '');
|
||||
} else {
|
||||
return url
|
||||
.replace(':', '/')
|
||||
.replace('git@', 'https://')
|
||||
.replace('.git', '');
|
||||
}
|
||||
}
|
||||
|
||||
function getVersionString() {
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`InspectedElementContext should inspect the currently selected element: 1: mount 1`] = `
|
||||
[root]
|
||||
<Example>
|
||||
`;
|
||||
|
||||
exports[`InspectedElementContext should inspect the currently selected element: 2: Inspected element 2 1`] = `
|
||||
{
|
||||
"id": 2,
|
||||
"owners": null,
|
||||
"context": null,
|
||||
"events": null,
|
||||
"hooks": [
|
||||
{
|
||||
"id": 0,
|
||||
"isStateEditable": true,
|
||||
"name": "State",
|
||||
"value": 1,
|
||||
"subHooks": []
|
||||
}
|
||||
],
|
||||
"props": {
|
||||
"foo": 1,
|
||||
"bar": "abc"
|
||||
},
|
||||
"state": null
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`InspectedElementContext should not re-render a function with hooks if it did not update since it was last inspected: 1: mount 1`] = `
|
||||
[root]
|
||||
▾ <Wrapper>
|
||||
<Anonymous>
|
||||
`;
|
||||
|
||||
exports[`InspectedElementContext should not re-render a function with hooks if it did not update since it was last inspected: 2: initial render 1`] = `
|
||||
{
|
||||
"id": 3,
|
||||
"owners": null,
|
||||
"context": null,
|
||||
"events": null,
|
||||
"hooks": [
|
||||
{
|
||||
"id": 0,
|
||||
"isStateEditable": true,
|
||||
"name": "State",
|
||||
"value": 0,
|
||||
"subHooks": []
|
||||
}
|
||||
],
|
||||
"props": {
|
||||
"foo": 1,
|
||||
"bar": "abc"
|
||||
},
|
||||
"state": null
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`InspectedElementContext should not re-render a function with hooks if it did not update since it was last inspected: 3: updated state 1`] = `
|
||||
{
|
||||
"id": 3,
|
||||
"owners": null,
|
||||
"context": null,
|
||||
"events": null,
|
||||
"hooks": [
|
||||
{
|
||||
"id": 0,
|
||||
"isStateEditable": true,
|
||||
"name": "State",
|
||||
"value": 0,
|
||||
"subHooks": []
|
||||
}
|
||||
],
|
||||
"props": {
|
||||
"foo": 2,
|
||||
"bar": "def"
|
||||
},
|
||||
"state": null
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`InspectedElementContext should poll for updates for the currently selected element: 1: mount 1`] = `
|
||||
[root]
|
||||
<Example>
|
||||
`;
|
||||
|
||||
exports[`InspectedElementContext should poll for updates for the currently selected element: 2: initial render 1`] = `
|
||||
{
|
||||
"id": 2,
|
||||
"owners": null,
|
||||
"context": null,
|
||||
"events": null,
|
||||
"hooks": null,
|
||||
"props": {
|
||||
"foo": 1,
|
||||
"bar": "abc"
|
||||
},
|
||||
"state": null
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`InspectedElementContext should poll for updates for the currently selected element: 2: updated state 1`] = `
|
||||
{
|
||||
"id": 2,
|
||||
"owners": null,
|
||||
"context": null,
|
||||
"events": null,
|
||||
"hooks": null,
|
||||
"props": {
|
||||
"foo": 2,
|
||||
"bar": "def"
|
||||
},
|
||||
"state": null
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,77 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`OwnersListContext should fetch the owners list for the selected element that includes filtered components: mount 1`] = `
|
||||
[root]
|
||||
▾ <Grandparent>
|
||||
<Child>
|
||||
<Child>
|
||||
`;
|
||||
|
||||
exports[`OwnersListContext should fetch the owners list for the selected element that includes filtered components: owners for "Child" 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"displayName": "Grandparent",
|
||||
"id": 7,
|
||||
},
|
||||
Object {
|
||||
"displayName": "Parent",
|
||||
"id": 9,
|
||||
},
|
||||
Object {
|
||||
"displayName": "Child",
|
||||
"id": 8,
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`OwnersListContext should fetch the owners list for the selected element: mount 1`] = `
|
||||
[root]
|
||||
▾ <Grandparent>
|
||||
▾ <Parent>
|
||||
<Child>
|
||||
<Child>
|
||||
`;
|
||||
|
||||
exports[`OwnersListContext should fetch the owners list for the selected element: owners for "Child" 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"displayName": "Grandparent",
|
||||
"id": 2,
|
||||
},
|
||||
Object {
|
||||
"displayName": "Parent",
|
||||
"id": 3,
|
||||
},
|
||||
Object {
|
||||
"displayName": "Child",
|
||||
"id": 4,
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`OwnersListContext should fetch the owners list for the selected element: owners for "Parent" 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"displayName": "Grandparent",
|
||||
"id": 2,
|
||||
},
|
||||
Object {
|
||||
"displayName": "Parent",
|
||||
"id": 3,
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`OwnersListContext should include the current element even if there are no other owners: mount 1`] = `
|
||||
[root]
|
||||
<Grandparent>
|
||||
`;
|
||||
|
||||
exports[`OwnersListContext should include the current element even if there are no other owners: owners for "Grandparent" 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"displayName": "Grandparent",
|
||||
"id": 5,
|
||||
},
|
||||
]
|
||||
`;
|
||||
@@ -0,0 +1,41 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ProfilerContext should auto-select the root ID matching the Components tab selection if it has profiling data: mounted 1`] = `
|
||||
[root]
|
||||
▾ <Parent>
|
||||
<Child>
|
||||
[root]
|
||||
▾ <Parent>
|
||||
<Child>
|
||||
`;
|
||||
|
||||
exports[`ProfilerContext should maintain root selection between profiling sessions so long as there is data for that root: mounted 1`] = `
|
||||
[root]
|
||||
▾ <Parent>
|
||||
<Child>
|
||||
[root]
|
||||
▾ <Parent>
|
||||
<Child>
|
||||
`;
|
||||
|
||||
exports[`ProfilerContext should not select the root ID matching the Components tab selection if it has no profiling data: mounted 1`] = `
|
||||
[root]
|
||||
▾ <Parent>
|
||||
<Child>
|
||||
[root]
|
||||
▾ <Parent>
|
||||
<Child>
|
||||
`;
|
||||
|
||||
exports[`ProfilerContext should sync selected element in the Components tab too, provided the element is a match: mounted 1`] = `
|
||||
[root]
|
||||
▾ <GrandParent>
|
||||
▾ <Parent>
|
||||
<Child>
|
||||
`;
|
||||
|
||||
exports[`ProfilerContext should sync selected element in the Components tab too, provided the element is a match: updated 1`] = `
|
||||
[root]
|
||||
▾ <GrandParent>
|
||||
<Parent>
|
||||
`;
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,7 @@ Object {
|
||||
"key": null,
|
||||
"parentID": 0,
|
||||
"treeBaseDuration": 15,
|
||||
"type": 11,
|
||||
},
|
||||
2 => Object {
|
||||
"children": Array [
|
||||
@@ -24,30 +25,34 @@ Object {
|
||||
"key": null,
|
||||
"parentID": 1,
|
||||
"treeBaseDuration": 15,
|
||||
"type": 5,
|
||||
},
|
||||
3 => Object {
|
||||
"children": Array [],
|
||||
"displayName": "Memo(Child)",
|
||||
"displayName": "Child",
|
||||
"id": 3,
|
||||
"key": "first",
|
||||
"parentID": 2,
|
||||
"treeBaseDuration": 3,
|
||||
"type": 8,
|
||||
},
|
||||
4 => Object {
|
||||
"children": Array [],
|
||||
"displayName": "Memo(Child)",
|
||||
"displayName": "Child",
|
||||
"id": 4,
|
||||
"key": "second",
|
||||
"parentID": 2,
|
||||
"treeBaseDuration": 2,
|
||||
"type": 8,
|
||||
},
|
||||
5 => Object {
|
||||
"children": Array [],
|
||||
"displayName": "Memo(Child)",
|
||||
"displayName": "Child",
|
||||
"id": 5,
|
||||
"key": "third",
|
||||
"parentID": 2,
|
||||
"treeBaseDuration": 0,
|
||||
"type": 8,
|
||||
},
|
||||
},
|
||||
"rootID": 1,
|
||||
@@ -65,6 +70,10 @@ Object {
|
||||
3 => 2,
|
||||
},
|
||||
"maxSelfDuration": 10,
|
||||
"renderPathNodes": Set {
|
||||
1,
|
||||
2,
|
||||
},
|
||||
"rows": Array [
|
||||
Array [
|
||||
Object {
|
||||
@@ -83,8 +92,8 @@ Object {
|
||||
"actualDuration": 0,
|
||||
"didRender": true,
|
||||
"id": 5,
|
||||
"label": "Memo(Child) key=\\"third\\" (<0.1ms of <0.1ms)",
|
||||
"name": "Memo(Child)",
|
||||
"label": "Child (Memo) key=\\"third\\" (<0.1ms of <0.1ms)",
|
||||
"name": "Child",
|
||||
"offset": 15,
|
||||
"selfDuration": 0,
|
||||
"treeBaseDuration": 0,
|
||||
@@ -93,8 +102,8 @@ Object {
|
||||
"actualDuration": 2,
|
||||
"didRender": true,
|
||||
"id": 4,
|
||||
"label": "Memo(Child) key=\\"second\\" (2ms of 2ms)",
|
||||
"name": "Memo(Child)",
|
||||
"label": "Child (Memo) key=\\"second\\" (2ms of 2ms)",
|
||||
"name": "Child",
|
||||
"offset": 13,
|
||||
"selfDuration": 2,
|
||||
"treeBaseDuration": 2,
|
||||
@@ -103,8 +112,8 @@ Object {
|
||||
"actualDuration": 3,
|
||||
"didRender": true,
|
||||
"id": 3,
|
||||
"label": "Memo(Child) key=\\"first\\" (3ms of 3ms)",
|
||||
"name": "Memo(Child)",
|
||||
"label": "Child (Memo) key=\\"first\\" (3ms of 3ms)",
|
||||
"name": "Child",
|
||||
"offset": 10,
|
||||
"selfDuration": 3,
|
||||
"treeBaseDuration": 3,
|
||||
@@ -126,6 +135,7 @@ Object {
|
||||
"key": null,
|
||||
"parentID": 0,
|
||||
"treeBaseDuration": 15,
|
||||
"type": 11,
|
||||
},
|
||||
2 => Object {
|
||||
"children": Array [
|
||||
@@ -138,30 +148,34 @@ Object {
|
||||
"key": null,
|
||||
"parentID": 1,
|
||||
"treeBaseDuration": 15,
|
||||
"type": 5,
|
||||
},
|
||||
3 => Object {
|
||||
"children": Array [],
|
||||
"displayName": "Memo(Child)",
|
||||
"displayName": "Child",
|
||||
"id": 3,
|
||||
"key": "first",
|
||||
"parentID": 2,
|
||||
"treeBaseDuration": 3,
|
||||
"type": 8,
|
||||
},
|
||||
4 => Object {
|
||||
"children": Array [],
|
||||
"displayName": "Memo(Child)",
|
||||
"displayName": "Child",
|
||||
"id": 4,
|
||||
"key": "second",
|
||||
"parentID": 2,
|
||||
"treeBaseDuration": 2,
|
||||
"type": 8,
|
||||
},
|
||||
5 => Object {
|
||||
"children": Array [],
|
||||
"displayName": "Memo(Child)",
|
||||
"displayName": "Child",
|
||||
"id": 5,
|
||||
"key": "third",
|
||||
"parentID": 2,
|
||||
"treeBaseDuration": 0,
|
||||
"type": 8,
|
||||
},
|
||||
},
|
||||
"rootID": 1,
|
||||
@@ -179,6 +193,9 @@ Object {
|
||||
3 => 2,
|
||||
},
|
||||
"maxSelfDuration": 10,
|
||||
"renderPathNodes": Set {
|
||||
1,
|
||||
},
|
||||
"rows": Array [
|
||||
Array [
|
||||
Object {
|
||||
@@ -197,8 +214,8 @@ Object {
|
||||
"actualDuration": 0,
|
||||
"didRender": false,
|
||||
"id": 5,
|
||||
"label": "Memo(Child) key=\\"third\\"",
|
||||
"name": "Memo(Child)",
|
||||
"label": "Child (Memo) key=\\"third\\"",
|
||||
"name": "Child",
|
||||
"offset": 15,
|
||||
"selfDuration": 0,
|
||||
"treeBaseDuration": 0,
|
||||
@@ -207,8 +224,8 @@ Object {
|
||||
"actualDuration": 0,
|
||||
"didRender": false,
|
||||
"id": 4,
|
||||
"label": "Memo(Child) key=\\"second\\"",
|
||||
"name": "Memo(Child)",
|
||||
"label": "Child (Memo) key=\\"second\\"",
|
||||
"name": "Child",
|
||||
"offset": 13,
|
||||
"selfDuration": 0,
|
||||
"treeBaseDuration": 2,
|
||||
@@ -217,8 +234,8 @@ Object {
|
||||
"actualDuration": 0,
|
||||
"didRender": false,
|
||||
"id": 3,
|
||||
"label": "Memo(Child) key=\\"first\\"",
|
||||
"name": "Memo(Child)",
|
||||
"label": "Child (Memo) key=\\"first\\"",
|
||||
"name": "Child",
|
||||
"offset": 10,
|
||||
"selfDuration": 0,
|
||||
"treeBaseDuration": 3,
|
||||
@@ -230,6 +247,20 @@ Object {
|
||||
|
||||
exports[`profiling charts interactions should contain valid data: Interactions 1`] = `
|
||||
Object {
|
||||
"interactions": Array [
|
||||
Object {
|
||||
"__count": 1,
|
||||
"id": 0,
|
||||
"name": "mount",
|
||||
"timestamp": 0,
|
||||
},
|
||||
Object {
|
||||
"__count": 0,
|
||||
"id": 1,
|
||||
"name": "update",
|
||||
"timestamp": 15,
|
||||
},
|
||||
],
|
||||
"lastInteractionTime": 25,
|
||||
"maxCommitDuration": 15,
|
||||
}
|
||||
@@ -237,6 +268,20 @@ Object {
|
||||
|
||||
exports[`profiling charts interactions should contain valid data: Interactions 2`] = `
|
||||
Object {
|
||||
"interactions": Array [
|
||||
Object {
|
||||
"__count": 1,
|
||||
"id": 0,
|
||||
"name": "mount",
|
||||
"timestamp": 0,
|
||||
},
|
||||
Object {
|
||||
"__count": 0,
|
||||
"id": 1,
|
||||
"name": "update",
|
||||
"timestamp": 15,
|
||||
},
|
||||
],
|
||||
"lastInteractionTime": 25,
|
||||
"maxCommitDuration": 15,
|
||||
}
|
||||
@@ -254,6 +299,7 @@ Object {
|
||||
"key": null,
|
||||
"parentID": 0,
|
||||
"treeBaseDuration": 15,
|
||||
"type": 11,
|
||||
},
|
||||
2 => Object {
|
||||
"children": Array [
|
||||
@@ -266,30 +312,34 @@ Object {
|
||||
"key": null,
|
||||
"parentID": 1,
|
||||
"treeBaseDuration": 15,
|
||||
"type": 5,
|
||||
},
|
||||
3 => Object {
|
||||
"children": Array [],
|
||||
"displayName": "Memo(Child)",
|
||||
"displayName": "Child",
|
||||
"id": 3,
|
||||
"key": "first",
|
||||
"parentID": 2,
|
||||
"treeBaseDuration": 3,
|
||||
"type": 8,
|
||||
},
|
||||
4 => Object {
|
||||
"children": Array [],
|
||||
"displayName": "Memo(Child)",
|
||||
"displayName": "Child",
|
||||
"id": 4,
|
||||
"key": "second",
|
||||
"parentID": 2,
|
||||
"treeBaseDuration": 2,
|
||||
"type": 8,
|
||||
},
|
||||
5 => Object {
|
||||
"children": Array [],
|
||||
"displayName": "Memo(Child)",
|
||||
"displayName": "Child",
|
||||
"id": 5,
|
||||
"key": "third",
|
||||
"parentID": 2,
|
||||
"treeBaseDuration": 0,
|
||||
"type": 8,
|
||||
},
|
||||
},
|
||||
"rootID": 1,
|
||||
@@ -308,20 +358,20 @@ Object {
|
||||
},
|
||||
Object {
|
||||
"id": 3,
|
||||
"label": "Memo(Child) key=\\"first\\" (3ms)",
|
||||
"name": "Memo(Child)",
|
||||
"label": "Child (Memo) key=\\"first\\" (3ms)",
|
||||
"name": "Child",
|
||||
"value": 3,
|
||||
},
|
||||
Object {
|
||||
"id": 4,
|
||||
"label": "Memo(Child) key=\\"second\\" (2ms)",
|
||||
"name": "Memo(Child)",
|
||||
"label": "Child (Memo) key=\\"second\\" (2ms)",
|
||||
"name": "Child",
|
||||
"value": 2,
|
||||
},
|
||||
Object {
|
||||
"id": 5,
|
||||
"label": "Memo(Child) key=\\"third\\" (<0.1ms)",
|
||||
"name": "Memo(Child)",
|
||||
"label": "Child (Memo) key=\\"third\\" (<0.1ms)",
|
||||
"name": "Child",
|
||||
"value": 0,
|
||||
},
|
||||
],
|
||||
@@ -340,6 +390,7 @@ Object {
|
||||
"key": null,
|
||||
"parentID": 0,
|
||||
"treeBaseDuration": 15,
|
||||
"type": 11,
|
||||
},
|
||||
2 => Object {
|
||||
"children": Array [
|
||||
@@ -352,30 +403,34 @@ Object {
|
||||
"key": null,
|
||||
"parentID": 1,
|
||||
"treeBaseDuration": 15,
|
||||
"type": 5,
|
||||
},
|
||||
3 => Object {
|
||||
"children": Array [],
|
||||
"displayName": "Memo(Child)",
|
||||
"displayName": "Child",
|
||||
"id": 3,
|
||||
"key": "first",
|
||||
"parentID": 2,
|
||||
"treeBaseDuration": 3,
|
||||
"type": 8,
|
||||
},
|
||||
4 => Object {
|
||||
"children": Array [],
|
||||
"displayName": "Memo(Child)",
|
||||
"displayName": "Child",
|
||||
"id": 4,
|
||||
"key": "second",
|
||||
"parentID": 2,
|
||||
"treeBaseDuration": 2,
|
||||
"type": 8,
|
||||
},
|
||||
5 => Object {
|
||||
"children": Array [],
|
||||
"displayName": "Memo(Child)",
|
||||
"displayName": "Child",
|
||||
"id": 5,
|
||||
"key": "third",
|
||||
"parentID": 2,
|
||||
"treeBaseDuration": 0,
|
||||
"type": 8,
|
||||
},
|
||||
},
|
||||
"rootID": 1,
|
||||
|
||||
@@ -12,6 +12,7 @@ Object {
|
||||
"key": null,
|
||||
"parentID": 0,
|
||||
"treeBaseDuration": 12,
|
||||
"type": 11,
|
||||
},
|
||||
2 => Object {
|
||||
"children": Array [
|
||||
@@ -22,14 +23,16 @@ Object {
|
||||
"key": null,
|
||||
"parentID": 1,
|
||||
"treeBaseDuration": 12,
|
||||
"type": 5,
|
||||
},
|
||||
3 => Object {
|
||||
"children": Array [],
|
||||
"displayName": "Memo(Child)",
|
||||
"displayName": "Child",
|
||||
"id": 3,
|
||||
"key": "0",
|
||||
"parentID": 2,
|
||||
"treeBaseDuration": 2,
|
||||
"type": 8,
|
||||
},
|
||||
},
|
||||
"rootID": 1,
|
||||
@@ -48,6 +51,7 @@ Object {
|
||||
"key": null,
|
||||
"parentID": 0,
|
||||
"treeBaseDuration": 16,
|
||||
"type": 11,
|
||||
},
|
||||
2 => Object {
|
||||
"children": Array [
|
||||
@@ -60,30 +64,34 @@ Object {
|
||||
"key": null,
|
||||
"parentID": 1,
|
||||
"treeBaseDuration": 16,
|
||||
"type": 5,
|
||||
},
|
||||
3 => Object {
|
||||
"children": Array [],
|
||||
"displayName": "Memo(Child)",
|
||||
"displayName": "Child",
|
||||
"id": 3,
|
||||
"key": "0",
|
||||
"parentID": 2,
|
||||
"treeBaseDuration": 2,
|
||||
"type": 8,
|
||||
},
|
||||
4 => Object {
|
||||
"children": Array [],
|
||||
"displayName": "Memo(Child)",
|
||||
"displayName": "Child",
|
||||
"id": 4,
|
||||
"key": "1",
|
||||
"parentID": 2,
|
||||
"treeBaseDuration": 2,
|
||||
"type": 8,
|
||||
},
|
||||
5 => Object {
|
||||
"children": Array [],
|
||||
"displayName": "Memo(Child)",
|
||||
"displayName": "Child",
|
||||
"id": 5,
|
||||
"key": "2",
|
||||
"parentID": 2,
|
||||
"treeBaseDuration": 2,
|
||||
"type": 8,
|
||||
},
|
||||
},
|
||||
"rootID": 1,
|
||||
@@ -102,6 +110,7 @@ Object {
|
||||
"key": null,
|
||||
"parentID": 0,
|
||||
"treeBaseDuration": 14,
|
||||
"type": 11,
|
||||
},
|
||||
2 => Object {
|
||||
"children": Array [
|
||||
@@ -113,22 +122,25 @@ Object {
|
||||
"key": null,
|
||||
"parentID": 1,
|
||||
"treeBaseDuration": 14,
|
||||
"type": 5,
|
||||
},
|
||||
3 => Object {
|
||||
"children": Array [],
|
||||
"displayName": "Memo(Child)",
|
||||
"displayName": "Child",
|
||||
"id": 3,
|
||||
"key": "0",
|
||||
"parentID": 2,
|
||||
"treeBaseDuration": 2,
|
||||
"type": 8,
|
||||
},
|
||||
4 => Object {
|
||||
"children": Array [],
|
||||
"displayName": "Memo(Child)",
|
||||
"displayName": "Child",
|
||||
"id": 4,
|
||||
"key": "1",
|
||||
"parentID": 2,
|
||||
"treeBaseDuration": 2,
|
||||
"type": 8,
|
||||
},
|
||||
},
|
||||
"rootID": 1,
|
||||
@@ -147,6 +159,7 @@ Object {
|
||||
"key": null,
|
||||
"parentID": 0,
|
||||
"treeBaseDuration": 10,
|
||||
"type": 11,
|
||||
},
|
||||
2 => Object {
|
||||
"children": Array [],
|
||||
@@ -155,6 +168,7 @@ Object {
|
||||
"key": null,
|
||||
"parentID": 1,
|
||||
"treeBaseDuration": 10,
|
||||
"type": 5,
|
||||
},
|
||||
},
|
||||
"rootID": 1,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,42 @@
|
||||
// @flow
|
||||
|
||||
describe('Bridge', () => {
|
||||
let Bridge;
|
||||
|
||||
beforeEach(() => {
|
||||
Bridge = require('src/bridge').default;
|
||||
});
|
||||
|
||||
it('should shutdown properly', () => {
|
||||
const wall = {
|
||||
listen: jest.fn(() => () => {}),
|
||||
send: jest.fn(),
|
||||
};
|
||||
const bridge = new Bridge(wall);
|
||||
|
||||
// Check that we're wired up correctly.
|
||||
bridge.send('init');
|
||||
jest.runAllTimers();
|
||||
expect(wall.send).toHaveBeenCalledWith('init', undefined, undefined);
|
||||
|
||||
// Should flush pending messages and then shut down.
|
||||
wall.send.mockClear();
|
||||
bridge.send('update', '1');
|
||||
bridge.send('update', '2');
|
||||
bridge.shutdown();
|
||||
jest.runAllTimers();
|
||||
expect(wall.send).toHaveBeenCalledWith('update', '1', undefined);
|
||||
expect(wall.send).toHaveBeenCalledWith('update', '2', undefined);
|
||||
expect(wall.send).toHaveBeenCalledWith('shutdown', undefined, undefined);
|
||||
|
||||
// Verify that the Bridge doesn't send messages after shutdown.
|
||||
spyOn(console, 'warn');
|
||||
wall.send.mockClear();
|
||||
bridge.send('should not send');
|
||||
jest.runAllTimers();
|
||||
expect(wall.send).not.toHaveBeenCalled();
|
||||
expect(console.warn).toHaveBeenCalledWith(
|
||||
'Cannot send message "should not send" through a Bridge that has been shutdown.'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,252 @@
|
||||
// @flow
|
||||
|
||||
import typeof ReactTestRenderer from 'react-test-renderer';
|
||||
import type { Element } from 'src/devtools/views/Components/types';
|
||||
import type Bridge from 'src/bridge';
|
||||
import type Store from 'src/devtools/store';
|
||||
|
||||
describe('InspectedElementContext', () => {
|
||||
let React;
|
||||
let ReactDOM;
|
||||
let TestRenderer: ReactTestRenderer;
|
||||
let bridge: Bridge;
|
||||
let store: Store;
|
||||
let utils;
|
||||
|
||||
let BridgeContext;
|
||||
let InspectedElementContext;
|
||||
let InspectedElementContextController;
|
||||
let StoreContext;
|
||||
let TreeContextController;
|
||||
|
||||
beforeEach(() => {
|
||||
utils = require('./utils');
|
||||
utils.beforeEachProfiling();
|
||||
|
||||
bridge = global.bridge;
|
||||
store = global.store;
|
||||
store.collapseNodesByDefault = false;
|
||||
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
TestRenderer = utils.requireTestRenderer();
|
||||
|
||||
BridgeContext = require('src/devtools/views/context').BridgeContext;
|
||||
InspectedElementContext = require('src/devtools/views/Components/InspectedElementContext')
|
||||
.InspectedElementContext;
|
||||
InspectedElementContextController = require('src/devtools/views/Components/InspectedElementContext')
|
||||
.InspectedElementContextController;
|
||||
StoreContext = require('src/devtools/views/context').StoreContext;
|
||||
TreeContextController = require('src/devtools/views/Components/TreeContext')
|
||||
.TreeContextController;
|
||||
});
|
||||
|
||||
const Contexts = ({
|
||||
children,
|
||||
defaultSelectedElementID = null,
|
||||
defaultSelectedElementIndex = null,
|
||||
}) => (
|
||||
<BridgeContext.Provider value={bridge}>
|
||||
<StoreContext.Provider value={store}>
|
||||
<TreeContextController
|
||||
defaultSelectedElementID={defaultSelectedElementID}
|
||||
defaultSelectedElementIndex={defaultSelectedElementIndex}
|
||||
>
|
||||
<InspectedElementContextController>
|
||||
{children}
|
||||
</InspectedElementContextController>
|
||||
</TreeContextController>
|
||||
</StoreContext.Provider>
|
||||
</BridgeContext.Provider>
|
||||
);
|
||||
|
||||
it('should inspect the currently selected element', async done => {
|
||||
const Example = () => {
|
||||
const [count] = React.useState(1);
|
||||
return count;
|
||||
};
|
||||
|
||||
const container = document.createElement('div');
|
||||
utils.act(() => ReactDOM.render(<Example foo={1} bar="abc" />, container));
|
||||
expect(store).toMatchSnapshot('1: mount');
|
||||
|
||||
const example = ((store.getElementAtIndex(0): any): Element);
|
||||
|
||||
let didFinish = false;
|
||||
|
||||
function Suspender({ target }) {
|
||||
const { read } = React.useContext(InspectedElementContext);
|
||||
const inspectedElement = read(target.id);
|
||||
expect(inspectedElement).toMatchSnapshot(
|
||||
`2: Inspected element ${target.id}`
|
||||
);
|
||||
didFinish = true;
|
||||
return null;
|
||||
}
|
||||
|
||||
await utils.actAsync(
|
||||
() =>
|
||||
TestRenderer.create(
|
||||
<Contexts
|
||||
defaultSelectedElementID={example.id}
|
||||
defaultSelectedElementIndex={0}
|
||||
>
|
||||
<React.Suspense fallback={null}>
|
||||
<Suspender target={example} />
|
||||
</React.Suspense>
|
||||
</Contexts>
|
||||
),
|
||||
3
|
||||
);
|
||||
expect(didFinish).toBe(true);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should poll for updates for the currently selected element', async done => {
|
||||
const Example = () => null;
|
||||
|
||||
const container = document.createElement('div');
|
||||
utils.act(() => ReactDOM.render(<Example foo={1} bar="abc" />, container));
|
||||
expect(store).toMatchSnapshot('1: mount');
|
||||
|
||||
const example = ((store.getElementAtIndex(0): any): Element);
|
||||
|
||||
let inspectedElement = null;
|
||||
|
||||
function Suspender({ target }) {
|
||||
const { read } = React.useContext(InspectedElementContext);
|
||||
inspectedElement = read(target.id);
|
||||
return null;
|
||||
}
|
||||
|
||||
await utils.actAsync(
|
||||
() =>
|
||||
TestRenderer.create(
|
||||
<Contexts
|
||||
defaultSelectedElementID={example.id}
|
||||
defaultSelectedElementIndex={0}
|
||||
>
|
||||
<React.Suspense fallback={null}>
|
||||
<Suspender target={example} />
|
||||
</React.Suspense>
|
||||
</Contexts>
|
||||
),
|
||||
3
|
||||
);
|
||||
expect(inspectedElement).toMatchSnapshot('2: initial render');
|
||||
|
||||
await utils.actAsync(() =>
|
||||
ReactDOM.render(<Example foo={2} bar="def" />, container)
|
||||
);
|
||||
|
||||
inspectedElement = null;
|
||||
await utils.actAsync(
|
||||
() =>
|
||||
TestRenderer.create(
|
||||
<Contexts
|
||||
defaultSelectedElementID={example.id}
|
||||
defaultSelectedElementIndex={0}
|
||||
>
|
||||
<React.Suspense fallback={null}>
|
||||
<Suspender target={example} />
|
||||
</React.Suspense>
|
||||
</Contexts>
|
||||
),
|
||||
1
|
||||
);
|
||||
expect(inspectedElement).toMatchSnapshot('2: updated state');
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should not re-render a function with hooks if it did not update since it was last inspected', async done => {
|
||||
let targetRenderCount = 0;
|
||||
|
||||
const Wrapper = ({ children }) => children;
|
||||
const Target = React.memo(props => {
|
||||
targetRenderCount++;
|
||||
React.useState(0);
|
||||
return null;
|
||||
});
|
||||
|
||||
const container = document.createElement('div');
|
||||
utils.act(() =>
|
||||
ReactDOM.render(
|
||||
<Wrapper>
|
||||
<Target foo={1} bar="abc" />
|
||||
</Wrapper>,
|
||||
container
|
||||
)
|
||||
);
|
||||
expect(store).toMatchSnapshot('1: mount');
|
||||
|
||||
const id = ((store.getElementIDAtIndex(1): any): number);
|
||||
|
||||
let inspectedElement = null;
|
||||
|
||||
function Suspender({ target }) {
|
||||
const { read } = React.useContext(InspectedElementContext);
|
||||
inspectedElement = read(target);
|
||||
return null;
|
||||
}
|
||||
|
||||
targetRenderCount = 0;
|
||||
|
||||
let renderer;
|
||||
await utils.actAsync(
|
||||
() =>
|
||||
(renderer = TestRenderer.create(
|
||||
<Contexts
|
||||
defaultSelectedElementID={id}
|
||||
defaultSelectedElementIndex={1}
|
||||
>
|
||||
<React.Suspense fallback={null}>
|
||||
<Suspender target={id} />
|
||||
</React.Suspense>
|
||||
</Contexts>
|
||||
)),
|
||||
3
|
||||
);
|
||||
expect(targetRenderCount).toBe(1);
|
||||
expect(inspectedElement).toMatchSnapshot('2: initial render');
|
||||
|
||||
const initialInspectedElement = inspectedElement;
|
||||
|
||||
targetRenderCount = 0;
|
||||
inspectedElement = null;
|
||||
await utils.actAsync(
|
||||
() =>
|
||||
renderer.update(
|
||||
<Contexts
|
||||
defaultSelectedElementID={id}
|
||||
defaultSelectedElementIndex={1}
|
||||
>
|
||||
<React.Suspense fallback={null}>
|
||||
<Suspender target={id} />
|
||||
</React.Suspense>
|
||||
</Contexts>
|
||||
),
|
||||
1
|
||||
);
|
||||
expect(targetRenderCount).toBe(0);
|
||||
expect(inspectedElement).toEqual(initialInspectedElement);
|
||||
|
||||
targetRenderCount = 0;
|
||||
|
||||
await utils.actAsync(() =>
|
||||
ReactDOM.render(
|
||||
<Wrapper>
|
||||
<Target foo={2} bar="def" />
|
||||
</Wrapper>,
|
||||
container
|
||||
)
|
||||
);
|
||||
|
||||
// Target should have been rendered once (by ReactDOM) and once by DevTools for inspection.
|
||||
expect(targetRenderCount).toBe(2);
|
||||
expect(inspectedElement).toMatchSnapshot('3: updated state');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
// test() is part of Jest's serializer API
|
||||
export function test(maybeInspectedElement) {
|
||||
return (
|
||||
maybeInspectedElement !== null &&
|
||||
typeof maybeInspectedElement === 'object' &&
|
||||
maybeInspectedElement.hasOwnProperty('canEditFunctionProps') &&
|
||||
maybeInspectedElement.hasOwnProperty('canEditHooks') &&
|
||||
maybeInspectedElement.hasOwnProperty('canToggleSuspense') &&
|
||||
maybeInspectedElement.hasOwnProperty('canViewSource')
|
||||
);
|
||||
}
|
||||
|
||||
// print() is part of Jest's serializer API
|
||||
export function print(inspectedElement, serialize, indent) {
|
||||
return JSON.stringify(
|
||||
{
|
||||
id: inspectedElement.id,
|
||||
owners: inspectedElement.owners,
|
||||
context: inspectedElement.context,
|
||||
events: inspectedElement.events,
|
||||
hooks: inspectedElement.hooks,
|
||||
props: inspectedElement.props,
|
||||
state: inspectedElement.state,
|
||||
},
|
||||
null,
|
||||
2
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
// @flow
|
||||
|
||||
import typeof ReactTestRenderer from 'react-test-renderer';
|
||||
import type { Element } from 'src/devtools/views/Components/types';
|
||||
import type Bridge from 'src/bridge';
|
||||
import type Store from 'src/devtools/store';
|
||||
|
||||
describe('OwnersListContext', () => {
|
||||
let React;
|
||||
let ReactDOM;
|
||||
let TestRenderer: ReactTestRenderer;
|
||||
let bridge: Bridge;
|
||||
let store: Store;
|
||||
let utils;
|
||||
|
||||
let BridgeContext;
|
||||
let OwnersListContext;
|
||||
let OwnersListContextController;
|
||||
let StoreContext;
|
||||
let TreeContextController;
|
||||
|
||||
beforeEach(() => {
|
||||
utils = require('./utils');
|
||||
utils.beforeEachProfiling();
|
||||
|
||||
bridge = global.bridge;
|
||||
store = global.store;
|
||||
store.collapseNodesByDefault = false;
|
||||
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
TestRenderer = utils.requireTestRenderer();
|
||||
|
||||
BridgeContext = require('src/devtools/views/context').BridgeContext;
|
||||
OwnersListContext = require('src/devtools/views/Components/OwnersListContext')
|
||||
.OwnersListContext;
|
||||
OwnersListContextController = require('src/devtools/views/Components/OwnersListContext')
|
||||
.OwnersListContextController;
|
||||
StoreContext = require('src/devtools/views/context').StoreContext;
|
||||
TreeContextController = require('src/devtools/views/Components/TreeContext')
|
||||
.TreeContextController;
|
||||
});
|
||||
|
||||
const Contexts = ({ children, defaultOwnerID = null }) => (
|
||||
<BridgeContext.Provider value={bridge}>
|
||||
<StoreContext.Provider value={store}>
|
||||
<TreeContextController defaultOwnerID={defaultOwnerID}>
|
||||
<OwnersListContextController>{children}</OwnersListContextController>
|
||||
</TreeContextController>
|
||||
</StoreContext.Provider>
|
||||
</BridgeContext.Provider>
|
||||
);
|
||||
|
||||
it('should fetch the owners list for the selected element', async done => {
|
||||
const Grandparent = () => <Parent />;
|
||||
const Parent = () => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Child />
|
||||
<Child />
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
const Child = () => null;
|
||||
|
||||
utils.act(() =>
|
||||
ReactDOM.render(<Grandparent />, document.createElement('div'))
|
||||
);
|
||||
|
||||
expect(store).toMatchSnapshot('mount');
|
||||
|
||||
const parent = ((store.getElementAtIndex(1): any): Element);
|
||||
const firstChild = ((store.getElementAtIndex(2): any): Element);
|
||||
|
||||
let didFinish = false;
|
||||
|
||||
function Suspender({ owner }) {
|
||||
const read = React.useContext(OwnersListContext);
|
||||
const owners = read(owner.id);
|
||||
expect(owners).toMatchSnapshot(
|
||||
`owners for "${(owner && owner.displayName) || ''}"`
|
||||
);
|
||||
didFinish = true;
|
||||
return null;
|
||||
}
|
||||
|
||||
await utils.actAsync(
|
||||
() =>
|
||||
TestRenderer.create(
|
||||
<Contexts defaultOwnerID={parent.id}>
|
||||
<React.Suspense fallback={null}>
|
||||
<Suspender owner={parent} />
|
||||
</React.Suspense>
|
||||
</Contexts>
|
||||
),
|
||||
3
|
||||
);
|
||||
expect(didFinish).toBe(true);
|
||||
|
||||
didFinish = false;
|
||||
await utils.actAsync(
|
||||
() =>
|
||||
TestRenderer.create(
|
||||
<Contexts defaultOwnerID={firstChild.id}>
|
||||
<React.Suspense fallback={null}>
|
||||
<Suspender owner={firstChild} />
|
||||
</React.Suspense>
|
||||
</Contexts>
|
||||
),
|
||||
3
|
||||
);
|
||||
expect(didFinish).toBe(true);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should fetch the owners list for the selected element that includes filtered components', async done => {
|
||||
store.componentFilters = [utils.createDisplayNameFilter('^Parent$')];
|
||||
|
||||
const Grandparent = () => <Parent />;
|
||||
const Parent = () => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Child />
|
||||
<Child />
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
const Child = () => null;
|
||||
|
||||
utils.act(() =>
|
||||
ReactDOM.render(<Grandparent />, document.createElement('div'))
|
||||
);
|
||||
|
||||
expect(store).toMatchSnapshot('mount');
|
||||
|
||||
const firstChild = ((store.getElementAtIndex(1): any): Element);
|
||||
|
||||
let didFinish = false;
|
||||
|
||||
function Suspender({ owner }) {
|
||||
const read = React.useContext(OwnersListContext);
|
||||
const owners = read(owner.id);
|
||||
expect(owners).toMatchSnapshot(
|
||||
`owners for "${(owner && owner.displayName) || ''}"`
|
||||
);
|
||||
didFinish = true;
|
||||
return null;
|
||||
}
|
||||
|
||||
await utils.actAsync(
|
||||
() =>
|
||||
TestRenderer.create(
|
||||
<Contexts defaultOwnerID={firstChild.id}>
|
||||
<React.Suspense fallback={null}>
|
||||
<Suspender owner={firstChild} />
|
||||
</React.Suspense>
|
||||
</Contexts>
|
||||
),
|
||||
3
|
||||
);
|
||||
expect(didFinish).toBe(true);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should include the current element even if there are no other owners', async done => {
|
||||
store.componentFilters = [utils.createDisplayNameFilter('^Parent$')];
|
||||
|
||||
const Grandparent = () => <Parent />;
|
||||
const Parent = () => null;
|
||||
|
||||
utils.act(() =>
|
||||
ReactDOM.render(<Grandparent />, document.createElement('div'))
|
||||
);
|
||||
|
||||
expect(store).toMatchSnapshot('mount');
|
||||
|
||||
const grandparent = ((store.getElementAtIndex(0): any): Element);
|
||||
|
||||
let didFinish = false;
|
||||
|
||||
function Suspender({ owner }) {
|
||||
const read = React.useContext(OwnersListContext);
|
||||
const owners = read(owner.id);
|
||||
expect(owners).toMatchSnapshot(
|
||||
`owners for "${(owner && owner.displayName) || ''}"`
|
||||
);
|
||||
didFinish = true;
|
||||
return null;
|
||||
}
|
||||
|
||||
await utils.actAsync(
|
||||
() =>
|
||||
TestRenderer.create(
|
||||
<Contexts defaultOwnerID={grandparent.id}>
|
||||
<React.Suspense fallback={null}>
|
||||
<Suspender owner={grandparent} />
|
||||
</React.Suspense>
|
||||
</Contexts>
|
||||
),
|
||||
3
|
||||
);
|
||||
expect(didFinish).toBe(true);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,347 @@
|
||||
// @flow
|
||||
|
||||
import typeof ReactTestRenderer from 'react-test-renderer';
|
||||
import type Bridge from 'src/bridge';
|
||||
import type { Context } from 'src/devtools/views/Profiler/ProfilerContext';
|
||||
import type { DispatcherContext } from 'src/devtools/views/Components/TreeContext';
|
||||
import type Store from 'src/devtools/store';
|
||||
|
||||
describe('ProfilerContext', () => {
|
||||
let React;
|
||||
let ReactDOM;
|
||||
let TestRenderer: ReactTestRenderer;
|
||||
let bridge: Bridge;
|
||||
let store: Store;
|
||||
let utils;
|
||||
|
||||
let BridgeContext;
|
||||
let ProfilerContext;
|
||||
let ProfilerContextController;
|
||||
let StoreContext;
|
||||
let TreeContextController;
|
||||
let TreeDispatcherContext;
|
||||
let TreeStateContext;
|
||||
|
||||
beforeEach(() => {
|
||||
utils = require('./utils');
|
||||
utils.beforeEachProfiling();
|
||||
|
||||
bridge = global.bridge;
|
||||
store = global.store;
|
||||
store.collapseNodesByDefault = false;
|
||||
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
TestRenderer = utils.requireTestRenderer();
|
||||
|
||||
BridgeContext = require('src/devtools/views/context').BridgeContext;
|
||||
ProfilerContext = require('src/devtools/views/Profiler/ProfilerContext')
|
||||
.ProfilerContext;
|
||||
ProfilerContextController = require('src/devtools/views/Profiler/ProfilerContext')
|
||||
.ProfilerContextController;
|
||||
StoreContext = require('src/devtools/views/context').StoreContext;
|
||||
TreeContextController = require('src/devtools/views/Components/TreeContext')
|
||||
.TreeContextController;
|
||||
TreeDispatcherContext = require('src/devtools/views/Components/TreeContext')
|
||||
.TreeDispatcherContext;
|
||||
TreeStateContext = require('src/devtools/views/Components/TreeContext')
|
||||
.TreeStateContext;
|
||||
});
|
||||
|
||||
const Contexts = ({
|
||||
children = null,
|
||||
defaultSelectedElementID = null,
|
||||
defaultSelectedElementIndex = null,
|
||||
}: any) => (
|
||||
<BridgeContext.Provider value={bridge}>
|
||||
<StoreContext.Provider value={store}>
|
||||
<TreeContextController
|
||||
defaultSelectedElementID={defaultSelectedElementID}
|
||||
defaultSelectedElementIndex={defaultSelectedElementIndex}
|
||||
>
|
||||
<ProfilerContextController>{children}</ProfilerContextController>
|
||||
</TreeContextController>
|
||||
</StoreContext.Provider>
|
||||
</BridgeContext.Provider>
|
||||
);
|
||||
|
||||
it('updates updates profiling support based on the attached roots', async done => {
|
||||
const Component = () => null;
|
||||
|
||||
let context: Context = ((null: any): Context);
|
||||
|
||||
function ContextReader() {
|
||||
context = React.useContext(ProfilerContext);
|
||||
return null;
|
||||
}
|
||||
await utils.actAsync(() => {
|
||||
TestRenderer.create(
|
||||
<Contexts>
|
||||
<ContextReader />
|
||||
</Contexts>
|
||||
);
|
||||
});
|
||||
|
||||
expect(context.supportsProfiling).toBe(false);
|
||||
|
||||
const containerA = document.createElement('div');
|
||||
const containerB = document.createElement('div');
|
||||
|
||||
await utils.actAsync(() => ReactDOM.render(<Component />, containerA));
|
||||
expect(context.supportsProfiling).toBe(true);
|
||||
|
||||
await utils.actAsync(() => ReactDOM.render(<Component />, containerB));
|
||||
await utils.actAsync(() => ReactDOM.unmountComponentAtNode(containerA));
|
||||
expect(context.supportsProfiling).toBe(true);
|
||||
|
||||
await utils.actAsync(() => ReactDOM.unmountComponentAtNode(containerB));
|
||||
expect(context.supportsProfiling).toBe(false);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should gracefully handle an empty profiling session (with no recorded commits)', async done => {
|
||||
const Example = () => null;
|
||||
|
||||
utils.act(() =>
|
||||
ReactDOM.render(<Example />, document.createElement('div'))
|
||||
);
|
||||
|
||||
let context: Context = ((null: any): Context);
|
||||
|
||||
function ContextReader() {
|
||||
context = React.useContext(ProfilerContext);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Profile but don't record any updates.
|
||||
await utils.actAsync(() => store.profilerStore.startProfiling());
|
||||
await utils.actAsync(() => {
|
||||
TestRenderer.create(
|
||||
<Contexts>
|
||||
<ContextReader />
|
||||
</Contexts>
|
||||
);
|
||||
});
|
||||
expect(context).not.toBeNull();
|
||||
expect(context.didRecordCommits).toBe(false);
|
||||
expect(context.isProcessingData).toBe(false);
|
||||
expect(context.isProfiling).toBe(true);
|
||||
expect(context.profilingData).toBe(null);
|
||||
await utils.actAsync(() => store.profilerStore.stopProfiling());
|
||||
|
||||
expect(context).not.toBeNull();
|
||||
expect(context.didRecordCommits).toBe(false);
|
||||
expect(context.isProcessingData).toBe(false);
|
||||
expect(context.isProfiling).toBe(false);
|
||||
expect(context.profilingData).not.toBe(null);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should auto-select the root ID matching the Components tab selection if it has profiling data', async done => {
|
||||
const Parent = () => <Child />;
|
||||
const Child = () => null;
|
||||
|
||||
const containerOne = document.createElement('div');
|
||||
const containerTwo = document.createElement('div');
|
||||
utils.act(() => ReactDOM.render(<Parent />, containerOne));
|
||||
utils.act(() => ReactDOM.render(<Parent />, containerTwo));
|
||||
expect(store).toMatchSnapshot('mounted');
|
||||
|
||||
// Profile and record updates to both roots.
|
||||
await utils.actAsync(() => store.profilerStore.startProfiling());
|
||||
await utils.actAsync(() => ReactDOM.render(<Parent />, containerOne));
|
||||
await utils.actAsync(() => ReactDOM.render(<Parent />, containerTwo));
|
||||
await utils.actAsync(() => store.profilerStore.stopProfiling());
|
||||
|
||||
let context: Context = ((null: any): Context);
|
||||
function ContextReader() {
|
||||
context = React.useContext(ProfilerContext);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Select an element within the second root.
|
||||
await utils.actAsync(() =>
|
||||
TestRenderer.create(
|
||||
<Contexts
|
||||
defaultSelectedElementID={store.getElementIDAtIndex(3)}
|
||||
defaultSelectedElementIndex={3}
|
||||
>
|
||||
<ContextReader />
|
||||
</Contexts>
|
||||
)
|
||||
);
|
||||
|
||||
expect(context).not.toBeNull();
|
||||
expect(context.rootID).toBe(
|
||||
store.getRootIDForElement(((store.getElementIDAtIndex(3): any): number))
|
||||
);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should not select the root ID matching the Components tab selection if it has no profiling data', async done => {
|
||||
const Parent = () => <Child />;
|
||||
const Child = () => null;
|
||||
|
||||
const containerOne = document.createElement('div');
|
||||
const containerTwo = document.createElement('div');
|
||||
utils.act(() => ReactDOM.render(<Parent />, containerOne));
|
||||
utils.act(() => ReactDOM.render(<Parent />, containerTwo));
|
||||
expect(store).toMatchSnapshot('mounted');
|
||||
|
||||
// Profile and record updates to only the first root.
|
||||
await utils.actAsync(() => store.profilerStore.startProfiling());
|
||||
await utils.actAsync(() => ReactDOM.render(<Parent />, containerOne));
|
||||
await utils.actAsync(() => store.profilerStore.stopProfiling());
|
||||
|
||||
let context: Context = ((null: any): Context);
|
||||
function ContextReader() {
|
||||
context = React.useContext(ProfilerContext);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Select an element within the second root.
|
||||
await utils.actAsync(() =>
|
||||
TestRenderer.create(
|
||||
<Contexts
|
||||
defaultSelectedElementID={store.getElementIDAtIndex(3)}
|
||||
defaultSelectedElementIndex={3}
|
||||
>
|
||||
<ContextReader />
|
||||
</Contexts>
|
||||
)
|
||||
);
|
||||
|
||||
// Verify the default profiling root is the first one.
|
||||
expect(context).not.toBeNull();
|
||||
expect(context.rootID).toBe(
|
||||
store.getRootIDForElement(((store.getElementIDAtIndex(0): any): number))
|
||||
);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should maintain root selection between profiling sessions so long as there is data for that root', async done => {
|
||||
const Parent = () => <Child />;
|
||||
const Child = () => null;
|
||||
|
||||
const containerA = document.createElement('div');
|
||||
const containerB = document.createElement('div');
|
||||
utils.act(() => ReactDOM.render(<Parent />, containerA));
|
||||
utils.act(() => ReactDOM.render(<Parent />, containerB));
|
||||
expect(store).toMatchSnapshot('mounted');
|
||||
|
||||
// Profile and record updates.
|
||||
await utils.actAsync(() => store.profilerStore.startProfiling());
|
||||
await utils.actAsync(() => ReactDOM.render(<Parent />, containerA));
|
||||
await utils.actAsync(() => ReactDOM.render(<Parent />, containerB));
|
||||
await utils.actAsync(() => store.profilerStore.stopProfiling());
|
||||
|
||||
let context: Context = ((null: any): Context);
|
||||
let dispatch: DispatcherContext = ((null: any): DispatcherContext);
|
||||
let selectedElementID = null;
|
||||
function ContextReader() {
|
||||
context = React.useContext(ProfilerContext);
|
||||
dispatch = React.useContext(TreeDispatcherContext);
|
||||
selectedElementID = React.useContext(TreeStateContext).selectedElementID;
|
||||
return null;
|
||||
}
|
||||
|
||||
const id = ((store.getElementIDAtIndex(3): any): number);
|
||||
|
||||
// Select an element within the second root.
|
||||
await utils.actAsync(() =>
|
||||
TestRenderer.create(
|
||||
<Contexts defaultSelectedElementID={id} defaultSelectedElementIndex={3}>
|
||||
<ContextReader />
|
||||
</Contexts>
|
||||
)
|
||||
);
|
||||
|
||||
expect(selectedElementID).toBe(id);
|
||||
|
||||
// Profile and record more updates to both roots
|
||||
await utils.actAsync(() => store.profilerStore.startProfiling());
|
||||
await utils.actAsync(() => ReactDOM.render(<Parent />, containerA));
|
||||
await utils.actAsync(() => ReactDOM.render(<Parent />, containerB));
|
||||
await utils.actAsync(() => store.profilerStore.stopProfiling());
|
||||
|
||||
const otherID = ((store.getElementIDAtIndex(0): any): number);
|
||||
|
||||
// Change the selected element within a the Components tab.
|
||||
utils.act(() => dispatch({ type: 'SELECT_ELEMENT_AT_INDEX', payload: 0 }));
|
||||
|
||||
// Verify that the initial Profiler root selection is maintained.
|
||||
expect(selectedElementID).toBe(otherID);
|
||||
expect(context).not.toBeNull();
|
||||
expect(context.rootID).toBe(store.getRootIDForElement(id));
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should sync selected element in the Components tab too, provided the element is a match', async done => {
|
||||
const GrandParent = ({ includeChild }) => (
|
||||
<Parent includeChild={includeChild} />
|
||||
);
|
||||
const Parent = ({ includeChild }) => (includeChild ? <Child /> : null);
|
||||
const Child = () => null;
|
||||
|
||||
const container = document.createElement('div');
|
||||
utils.act(() =>
|
||||
ReactDOM.render(<GrandParent includeChild={true} />, container)
|
||||
);
|
||||
expect(store).toMatchSnapshot('mounted');
|
||||
|
||||
const parentID = ((store.getElementIDAtIndex(1): any): number);
|
||||
const childID = ((store.getElementIDAtIndex(2): any): number);
|
||||
|
||||
// Profile and record updates.
|
||||
await utils.actAsync(() => store.profilerStore.startProfiling());
|
||||
await utils.actAsync(() =>
|
||||
ReactDOM.render(<GrandParent includeChild={true} />, container)
|
||||
);
|
||||
await utils.actAsync(() =>
|
||||
ReactDOM.render(<GrandParent includeChild={false} />, container)
|
||||
);
|
||||
await utils.actAsync(() => store.profilerStore.stopProfiling());
|
||||
|
||||
expect(store).toMatchSnapshot('updated');
|
||||
|
||||
let context: Context = ((null: any): Context);
|
||||
let selectedElementID = null;
|
||||
function ContextReader() {
|
||||
context = React.useContext(ProfilerContext);
|
||||
selectedElementID = React.useContext(TreeStateContext).selectedElementID;
|
||||
return null;
|
||||
}
|
||||
|
||||
await utils.actAsync(() =>
|
||||
TestRenderer.create(
|
||||
<Contexts>
|
||||
<ContextReader />
|
||||
</Contexts>
|
||||
)
|
||||
);
|
||||
expect(selectedElementID).toBeNull();
|
||||
|
||||
// Select an element in the Profiler tab and verify that the selection is synced to the Components tab.
|
||||
await utils.actAsync(() => context.selectFiber(parentID, 'Parent'));
|
||||
expect(selectedElementID).toBe(parentID);
|
||||
|
||||
// We expect a "no element found" warning.
|
||||
// Let's hide it from the test console though.
|
||||
spyOn(console, 'warn');
|
||||
|
||||
// Select an unmounted element and verify no Components tab selection doesn't change.
|
||||
await utils.actAsync(() => context.selectFiber(childID, 'Child'));
|
||||
expect(selectedElementID).toBe(parentID);
|
||||
|
||||
expect(console.warn).toHaveBeenCalledWith(
|
||||
`No element found with id "${childID}"`
|
||||
);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
// @flow
|
||||
|
||||
import type Store from 'src/devtools/store';
|
||||
|
||||
describe('ProfilerStore', () => {
|
||||
let React;
|
||||
let ReactDOM;
|
||||
let store: Store;
|
||||
let utils;
|
||||
|
||||
beforeEach(() => {
|
||||
utils = require('./utils');
|
||||
utils.beforeEachProfiling();
|
||||
|
||||
store = global.store;
|
||||
store.collapseNodesByDefault = false;
|
||||
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
});
|
||||
|
||||
it('should not remove profiling data when roots are unmounted', async () => {
|
||||
const Parent = ({ count }) =>
|
||||
new Array(count)
|
||||
.fill(true)
|
||||
.map((_, index) => <Child key={index} duration={index} />);
|
||||
const Child = () => <div>Hi!</div>;
|
||||
|
||||
const containerA = document.createElement('div');
|
||||
const containerB = document.createElement('div');
|
||||
|
||||
utils.act(() => {
|
||||
ReactDOM.render(<Parent key="A" count={3} />, containerA);
|
||||
ReactDOM.render(<Parent key="B" count={2} />, containerB);
|
||||
});
|
||||
|
||||
utils.act(() => store.profilerStore.startProfiling());
|
||||
|
||||
utils.act(() => {
|
||||
ReactDOM.render(<Parent key="A" count={4} />, containerA);
|
||||
ReactDOM.render(<Parent key="B" count={1} />, containerB);
|
||||
});
|
||||
|
||||
utils.act(() => store.profilerStore.stopProfiling());
|
||||
|
||||
const rootA = store.roots[0];
|
||||
const rootB = store.roots[1];
|
||||
|
||||
utils.act(() => ReactDOM.unmountComponentAtNode(containerB));
|
||||
|
||||
expect(store.profilerStore.getDataForRoot(rootA)).not.toBeNull();
|
||||
|
||||
utils.act(() => ReactDOM.unmountComponentAtNode(containerA));
|
||||
|
||||
expect(store.profilerStore.getDataForRoot(rootB)).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should not allow new/saved profiling data to be set while profiling is in progress', () => {
|
||||
utils.act(() => store.profilerStore.startProfiling());
|
||||
const fauxProfilingData = {
|
||||
dataForRoots: new Map(),
|
||||
};
|
||||
spyOn(console, 'warn');
|
||||
store.profilerStore.profilingData = fauxProfilingData;
|
||||
expect(store.profilerStore.profilingData).not.toBe(fauxProfilingData);
|
||||
expect(console.warn).toHaveBeenCalledTimes(1);
|
||||
expect(console.warn).toHaveBeenCalledWith(
|
||||
'Profiling data cannot be updated while profiling is in progress.'
|
||||
);
|
||||
utils.act(() => store.profilerStore.stopProfiling());
|
||||
store.profilerStore.profilingData = fauxProfilingData;
|
||||
expect(store.profilerStore.profilingData).toBe(fauxProfilingData);
|
||||
});
|
||||
});
|
||||
@@ -1,479 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import typeof ReactTestRenderer from 'react-test-renderer';
|
||||
import type Bridge from 'src/bridge';
|
||||
import type Store from 'src/devtools/store';
|
||||
|
||||
describe('profiling', () => {
|
||||
let React;
|
||||
let ReactDOM;
|
||||
let Scheduler;
|
||||
let SchedulerTracing;
|
||||
let TestRenderer: ReactTestRenderer;
|
||||
let bridge: Bridge;
|
||||
let store: Store;
|
||||
let utils;
|
||||
|
||||
const exportImportHelper = (rendererID: number, rootID: number) => {
|
||||
const {
|
||||
prepareProfilingExport,
|
||||
prepareProfilingImport,
|
||||
} = require('src/devtools/views/Profiler/utils');
|
||||
|
||||
let exportedProfilingSummary;
|
||||
bridge.addListener('exportFile', ({ contents }) => {
|
||||
exportedProfilingSummary = contents;
|
||||
});
|
||||
|
||||
utils.act(() => {
|
||||
const exportProfilingSummary = prepareProfilingExport(
|
||||
store.profilingOperations,
|
||||
store.profilingSnapshots,
|
||||
rootID,
|
||||
rendererID
|
||||
);
|
||||
bridge.send('exportProfilingSummary', exportProfilingSummary);
|
||||
});
|
||||
|
||||
expect(exportedProfilingSummary).toBeDefined();
|
||||
|
||||
const importedProfilingSummary = prepareProfilingImport(
|
||||
((exportedProfilingSummary: any): string)
|
||||
);
|
||||
|
||||
// Sanity check that profiling snapshots are serialized correctly.
|
||||
expect(store.profilingSnapshots.get(rootID)).toEqual(
|
||||
importedProfilingSummary.profilingSnapshots.get(rootID)
|
||||
);
|
||||
|
||||
// Snapshot the JSON-parsed object, rather than the raw string, because Jest formats the diff nicer.
|
||||
expect(importedProfilingSummary).toMatchSnapshot('exported data');
|
||||
|
||||
utils.act(() => {
|
||||
store.importedProfilingData = importedProfilingSummary;
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
utils = require('./utils');
|
||||
utils.beforeEachProfiling();
|
||||
|
||||
bridge = global.bridge;
|
||||
store = global.store;
|
||||
store.collapseNodesByDefault = false;
|
||||
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
Scheduler = require('scheduler');
|
||||
SchedulerTracing = require('scheduler/tracing');
|
||||
TestRenderer = utils.requireTestRenderer();
|
||||
});
|
||||
|
||||
it('should throw if importing older/unsupported data', () => {
|
||||
const {
|
||||
prepareProfilingImport,
|
||||
} = require('src/devtools/views/Profiler/utils');
|
||||
expect(() =>
|
||||
prepareProfilingImport(
|
||||
JSON.stringify({
|
||||
version: 0,
|
||||
})
|
||||
)
|
||||
).toThrow('Unsupported profiler export version "0"');
|
||||
});
|
||||
|
||||
describe('ProfilingSummary', () => {
|
||||
it('should be collected for each commit', async done => {
|
||||
const Parent = ({ count }) => {
|
||||
Scheduler.advanceTime(10);
|
||||
const children = new Array(count)
|
||||
.fill(true)
|
||||
.map((_, index) => <Child key={index} duration={index} />);
|
||||
return (
|
||||
<React.Fragment>
|
||||
{children}
|
||||
<MemoizedChild duration={1} />
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
const Child = ({ duration }) => {
|
||||
Scheduler.advanceTime(duration);
|
||||
return null;
|
||||
};
|
||||
const MemoizedChild = React.memo(Child);
|
||||
|
||||
const container = document.createElement('div');
|
||||
|
||||
utils.act(() => ReactDOM.render(<Parent count={2} />, container));
|
||||
utils.act(() => store.startProfiling());
|
||||
utils.act(() => ReactDOM.render(<Parent count={3} />, container));
|
||||
utils.act(() => ReactDOM.render(<Parent count={1} />, container));
|
||||
utils.act(() => ReactDOM.render(<Parent count={0} />, container));
|
||||
utils.act(() => store.stopProfiling());
|
||||
|
||||
let profilingSummary = null;
|
||||
|
||||
function Suspender({ previousPofilingSummary, rendererID, rootID }) {
|
||||
profilingSummary = store.profilingCache.ProfilingSummary.read({
|
||||
rendererID,
|
||||
rootID,
|
||||
});
|
||||
if (previousPofilingSummary != null) {
|
||||
expect(profilingSummary).toEqual(previousPofilingSummary);
|
||||
} else {
|
||||
expect(profilingSummary).toMatchSnapshot('ProfilingSummary');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const rendererID = utils.getRendererID();
|
||||
const rootID = store.roots[0];
|
||||
|
||||
await utils.actSuspense(() =>
|
||||
TestRenderer.create(
|
||||
<React.Suspense fallback={null}>
|
||||
<Suspender
|
||||
previousPofilingSummary={null}
|
||||
rendererID={rendererID}
|
||||
rootID={rootID}
|
||||
/>
|
||||
</React.Suspense>
|
||||
)
|
||||
);
|
||||
|
||||
expect(profilingSummary).not.toBeNull();
|
||||
|
||||
exportImportHelper(rendererID, rootID);
|
||||
|
||||
await utils.actSuspense(() =>
|
||||
TestRenderer.create(
|
||||
<React.Suspense fallback={null}>
|
||||
<Suspender
|
||||
previousPofilingSummary={profilingSummary}
|
||||
rendererID={rendererID}
|
||||
rootID={rootID}
|
||||
/>
|
||||
</React.Suspense>
|
||||
)
|
||||
);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('CommitDetails', () => {
|
||||
it('should be collected for each commit', async done => {
|
||||
const Parent = ({ count }) => {
|
||||
Scheduler.advanceTime(10);
|
||||
const children = new Array(count)
|
||||
.fill(true)
|
||||
.map((_, index) => <Child key={index} duration={index} />);
|
||||
return (
|
||||
<React.Fragment>
|
||||
{children}
|
||||
<MemoizedChild duration={1} />
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
const Child = ({ duration }) => {
|
||||
Scheduler.advanceTime(duration);
|
||||
return null;
|
||||
};
|
||||
const MemoizedChild = React.memo(Child);
|
||||
|
||||
const container = document.createElement('div');
|
||||
|
||||
utils.act(() => store.startProfiling());
|
||||
utils.act(() => ReactDOM.render(<Parent count={2} />, container));
|
||||
utils.act(() => ReactDOM.render(<Parent count={3} />, container));
|
||||
utils.act(() => ReactDOM.render(<Parent count={1} />, container));
|
||||
utils.act(() => ReactDOM.render(<Parent count={0} />, container));
|
||||
utils.act(() => store.stopProfiling());
|
||||
|
||||
const allCommitDetails = [];
|
||||
|
||||
function Suspender({
|
||||
commitIndex,
|
||||
previousCommitDetails,
|
||||
rendererID,
|
||||
rootID,
|
||||
}) {
|
||||
const commitDetails = store.profilingCache.CommitDetails.read({
|
||||
commitIndex,
|
||||
rendererID,
|
||||
rootID,
|
||||
});
|
||||
if (previousCommitDetails != null) {
|
||||
expect(commitDetails).toEqual(previousCommitDetails);
|
||||
} else {
|
||||
allCommitDetails.push(commitDetails);
|
||||
expect(commitDetails).toMatchSnapshot(
|
||||
`CommitDetails commitIndex: ${commitIndex}`
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const rendererID = utils.getRendererID();
|
||||
const rootID = store.roots[0];
|
||||
|
||||
for (let commitIndex = 0; commitIndex < 4; commitIndex++) {
|
||||
await utils.actSuspense(() => {
|
||||
TestRenderer.create(
|
||||
<React.Suspense fallback={null}>
|
||||
<Suspender
|
||||
commitIndex={commitIndex}
|
||||
previousCommitDetails={null}
|
||||
rendererID={rendererID}
|
||||
rootID={rootID}
|
||||
/>
|
||||
</React.Suspense>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
expect(allCommitDetails).toHaveLength(4);
|
||||
|
||||
exportImportHelper(rendererID, rootID);
|
||||
|
||||
for (let commitIndex = 0; commitIndex < 4; commitIndex++) {
|
||||
await utils.actSuspense(() => {
|
||||
TestRenderer.create(
|
||||
<React.Suspense fallback={null}>
|
||||
<Suspender
|
||||
commitIndex={commitIndex}
|
||||
previousCommitDetails={allCommitDetails[commitIndex]}
|
||||
rendererID={rendererID}
|
||||
rootID={rootID}
|
||||
/>
|
||||
</React.Suspense>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('FiberCommits', () => {
|
||||
it('should be collected for each rendered fiber', async done => {
|
||||
const Parent = ({ count }) => {
|
||||
Scheduler.advanceTime(10);
|
||||
const children = new Array(count)
|
||||
.fill(true)
|
||||
.map((_, index) => <Child key={index} duration={index} />);
|
||||
return (
|
||||
<React.Fragment>
|
||||
{children}
|
||||
<MemoizedChild duration={1} />
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
const Child = ({ duration }) => {
|
||||
Scheduler.advanceTime(duration);
|
||||
return null;
|
||||
};
|
||||
const MemoizedChild = React.memo(Child);
|
||||
|
||||
const container = document.createElement('div');
|
||||
|
||||
utils.act(() => store.startProfiling());
|
||||
utils.act(() => ReactDOM.render(<Parent count={1} />, container));
|
||||
utils.act(() => ReactDOM.render(<Parent count={2} />, container));
|
||||
utils.act(() => ReactDOM.render(<Parent count={3} />, container));
|
||||
utils.act(() => store.stopProfiling());
|
||||
|
||||
const allFiberCommits = [];
|
||||
|
||||
function Suspender({
|
||||
fiberID,
|
||||
previousFiberCommits,
|
||||
rendererID,
|
||||
rootID,
|
||||
}) {
|
||||
const fiberCommits = store.profilingCache.FiberCommits.read({
|
||||
fiberID,
|
||||
rendererID,
|
||||
rootID,
|
||||
});
|
||||
if (previousFiberCommits != null) {
|
||||
expect(fiberCommits).toEqual(previousFiberCommits);
|
||||
} else {
|
||||
allFiberCommits.push(fiberCommits);
|
||||
expect(fiberCommits).toMatchSnapshot(
|
||||
`FiberCommits: element ${fiberID}`
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const rendererID = utils.getRendererID();
|
||||
const rootID = store.roots[0];
|
||||
|
||||
for (let index = 0; index < store.numElements; index++) {
|
||||
await utils.actSuspense(() => {
|
||||
const fiberID = store.getElementIDAtIndex(index);
|
||||
if (fiberID == null) {
|
||||
throw Error(`Unexpected null ID for element at index ${index}`);
|
||||
}
|
||||
TestRenderer.create(
|
||||
<React.Suspense fallback={null}>
|
||||
<Suspender
|
||||
fiberID={fiberID}
|
||||
previousFiberCommits={null}
|
||||
rendererID={rendererID}
|
||||
rootID={rootID}
|
||||
/>
|
||||
</React.Suspense>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
expect(allFiberCommits).toHaveLength(store.numElements);
|
||||
|
||||
exportImportHelper(rendererID, rootID);
|
||||
|
||||
for (let index = 0; index < store.numElements; index++) {
|
||||
await utils.actSuspense(() => {
|
||||
const fiberID = store.getElementIDAtIndex(index);
|
||||
if (fiberID == null) {
|
||||
throw Error(`Unexpected null ID for element at index ${index}`);
|
||||
}
|
||||
TestRenderer.create(
|
||||
<React.Suspense fallback={null}>
|
||||
<Suspender
|
||||
fiberID={fiberID}
|
||||
previousFiberCommits={allFiberCommits[index]}
|
||||
rendererID={rendererID}
|
||||
rootID={rootID}
|
||||
/>
|
||||
</React.Suspense>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Interactions', () => {
|
||||
it('should be collected for every traced interaction', async done => {
|
||||
const Parent = ({ count }) => {
|
||||
Scheduler.advanceTime(10);
|
||||
const children = new Array(count)
|
||||
.fill(true)
|
||||
.map((_, index) => <Child key={index} duration={index} />);
|
||||
return (
|
||||
<React.Fragment>
|
||||
{children}
|
||||
<MemoizedChild duration={1} />
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
const Child = ({ duration }) => {
|
||||
Scheduler.advanceTime(duration);
|
||||
return null;
|
||||
};
|
||||
const MemoizedChild = React.memo(Child);
|
||||
|
||||
const container = document.createElement('div');
|
||||
|
||||
utils.act(() => store.startProfiling());
|
||||
utils.act(() =>
|
||||
SchedulerTracing.unstable_trace(
|
||||
'mount: one child',
|
||||
Scheduler.unstable_now(),
|
||||
() => ReactDOM.render(<Parent count={1} />, container)
|
||||
)
|
||||
);
|
||||
utils.act(() =>
|
||||
SchedulerTracing.unstable_trace(
|
||||
'update: two children',
|
||||
Scheduler.unstable_now(),
|
||||
() => ReactDOM.render(<Parent count={2} />, container)
|
||||
)
|
||||
);
|
||||
utils.act(() => store.stopProfiling());
|
||||
|
||||
let interactions = null;
|
||||
|
||||
function Suspender({ previousInteractions, rendererID, rootID }) {
|
||||
interactions = store.profilingCache.Interactions.read({
|
||||
rendererID,
|
||||
rootID,
|
||||
});
|
||||
if (previousInteractions != null) {
|
||||
expect(interactions).toEqual(previousInteractions);
|
||||
} else {
|
||||
expect(interactions).toMatchSnapshot('Interactions');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const rendererID = utils.getRendererID();
|
||||
const rootID = store.roots[0];
|
||||
|
||||
await utils.actSuspense(() =>
|
||||
TestRenderer.create(
|
||||
<React.Suspense fallback={null}>
|
||||
<Suspender
|
||||
previousInteractions={null}
|
||||
rendererID={rendererID}
|
||||
rootID={rootID}
|
||||
/>
|
||||
</React.Suspense>
|
||||
)
|
||||
);
|
||||
|
||||
expect(interactions).not.toBeNull();
|
||||
|
||||
exportImportHelper(rendererID, rootID);
|
||||
|
||||
await utils.actSuspense(() =>
|
||||
TestRenderer.create(
|
||||
<React.Suspense fallback={null}>
|
||||
<Suspender
|
||||
previousInteractions={interactions}
|
||||
rendererID={rendererID}
|
||||
rootID={rootID}
|
||||
/>
|
||||
</React.Suspense>
|
||||
)
|
||||
);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove profiling data when roots are unmounted', async () => {
|
||||
const Parent = ({ count }) =>
|
||||
new Array(count)
|
||||
.fill(true)
|
||||
.map((_, index) => <Child key={index} duration={index} />);
|
||||
const Child = () => <div>Hi!</div>;
|
||||
|
||||
const containerA = document.createElement('div');
|
||||
const containerB = document.createElement('div');
|
||||
|
||||
utils.act(() => {
|
||||
ReactDOM.render(<Parent key="A" count={3} />, containerA);
|
||||
ReactDOM.render(<Parent key="B" count={2} />, containerB);
|
||||
});
|
||||
|
||||
utils.act(() => store.startProfiling());
|
||||
|
||||
utils.act(() => {
|
||||
ReactDOM.render(<Parent key="A" count={4} />, containerA);
|
||||
ReactDOM.render(<Parent key="B" count={1} />, containerB);
|
||||
});
|
||||
|
||||
utils.act(() => ReactDOM.unmountComponentAtNode(containerB));
|
||||
|
||||
utils.act(() => ReactDOM.unmountComponentAtNode(containerA));
|
||||
|
||||
utils.act(() => store.stopProfiling());
|
||||
|
||||
// Assert all maps are empty
|
||||
store.assertExpectedRootMapSizes();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,455 @@
|
||||
// @flow
|
||||
|
||||
import typeof ReactTestRenderer from 'react-test-renderer';
|
||||
import type Bridge from 'src/bridge';
|
||||
import type Store from 'src/devtools/store';
|
||||
|
||||
describe('ProfilingCache', () => {
|
||||
let React;
|
||||
let ReactDOM;
|
||||
let Scheduler;
|
||||
let SchedulerTracing;
|
||||
let TestRenderer: ReactTestRenderer;
|
||||
let bridge: Bridge;
|
||||
let store: Store;
|
||||
let utils;
|
||||
|
||||
beforeEach(() => {
|
||||
utils = require('./utils');
|
||||
utils.beforeEachProfiling();
|
||||
|
||||
bridge = global.bridge;
|
||||
store = global.store;
|
||||
store.collapseNodesByDefault = false;
|
||||
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
Scheduler = require('scheduler');
|
||||
SchedulerTracing = require('scheduler/tracing');
|
||||
TestRenderer = utils.requireTestRenderer();
|
||||
});
|
||||
|
||||
it('should collect data for each root (including ones added or mounted after profiling started)', () => {
|
||||
const Parent = ({ count }) => {
|
||||
Scheduler.advanceTime(10);
|
||||
const children = new Array(count)
|
||||
.fill(true)
|
||||
.map((_, index) => <Child key={index} duration={index} />);
|
||||
return (
|
||||
<React.Fragment>
|
||||
{children}
|
||||
<MemoizedChild duration={1} />
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
const Child = ({ duration }) => {
|
||||
Scheduler.advanceTime(duration);
|
||||
return null;
|
||||
};
|
||||
const MemoizedChild = React.memo(Child);
|
||||
|
||||
const containerA = document.createElement('div');
|
||||
const containerB = document.createElement('div');
|
||||
const containerC = document.createElement('div');
|
||||
|
||||
utils.act(() => ReactDOM.render(<Parent count={2} />, containerA));
|
||||
utils.act(() => ReactDOM.render(<Parent count={1} />, containerB));
|
||||
utils.act(() => store.profilerStore.startProfiling());
|
||||
utils.act(() => ReactDOM.render(<Parent count={3} />, containerA));
|
||||
utils.act(() => ReactDOM.render(<Parent count={1} />, containerC));
|
||||
utils.act(() => ReactDOM.render(<Parent count={1} />, containerA));
|
||||
utils.act(() => ReactDOM.unmountComponentAtNode(containerB));
|
||||
utils.act(() => ReactDOM.render(<Parent count={0} />, containerA));
|
||||
utils.act(() => store.profilerStore.stopProfiling());
|
||||
|
||||
let allProfilingDataForRoots = [];
|
||||
|
||||
function Validator({ previousProfilingDataForRoot, rootID }) {
|
||||
const profilingDataForRoot = store.profilerStore.getDataForRoot(rootID);
|
||||
if (previousProfilingDataForRoot != null) {
|
||||
expect(profilingDataForRoot).toEqual(previousProfilingDataForRoot);
|
||||
} else {
|
||||
expect(profilingDataForRoot).toMatchSnapshot(
|
||||
`Data for root ${profilingDataForRoot.displayName}`
|
||||
);
|
||||
}
|
||||
allProfilingDataForRoots.push(profilingDataForRoot);
|
||||
return null;
|
||||
}
|
||||
|
||||
const dataForRoots =
|
||||
store.profilerStore.profilingData !== null
|
||||
? store.profilerStore.profilingData.dataForRoots
|
||||
: null;
|
||||
|
||||
expect(dataForRoots).not.toBeNull();
|
||||
|
||||
if (dataForRoots !== null) {
|
||||
dataForRoots.forEach(dataForRoot => {
|
||||
utils.act(() =>
|
||||
TestRenderer.create(
|
||||
<Validator
|
||||
previousProfilingDataForRoot={null}
|
||||
rootID={dataForRoot.rootID}
|
||||
/>
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
expect(allProfilingDataForRoots).toHaveLength(3);
|
||||
|
||||
utils.exportImportHelper(bridge, store);
|
||||
|
||||
allProfilingDataForRoots.forEach(profilingDataForRoot => {
|
||||
utils.act(() =>
|
||||
TestRenderer.create(
|
||||
<Validator
|
||||
previousProfilingDataForRoot={profilingDataForRoot}
|
||||
rootID={profilingDataForRoot.rootID}
|
||||
/>
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should collect data for each commit', () => {
|
||||
const Parent = ({ count }) => {
|
||||
Scheduler.advanceTime(10);
|
||||
const children = new Array(count)
|
||||
.fill(true)
|
||||
.map((_, index) => <Child key={index} duration={index} />);
|
||||
return (
|
||||
<React.Fragment>
|
||||
{children}
|
||||
<MemoizedChild duration={1} />
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
const Child = ({ duration }) => {
|
||||
Scheduler.advanceTime(duration);
|
||||
return null;
|
||||
};
|
||||
const MemoizedChild = React.memo(Child);
|
||||
|
||||
const container = document.createElement('div');
|
||||
|
||||
utils.act(() => store.profilerStore.startProfiling());
|
||||
utils.act(() => ReactDOM.render(<Parent count={2} />, container));
|
||||
utils.act(() => ReactDOM.render(<Parent count={3} />, container));
|
||||
utils.act(() => ReactDOM.render(<Parent count={1} />, container));
|
||||
utils.act(() => ReactDOM.render(<Parent count={0} />, container));
|
||||
utils.act(() => store.profilerStore.stopProfiling());
|
||||
|
||||
const allCommitData = [];
|
||||
|
||||
function Validator({ commitIndex, previousCommitDetails, rootID }) {
|
||||
const commitData = store.profilerStore.getCommitData(rootID, commitIndex);
|
||||
if (previousCommitDetails != null) {
|
||||
expect(commitData).toEqual(previousCommitDetails);
|
||||
} else {
|
||||
allCommitData.push(commitData);
|
||||
expect(commitData).toMatchSnapshot(
|
||||
`CommitDetails commitIndex: ${commitIndex}`
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const rootID = store.roots[0];
|
||||
|
||||
for (let commitIndex = 0; commitIndex < 4; commitIndex++) {
|
||||
utils.act(() => {
|
||||
TestRenderer.create(
|
||||
<Validator
|
||||
commitIndex={commitIndex}
|
||||
previousCommitDetails={null}
|
||||
rootID={rootID}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
expect(allCommitData).toHaveLength(4);
|
||||
|
||||
utils.exportImportHelper(bridge, store);
|
||||
|
||||
for (let commitIndex = 0; commitIndex < 4; commitIndex++) {
|
||||
utils.act(() => {
|
||||
TestRenderer.create(
|
||||
<Validator
|
||||
commitIndex={commitIndex}
|
||||
previousCommitDetails={allCommitData[commitIndex]}
|
||||
rootID={rootID}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('should calculate a self duration based on actual children (not filtered children)', () => {
|
||||
store.componentFilters = [utils.createDisplayNameFilter('^Parent$')];
|
||||
|
||||
const Grandparent = () => {
|
||||
Scheduler.advanceTime(10);
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Parent key="one" />
|
||||
<Parent key="two" />
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
const Parent = () => {
|
||||
Scheduler.advanceTime(2);
|
||||
return <Child />;
|
||||
};
|
||||
const Child = () => {
|
||||
Scheduler.advanceTime(1);
|
||||
return null;
|
||||
};
|
||||
|
||||
utils.act(() => store.profilerStore.startProfiling());
|
||||
utils.act(() =>
|
||||
ReactDOM.render(<Grandparent />, document.createElement('div'))
|
||||
);
|
||||
utils.act(() => store.profilerStore.stopProfiling());
|
||||
|
||||
let commitData = null;
|
||||
|
||||
function Validator({ commitIndex, rootID }) {
|
||||
commitData = store.profilerStore.getCommitData(rootID, commitIndex);
|
||||
expect(commitData).toMatchSnapshot(
|
||||
`CommitDetails with filtered self durations`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
const rootID = store.roots[0];
|
||||
|
||||
utils.act(() => {
|
||||
TestRenderer.create(<Validator commitIndex={0} rootID={rootID} />);
|
||||
});
|
||||
|
||||
expect(commitData).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should calculate self duration correctly for suspended views', async done => {
|
||||
let data;
|
||||
const getData = () => {
|
||||
if (data) {
|
||||
return data;
|
||||
} else {
|
||||
throw new Promise(resolve => {
|
||||
data = 'abc';
|
||||
resolve(data);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const Parent = () => {
|
||||
Scheduler.advanceTime(10);
|
||||
return (
|
||||
<React.Suspense fallback={<Fallback />}>
|
||||
<Async />
|
||||
</React.Suspense>
|
||||
);
|
||||
};
|
||||
const Fallback = () => {
|
||||
Scheduler.advanceTime(2);
|
||||
return 'Fallback...';
|
||||
};
|
||||
const Async = () => {
|
||||
Scheduler.advanceTime(3);
|
||||
const data = getData();
|
||||
return data;
|
||||
};
|
||||
|
||||
utils.act(() => store.profilerStore.startProfiling());
|
||||
await utils.actAsync(() =>
|
||||
ReactDOM.render(<Parent />, document.createElement('div'))
|
||||
);
|
||||
utils.act(() => store.profilerStore.stopProfiling());
|
||||
|
||||
const allCommitData = [];
|
||||
|
||||
function Validator({ commitIndex, rootID }) {
|
||||
const commitData = store.profilerStore.getCommitData(rootID, commitIndex);
|
||||
allCommitData.push(commitData);
|
||||
expect(commitData).toMatchSnapshot(
|
||||
`CommitDetails with filtered self durations`
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
const rootID = store.roots[0];
|
||||
|
||||
for (let commitIndex = 0; commitIndex < 2; commitIndex++) {
|
||||
utils.act(() => {
|
||||
TestRenderer.create(
|
||||
<Validator commitIndex={commitIndex} rootID={rootID} />
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
expect(allCommitData).toHaveLength(2);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should collect data for each rendered fiber', () => {
|
||||
const Parent = ({ count }) => {
|
||||
Scheduler.advanceTime(10);
|
||||
const children = new Array(count)
|
||||
.fill(true)
|
||||
.map((_, index) => <Child key={index} duration={index} />);
|
||||
return (
|
||||
<React.Fragment>
|
||||
{children}
|
||||
<MemoizedChild duration={1} />
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
const Child = ({ duration }) => {
|
||||
Scheduler.advanceTime(duration);
|
||||
return null;
|
||||
};
|
||||
const MemoizedChild = React.memo(Child);
|
||||
|
||||
const container = document.createElement('div');
|
||||
|
||||
utils.act(() => store.profilerStore.startProfiling());
|
||||
utils.act(() => ReactDOM.render(<Parent count={1} />, container));
|
||||
utils.act(() => ReactDOM.render(<Parent count={2} />, container));
|
||||
utils.act(() => ReactDOM.render(<Parent count={3} />, container));
|
||||
utils.act(() => store.profilerStore.stopProfiling());
|
||||
|
||||
const allFiberCommits = [];
|
||||
|
||||
function Validator({ fiberID, previousFiberCommits, rootID }) {
|
||||
const fiberCommits = store.profilerStore.profilingCache.getFiberCommits({
|
||||
fiberID,
|
||||
rootID,
|
||||
});
|
||||
if (previousFiberCommits != null) {
|
||||
expect(fiberCommits).toEqual(previousFiberCommits);
|
||||
} else {
|
||||
allFiberCommits.push(fiberCommits);
|
||||
expect(fiberCommits).toMatchSnapshot(
|
||||
`FiberCommits: element ${fiberID}`
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const rootID = store.roots[0];
|
||||
|
||||
for (let index = 0; index < store.numElements; index++) {
|
||||
utils.act(() => {
|
||||
const fiberID = store.getElementIDAtIndex(index);
|
||||
if (fiberID == null) {
|
||||
throw Error(`Unexpected null ID for element at index ${index}`);
|
||||
}
|
||||
TestRenderer.create(
|
||||
<Validator
|
||||
fiberID={fiberID}
|
||||
previousFiberCommits={null}
|
||||
rootID={rootID}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
expect(allFiberCommits).toHaveLength(store.numElements);
|
||||
|
||||
utils.exportImportHelper(bridge, store);
|
||||
|
||||
for (let index = 0; index < store.numElements; index++) {
|
||||
utils.act(() => {
|
||||
const fiberID = store.getElementIDAtIndex(index);
|
||||
if (fiberID == null) {
|
||||
throw Error(`Unexpected null ID for element at index ${index}`);
|
||||
}
|
||||
TestRenderer.create(
|
||||
<Validator
|
||||
fiberID={fiberID}
|
||||
previousFiberCommits={allFiberCommits[index]}
|
||||
rootID={rootID}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('should report every traced interaction', () => {
|
||||
const Parent = ({ count }) => {
|
||||
Scheduler.advanceTime(10);
|
||||
const children = new Array(count)
|
||||
.fill(true)
|
||||
.map((_, index) => <Child key={index} duration={index} />);
|
||||
return (
|
||||
<React.Fragment>
|
||||
{children}
|
||||
<MemoizedChild duration={1} />
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
const Child = ({ duration }) => {
|
||||
Scheduler.advanceTime(duration);
|
||||
return null;
|
||||
};
|
||||
const MemoizedChild = React.memo(Child);
|
||||
|
||||
const container = document.createElement('div');
|
||||
|
||||
utils.act(() => store.profilerStore.startProfiling());
|
||||
utils.act(() =>
|
||||
SchedulerTracing.unstable_trace(
|
||||
'mount: one child',
|
||||
Scheduler.unstable_now(),
|
||||
() => ReactDOM.render(<Parent count={1} />, container)
|
||||
)
|
||||
);
|
||||
utils.act(() =>
|
||||
SchedulerTracing.unstable_trace(
|
||||
'update: two children',
|
||||
Scheduler.unstable_now(),
|
||||
() => ReactDOM.render(<Parent count={2} />, container)
|
||||
)
|
||||
);
|
||||
utils.act(() => store.profilerStore.stopProfiling());
|
||||
|
||||
let interactions = null;
|
||||
|
||||
function Validator({ previousInteractions, rootID }) {
|
||||
interactions = store.profilerStore.profilingCache.getInteractionsChartData(
|
||||
{
|
||||
rootID,
|
||||
}
|
||||
).interactions;
|
||||
if (previousInteractions != null) {
|
||||
expect(interactions).toEqual(previousInteractions);
|
||||
} else {
|
||||
expect(interactions).toMatchSnapshot('Interactions');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const rootID = store.roots[0];
|
||||
|
||||
utils.act(() =>
|
||||
TestRenderer.create(
|
||||
<Validator previousInteractions={null} rootID={rootID} />
|
||||
)
|
||||
);
|
||||
|
||||
expect(interactions).not.toBeNull();
|
||||
|
||||
utils.exportImportHelper(bridge, store);
|
||||
|
||||
utils.act(() =>
|
||||
TestRenderer.create(
|
||||
<Validator previousInteractions={interactions} rootID={rootID} />
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -27,7 +27,7 @@ describe('profiling charts', () => {
|
||||
});
|
||||
|
||||
describe('flamegraph chart', () => {
|
||||
it('should contain valid data', async done => {
|
||||
it('should contain valid data', () => {
|
||||
const Parent = ({ count }) => {
|
||||
Scheduler.advanceTime(10);
|
||||
return (
|
||||
@@ -47,7 +47,7 @@ describe('profiling charts', () => {
|
||||
|
||||
const container = document.createElement('div');
|
||||
|
||||
utils.act(() => store.startProfiling());
|
||||
utils.act(() => store.profilerStore.startProfiling());
|
||||
utils.act(() =>
|
||||
SchedulerTracing.unstable_trace('mount', Scheduler.unstable_now(), () =>
|
||||
ReactDOM.render(<Parent />, container)
|
||||
@@ -60,66 +60,49 @@ describe('profiling charts', () => {
|
||||
() => ReactDOM.render(<Parent />, container)
|
||||
)
|
||||
);
|
||||
utils.act(() => store.stopProfiling());
|
||||
utils.act(() => store.profilerStore.stopProfiling());
|
||||
|
||||
let suspenseResolved = false;
|
||||
let renderFinished = false;
|
||||
|
||||
function Suspender({ commitIndex, rendererID, rootID }) {
|
||||
const profilingSummary = store.profilingCache.ProfilingSummary.read({
|
||||
rendererID,
|
||||
function Validator({ commitIndex, rootID }) {
|
||||
const commitTree = store.profilerStore.profilingCache.getCommitTree({
|
||||
commitIndex,
|
||||
rootID,
|
||||
});
|
||||
const commitDetails = store.profilingCache.CommitDetails.read({
|
||||
commitIndex,
|
||||
rendererID,
|
||||
rootID,
|
||||
});
|
||||
suspenseResolved = true;
|
||||
const commitTree = store.profilingCache.getCommitTree({
|
||||
commitIndex,
|
||||
profilingSummary,
|
||||
});
|
||||
const chartData = store.profilingCache.getFlamegraphChartData({
|
||||
commitDetails,
|
||||
commitIndex,
|
||||
commitTree,
|
||||
});
|
||||
const chartData = store.profilerStore.profilingCache.getFlamegraphChartData(
|
||||
{
|
||||
commitIndex,
|
||||
commitTree,
|
||||
rootID,
|
||||
}
|
||||
);
|
||||
expect(commitTree).toMatchSnapshot(`${commitIndex}: CommitTree`);
|
||||
expect(chartData).toMatchSnapshot(
|
||||
`${commitIndex}: FlamegraphChartData`
|
||||
);
|
||||
renderFinished = true;
|
||||
return null;
|
||||
}
|
||||
|
||||
const rendererID = utils.getRendererID();
|
||||
const rootID = store.roots[0];
|
||||
|
||||
for (let commitIndex = 0; commitIndex < 2; commitIndex++) {
|
||||
suspenseResolved = false;
|
||||
|
||||
await utils.actSuspense(() =>
|
||||
renderFinished = false;
|
||||
utils.act(() => {
|
||||
TestRenderer.create(
|
||||
<React.Suspense fallback={null}>
|
||||
<Suspender
|
||||
commitIndex={commitIndex}
|
||||
rendererID={rendererID}
|
||||
rootID={rootID}
|
||||
/>
|
||||
</React.Suspense>
|
||||
)
|
||||
);
|
||||
<Validator commitIndex={commitIndex} rootID={rootID} />
|
||||
);
|
||||
});
|
||||
|
||||
expect(suspenseResolved).toBe(true);
|
||||
expect(renderFinished).toBe(true);
|
||||
}
|
||||
|
||||
expect(suspenseResolved).toBe(true);
|
||||
|
||||
done();
|
||||
expect(renderFinished).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ranked chart', () => {
|
||||
it('should contain valid data', async done => {
|
||||
it('should contain valid data', () => {
|
||||
const Parent = ({ count }) => {
|
||||
Scheduler.advanceTime(10);
|
||||
return (
|
||||
@@ -139,7 +122,7 @@ describe('profiling charts', () => {
|
||||
|
||||
const container = document.createElement('div');
|
||||
|
||||
utils.act(() => store.startProfiling());
|
||||
utils.act(() => store.profilerStore.startProfiling());
|
||||
utils.act(() =>
|
||||
SchedulerTracing.unstable_trace('mount', Scheduler.unstable_now(), () =>
|
||||
ReactDOM.render(<Parent />, container)
|
||||
@@ -152,62 +135,45 @@ describe('profiling charts', () => {
|
||||
() => ReactDOM.render(<Parent />, container)
|
||||
)
|
||||
);
|
||||
utils.act(() => store.stopProfiling());
|
||||
utils.act(() => store.profilerStore.stopProfiling());
|
||||
|
||||
let suspenseResolved = false;
|
||||
let renderFinished = false;
|
||||
|
||||
function Suspender({ commitIndex, rendererID, rootID }) {
|
||||
const profilingSummary = store.profilingCache.ProfilingSummary.read({
|
||||
rendererID,
|
||||
function Validator({ commitIndex, rootID }) {
|
||||
const commitTree = store.profilerStore.profilingCache.getCommitTree({
|
||||
commitIndex,
|
||||
rootID,
|
||||
});
|
||||
const commitDetails = store.profilingCache.CommitDetails.read({
|
||||
commitIndex,
|
||||
rendererID,
|
||||
rootID,
|
||||
});
|
||||
suspenseResolved = true;
|
||||
const commitTree = store.profilingCache.getCommitTree({
|
||||
commitIndex,
|
||||
profilingSummary,
|
||||
});
|
||||
const chartData = store.profilingCache.getRankedChartData({
|
||||
commitDetails,
|
||||
commitIndex,
|
||||
commitTree,
|
||||
});
|
||||
const chartData = store.profilerStore.profilingCache.getRankedChartData(
|
||||
{
|
||||
commitIndex,
|
||||
commitTree,
|
||||
rootID,
|
||||
}
|
||||
);
|
||||
expect(commitTree).toMatchSnapshot(`${commitIndex}: CommitTree`);
|
||||
expect(chartData).toMatchSnapshot(`${commitIndex}: RankedChartData`);
|
||||
renderFinished = true;
|
||||
return null;
|
||||
}
|
||||
|
||||
const rendererID = utils.getRendererID();
|
||||
const rootID = store.roots[0];
|
||||
|
||||
for (let commitIndex = 0; commitIndex < 2; commitIndex++) {
|
||||
suspenseResolved = false;
|
||||
|
||||
await utils.actSuspense(() =>
|
||||
renderFinished = false;
|
||||
utils.act(() => {
|
||||
TestRenderer.create(
|
||||
<React.Suspense fallback={null}>
|
||||
<Suspender
|
||||
commitIndex={commitIndex}
|
||||
rendererID={rendererID}
|
||||
rootID={rootID}
|
||||
/>
|
||||
</React.Suspense>
|
||||
)
|
||||
);
|
||||
<Validator commitIndex={commitIndex} rootID={rootID} />
|
||||
);
|
||||
});
|
||||
|
||||
expect(suspenseResolved).toBe(true);
|
||||
expect(renderFinished).toBe(true);
|
||||
}
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('interactions', () => {
|
||||
it('should contain valid data', async done => {
|
||||
it('should contain valid data', () => {
|
||||
const Parent = ({ count }) => {
|
||||
Scheduler.advanceTime(10);
|
||||
return (
|
||||
@@ -227,7 +193,7 @@ describe('profiling charts', () => {
|
||||
|
||||
const container = document.createElement('div');
|
||||
|
||||
utils.act(() => store.startProfiling());
|
||||
utils.act(() => store.profilerStore.startProfiling());
|
||||
utils.act(() =>
|
||||
SchedulerTracing.unstable_trace('mount', Scheduler.unstable_now(), () =>
|
||||
ReactDOM.render(<Parent />, container)
|
||||
@@ -240,50 +206,33 @@ describe('profiling charts', () => {
|
||||
() => ReactDOM.render(<Parent />, container)
|
||||
)
|
||||
);
|
||||
utils.act(() => store.stopProfiling());
|
||||
utils.act(() => store.profilerStore.stopProfiling());
|
||||
|
||||
let suspenseResolved = false;
|
||||
let renderFinished = false;
|
||||
|
||||
function Suspender({ commitIndex, rendererID, rootID }) {
|
||||
const profilingSummary = store.profilingCache.ProfilingSummary.read({
|
||||
rendererID,
|
||||
rootID,
|
||||
});
|
||||
const interactions = store.profilingCache.Interactions.read({
|
||||
rendererID,
|
||||
rootID,
|
||||
});
|
||||
suspenseResolved = true;
|
||||
const chartData = store.profilingCache.getInteractionsChartData({
|
||||
interactions,
|
||||
profilingSummary,
|
||||
});
|
||||
function Validator({ commitIndex, rootID }) {
|
||||
const chartData = store.profilerStore.profilingCache.getInteractionsChartData(
|
||||
{
|
||||
rootID,
|
||||
}
|
||||
);
|
||||
expect(chartData).toMatchSnapshot('Interactions');
|
||||
renderFinished = true;
|
||||
return null;
|
||||
}
|
||||
|
||||
const rendererID = utils.getRendererID();
|
||||
const rootID = store.roots[0];
|
||||
|
||||
for (let commitIndex = 0; commitIndex < 2; commitIndex++) {
|
||||
suspenseResolved = false;
|
||||
|
||||
await utils.actSuspense(() =>
|
||||
renderFinished = false;
|
||||
utils.act(() => {
|
||||
TestRenderer.create(
|
||||
<React.Suspense fallback={null}>
|
||||
<Suspender
|
||||
commitIndex={commitIndex}
|
||||
rendererID={rendererID}
|
||||
rootID={rootID}
|
||||
/>
|
||||
</React.Suspense>
|
||||
)
|
||||
);
|
||||
<Validator commitIndex={commitIndex} rootID={rootID} />
|
||||
);
|
||||
});
|
||||
|
||||
expect(suspenseResolved).toBe(true);
|
||||
expect(renderFinished).toBe(true);
|
||||
}
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,7 +24,7 @@ describe('commit tree', () => {
|
||||
TestRenderer = utils.requireTestRenderer();
|
||||
});
|
||||
|
||||
it('should be able to rebuild the store tree for each commit', async done => {
|
||||
it('should be able to rebuild the store tree for each commit', () => {
|
||||
const Parent = ({ count }) => {
|
||||
Scheduler.advanceTime(10);
|
||||
return new Array(count)
|
||||
@@ -38,50 +38,37 @@ describe('commit tree', () => {
|
||||
|
||||
const container = document.createElement('div');
|
||||
|
||||
utils.act(() => store.startProfiling());
|
||||
utils.act(() => store.profilerStore.startProfiling());
|
||||
utils.act(() => ReactDOM.render(<Parent count={1} />, container));
|
||||
utils.act(() => ReactDOM.render(<Parent count={3} />, container));
|
||||
utils.act(() => ReactDOM.render(<Parent count={2} />, container));
|
||||
utils.act(() => ReactDOM.render(<Parent count={0} />, container));
|
||||
utils.act(() => store.stopProfiling());
|
||||
utils.act(() => store.profilerStore.stopProfiling());
|
||||
|
||||
let suspenseResolved = false;
|
||||
let renderFinished = false;
|
||||
|
||||
function Suspender({ commitIndex, rendererID, rootID }) {
|
||||
const profilingSummary = store.profilingCache.ProfilingSummary.read({
|
||||
rendererID,
|
||||
function Validator({ commitIndex, rootID }) {
|
||||
const commitTree = store.profilerStore.profilingCache.getCommitTree({
|
||||
commitIndex,
|
||||
rootID,
|
||||
});
|
||||
suspenseResolved = true;
|
||||
const commitTree = store.profilingCache.getCommitTree({
|
||||
commitIndex,
|
||||
profilingSummary,
|
||||
});
|
||||
expect(commitTree).toMatchSnapshot(`${commitIndex}: CommitTree`);
|
||||
renderFinished = true;
|
||||
return null;
|
||||
}
|
||||
|
||||
const rendererID = utils.getRendererID();
|
||||
const rootID = store.roots[0];
|
||||
|
||||
for (let commitIndex = 0; commitIndex < 4; commitIndex++) {
|
||||
suspenseResolved = false;
|
||||
renderFinished = false;
|
||||
|
||||
await utils.actSuspense(() =>
|
||||
utils.act(() => {
|
||||
TestRenderer.create(
|
||||
<React.Suspense fallback={null}>
|
||||
<Suspender
|
||||
commitIndex={commitIndex}
|
||||
rendererID={rendererID}
|
||||
rootID={rootID}
|
||||
/>
|
||||
</React.Suspense>
|
||||
)
|
||||
);
|
||||
<Validator commitIndex={commitIndex} rootID={rootID} />
|
||||
);
|
||||
});
|
||||
|
||||
expect(suspenseResolved).toBe(true);
|
||||
expect(renderFinished).toBe(true);
|
||||
}
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
// @flow
|
||||
|
||||
describe('profiling utils', () => {
|
||||
let utils;
|
||||
|
||||
beforeEach(() => {
|
||||
utils = require('src/devtools/views/Profiler/utils');
|
||||
});
|
||||
|
||||
it('should throw if importing older/unsupported data', () => {
|
||||
expect(() =>
|
||||
utils.prepareProfilingDataFrontendFromExport(
|
||||
({
|
||||
version: 0,
|
||||
dataForRoots: [],
|
||||
}: any)
|
||||
)
|
||||
).toThrow('Unsupported profiler export version "0"');
|
||||
});
|
||||
});
|
||||
@@ -53,9 +53,11 @@ env.beforeEach(() => {
|
||||
|
||||
initBackend(hook, agent, global);
|
||||
|
||||
const store = new Store(bridge);
|
||||
|
||||
global.agent = agent;
|
||||
global.bridge = bridge;
|
||||
global.store = new Store(bridge);
|
||||
global.store = store;
|
||||
});
|
||||
env.afterEach(() => {
|
||||
delete global.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
||||
|
||||
@@ -778,4 +778,23 @@ describe('Store', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('detects and updates profiling support based on the attached roots', () => {
|
||||
const Component = () => null;
|
||||
|
||||
const containerA = document.createElement('div');
|
||||
const containerB = document.createElement('div');
|
||||
|
||||
expect(store.supportsProfiling).toBe(false);
|
||||
|
||||
act(() => ReactDOM.render(<Component />, containerA));
|
||||
expect(store.supportsProfiling).toBe(true);
|
||||
|
||||
act(() => ReactDOM.render(<Component />, containerB));
|
||||
act(() => ReactDOM.unmountComponentAtNode(containerA));
|
||||
expect(store.supportsProfiling).toBe(true);
|
||||
|
||||
act(() => ReactDOM.unmountComponentAtNode(containerB));
|
||||
expect(store.supportsProfiling).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,47 +1,14 @@
|
||||
// @flow
|
||||
|
||||
import type Store from 'src/devtools/store';
|
||||
|
||||
describe('Store component filters', () => {
|
||||
let React;
|
||||
let ReactDOM;
|
||||
let TestUtils;
|
||||
let Types;
|
||||
let store;
|
||||
|
||||
const createElementTypeFilter = (elementType, isEnabled = true) => ({
|
||||
type: Types.ComponentFilterElementType,
|
||||
isEnabled,
|
||||
value: elementType,
|
||||
});
|
||||
|
||||
const createDisplayNameFilter = (source, isEnabled = true) => {
|
||||
let isValid = true;
|
||||
try {
|
||||
new RegExp(source);
|
||||
} catch (error) {
|
||||
isValid = false;
|
||||
}
|
||||
return {
|
||||
type: Types.ComponentFilterDisplayName,
|
||||
isEnabled,
|
||||
isValid,
|
||||
value: source,
|
||||
};
|
||||
};
|
||||
|
||||
const createLocationFilter = (source, isEnabled = true) => {
|
||||
let isValid = true;
|
||||
try {
|
||||
new RegExp(source);
|
||||
} catch (error) {
|
||||
isValid = false;
|
||||
}
|
||||
return {
|
||||
type: Types.ComponentFilterLocation,
|
||||
isEnabled,
|
||||
isValid,
|
||||
value: source,
|
||||
};
|
||||
};
|
||||
let store: Store;
|
||||
let utils;
|
||||
|
||||
const act = (callback: Function) => {
|
||||
TestUtils.act(() => {
|
||||
@@ -59,10 +26,11 @@ describe('Store component filters', () => {
|
||||
ReactDOM = require('react-dom');
|
||||
TestUtils = require('react-dom/test-utils');
|
||||
Types = require('src/types');
|
||||
utils = require('./utils');
|
||||
});
|
||||
|
||||
it('should throw if filters are updated while profiling', () => {
|
||||
act(() => store.startProfiling());
|
||||
act(() => store.profilerStore.startProfiling());
|
||||
expect(() => (store.componentFilters = [])).toThrow(
|
||||
'Cannot modify filter preferences while profiling'
|
||||
);
|
||||
@@ -89,7 +57,7 @@ describe('Store component filters', () => {
|
||||
act(
|
||||
() =>
|
||||
(store.componentFilters = [
|
||||
createElementTypeFilter(Types.ElementTypeHostComponent),
|
||||
utils.createElementTypeFilter(Types.ElementTypeHostComponent),
|
||||
])
|
||||
);
|
||||
|
||||
@@ -98,7 +66,7 @@ describe('Store component filters', () => {
|
||||
act(
|
||||
() =>
|
||||
(store.componentFilters = [
|
||||
createElementTypeFilter(Types.ElementTypeClass),
|
||||
utils.createElementTypeFilter(Types.ElementTypeClass),
|
||||
])
|
||||
);
|
||||
|
||||
@@ -107,8 +75,8 @@ describe('Store component filters', () => {
|
||||
act(
|
||||
() =>
|
||||
(store.componentFilters = [
|
||||
createElementTypeFilter(Types.ElementTypeClass),
|
||||
createElementTypeFilter(Types.ElementTypeFunction),
|
||||
utils.createElementTypeFilter(Types.ElementTypeClass),
|
||||
utils.createElementTypeFilter(Types.ElementTypeFunction),
|
||||
])
|
||||
);
|
||||
|
||||
@@ -117,8 +85,8 @@ describe('Store component filters', () => {
|
||||
act(
|
||||
() =>
|
||||
(store.componentFilters = [
|
||||
createElementTypeFilter(Types.ElementTypeClass, false),
|
||||
createElementTypeFilter(Types.ElementTypeFunction, false),
|
||||
utils.createElementTypeFilter(Types.ElementTypeClass, false),
|
||||
utils.createElementTypeFilter(Types.ElementTypeFunction, false),
|
||||
])
|
||||
);
|
||||
|
||||
@@ -134,7 +102,7 @@ describe('Store component filters', () => {
|
||||
act(
|
||||
() =>
|
||||
(store.componentFilters = [
|
||||
createElementTypeFilter(Types.ElementTypeRoot),
|
||||
utils.createElementTypeFilter(Types.ElementTypeRoot),
|
||||
])
|
||||
);
|
||||
|
||||
@@ -159,13 +127,17 @@ describe('Store component filters', () => {
|
||||
);
|
||||
expect(store).toMatchSnapshot('1: mount');
|
||||
|
||||
act(() => (store.componentFilters = [createDisplayNameFilter('Foo')]));
|
||||
act(
|
||||
() => (store.componentFilters = [utils.createDisplayNameFilter('Foo')])
|
||||
);
|
||||
expect(store).toMatchSnapshot('2: filter "Foo"');
|
||||
|
||||
act(() => (store.componentFilters = [createDisplayNameFilter('Ba')]));
|
||||
act(() => (store.componentFilters = [utils.createDisplayNameFilter('Ba')]));
|
||||
expect(store).toMatchSnapshot('3: filter "Ba"');
|
||||
|
||||
act(() => (store.componentFilters = [createDisplayNameFilter('B.z')]));
|
||||
act(
|
||||
() => (store.componentFilters = [utils.createDisplayNameFilter('B.z')])
|
||||
);
|
||||
expect(store).toMatchSnapshot('4: filter "B.z"');
|
||||
});
|
||||
|
||||
@@ -178,7 +150,7 @@ describe('Store component filters', () => {
|
||||
act(
|
||||
() =>
|
||||
(store.componentFilters = [
|
||||
createLocationFilter(__filename.replace(__dirname, '')),
|
||||
utils.createLocationFilter(__filename.replace(__dirname, '')),
|
||||
])
|
||||
);
|
||||
|
||||
@@ -189,7 +161,7 @@ describe('Store component filters', () => {
|
||||
act(
|
||||
() =>
|
||||
(store.componentFilters = [
|
||||
createLocationFilter('this:is:a:made:up:path'),
|
||||
utils.createLocationFilter('this:is:a:made:up:path'),
|
||||
])
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,550 @@
|
||||
// @flow
|
||||
|
||||
import typeof ReactTestRenderer from 'react-test-renderer';
|
||||
import type Bridge from 'src/bridge';
|
||||
import type Store from 'src/devtools/store';
|
||||
import type {
|
||||
DispatcherContext,
|
||||
StateContext,
|
||||
} from 'src/devtools/views/Components/TreeContext';
|
||||
|
||||
describe('TreeListContext', () => {
|
||||
let React;
|
||||
let ReactDOM;
|
||||
let TestRenderer: ReactTestRenderer;
|
||||
let bridge: Bridge;
|
||||
let store: Store;
|
||||
let utils;
|
||||
|
||||
let BridgeContext;
|
||||
let StoreContext;
|
||||
let TreeContext;
|
||||
|
||||
let dispatch: DispatcherContext;
|
||||
let state: StateContext;
|
||||
|
||||
beforeEach(() => {
|
||||
utils = require('./utils');
|
||||
utils.beforeEachProfiling();
|
||||
|
||||
bridge = global.bridge;
|
||||
store = global.store;
|
||||
store.collapseNodesByDefault = false;
|
||||
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
TestRenderer = utils.requireTestRenderer();
|
||||
|
||||
BridgeContext = require('src/devtools/views/context').BridgeContext;
|
||||
StoreContext = require('src/devtools/views/context').StoreContext;
|
||||
TreeContext = require('src/devtools/views/Components/TreeContext');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Reset between tests
|
||||
dispatch = ((null: any): DispatcherContext);
|
||||
state = ((null: any): StateContext);
|
||||
});
|
||||
|
||||
const Capture = () => {
|
||||
dispatch = React.useContext(TreeContext.TreeDispatcherContext);
|
||||
state = React.useContext(TreeContext.TreeStateContext);
|
||||
return null;
|
||||
};
|
||||
|
||||
const Contexts = () => {
|
||||
return (
|
||||
<BridgeContext.Provider value={bridge}>
|
||||
<StoreContext.Provider value={store}>
|
||||
<TreeContext.TreeContextController>
|
||||
<Capture />
|
||||
</TreeContext.TreeContextController>
|
||||
</StoreContext.Provider>
|
||||
</BridgeContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
describe('tree state', () => {
|
||||
it('should select the next and previous elements in the tree', () => {
|
||||
const Grandparent = () => <Parent />;
|
||||
const Parent = () => (
|
||||
<React.Fragment>
|
||||
<Child />
|
||||
<Child />
|
||||
</React.Fragment>
|
||||
);
|
||||
const Child = () => null;
|
||||
|
||||
utils.act(() =>
|
||||
ReactDOM.render(<Grandparent />, document.createElement('div'))
|
||||
);
|
||||
|
||||
expect(store).toMatchSnapshot('0: mount');
|
||||
|
||||
let renderer;
|
||||
utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
|
||||
expect(state).toMatchSnapshot('1: initial state');
|
||||
|
||||
utils.act(() => dispatch({ type: 'SELECT_NEXT_ELEMENT_IN_TREE' }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('2: select first element');
|
||||
|
||||
while (
|
||||
state.selectedElementIndex !== null &&
|
||||
state.selectedElementIndex < store.numElements - 1
|
||||
) {
|
||||
const index = ((state.selectedElementIndex: any): number);
|
||||
utils.act(() => dispatch({ type: 'SELECT_NEXT_ELEMENT_IN_TREE' }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot(`3: select element after (${index})`);
|
||||
}
|
||||
|
||||
while (
|
||||
state.selectedElementIndex !== null &&
|
||||
state.selectedElementIndex > 0
|
||||
) {
|
||||
const index = ((state.selectedElementIndex: any): number);
|
||||
utils.act(() => dispatch({ type: 'SELECT_PREVIOUS_ELEMENT_IN_TREE' }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot(`4: select element before (${index})`);
|
||||
}
|
||||
|
||||
utils.act(() => dispatch({ type: 'SELECT_PREVIOUS_ELEMENT_IN_TREE' }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('5: select previous wraps around to last');
|
||||
|
||||
utils.act(() => dispatch({ type: 'SELECT_NEXT_ELEMENT_IN_TREE' }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('6: select next wraps around to first');
|
||||
});
|
||||
|
||||
it('should select child elements', () => {
|
||||
const Grandparent = () => (
|
||||
<React.Fragment>
|
||||
<Parent />
|
||||
<Parent />
|
||||
</React.Fragment>
|
||||
);
|
||||
const Parent = () => (
|
||||
<React.Fragment>
|
||||
<Child />
|
||||
<Child />
|
||||
</React.Fragment>
|
||||
);
|
||||
const Child = () => null;
|
||||
|
||||
utils.act(() =>
|
||||
ReactDOM.render(<Grandparent />, document.createElement('div'))
|
||||
);
|
||||
|
||||
expect(store).toMatchSnapshot('0: mount');
|
||||
|
||||
let renderer;
|
||||
utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
|
||||
expect(state).toMatchSnapshot('1: initial state');
|
||||
|
||||
utils.act(() =>
|
||||
dispatch({ type: 'SELECT_ELEMENT_AT_INDEX', payload: 0 })
|
||||
);
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('2: select first element');
|
||||
|
||||
utils.act(() => dispatch({ type: 'SELECT_CHILD_ELEMENT_IN_TREE' }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('3: select Parent');
|
||||
|
||||
utils.act(() => dispatch({ type: 'SELECT_CHILD_ELEMENT_IN_TREE' }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('4: select Child');
|
||||
|
||||
const previousState = state;
|
||||
|
||||
// There are no more children to select, so this should be a no-op
|
||||
utils.act(() => dispatch({ type: 'SELECT_CHILD_ELEMENT_IN_TREE' }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toEqual(previousState);
|
||||
});
|
||||
|
||||
it('should select parent elements and then collapse', () => {
|
||||
const Grandparent = () => (
|
||||
<React.Fragment>
|
||||
<Parent />
|
||||
<Parent />
|
||||
</React.Fragment>
|
||||
);
|
||||
const Parent = () => (
|
||||
<React.Fragment>
|
||||
<Child />
|
||||
<Child />
|
||||
</React.Fragment>
|
||||
);
|
||||
const Child = () => null;
|
||||
|
||||
utils.act(() =>
|
||||
ReactDOM.render(<Grandparent />, document.createElement('div'))
|
||||
);
|
||||
|
||||
expect(store).toMatchSnapshot('0: mount');
|
||||
|
||||
let renderer;
|
||||
utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
|
||||
expect(state).toMatchSnapshot('1: initial state');
|
||||
|
||||
const lastChildID = store.getElementIDAtIndex(store.numElements - 1);
|
||||
|
||||
utils.act(() =>
|
||||
dispatch({ type: 'SELECT_ELEMENT_BY_ID', payload: lastChildID })
|
||||
);
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('2: select last child');
|
||||
|
||||
utils.act(() => dispatch({ type: 'SELECT_PARENT_ELEMENT_IN_TREE' }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('3: select Parent');
|
||||
|
||||
utils.act(() => dispatch({ type: 'SELECT_PARENT_ELEMENT_IN_TREE' }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('4: select Grandparent');
|
||||
|
||||
const previousState = state;
|
||||
|
||||
// There are no more ancestors to select, so this should be a no-op
|
||||
utils.act(() => dispatch({ type: 'SELECT_PARENT_ELEMENT_IN_TREE' }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toEqual(previousState);
|
||||
});
|
||||
|
||||
it('should clear selection if the selected element is unmounted', async done => {
|
||||
const Grandparent = props => props.children || null;
|
||||
const Parent = props => props.children || null;
|
||||
const Child = () => null;
|
||||
|
||||
const container = document.createElement('div');
|
||||
utils.act(() =>
|
||||
ReactDOM.render(
|
||||
<Grandparent>
|
||||
<Parent>
|
||||
<Child />
|
||||
<Child />
|
||||
</Parent>
|
||||
</Grandparent>,
|
||||
container
|
||||
)
|
||||
);
|
||||
|
||||
expect(store).toMatchSnapshot('0: mount');
|
||||
|
||||
let renderer;
|
||||
utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
|
||||
expect(state).toMatchSnapshot('1: initial state');
|
||||
|
||||
utils.act(() =>
|
||||
dispatch({ type: 'SELECT_ELEMENT_AT_INDEX', payload: 3 })
|
||||
);
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('2: select second child');
|
||||
|
||||
await utils.actAsync(() =>
|
||||
ReactDOM.render(
|
||||
<Grandparent>
|
||||
<Parent />
|
||||
</Grandparent>,
|
||||
container
|
||||
)
|
||||
);
|
||||
expect(state).toMatchSnapshot(
|
||||
'3: remove children (parent should now be selected)'
|
||||
);
|
||||
|
||||
await utils.actAsync(() => ReactDOM.unmountComponentAtNode(container));
|
||||
expect(state).toMatchSnapshot(
|
||||
'4: unmount root (nothing should be selected)'
|
||||
);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('search state', () => {
|
||||
it('should find elements matching search text', () => {
|
||||
const Foo = () => null;
|
||||
const Bar = () => null;
|
||||
const Baz = () => null;
|
||||
|
||||
utils.act(() =>
|
||||
ReactDOM.render(
|
||||
<React.Fragment>
|
||||
<Foo />
|
||||
<Bar />
|
||||
<Baz />
|
||||
</React.Fragment>,
|
||||
document.createElement('div')
|
||||
)
|
||||
);
|
||||
|
||||
expect(store).toMatchSnapshot('0: mount');
|
||||
|
||||
let renderer;
|
||||
utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
|
||||
expect(state).toMatchSnapshot('1: initial state');
|
||||
|
||||
utils.act(() => dispatch({ type: 'SET_SEARCH_TEXT', payload: 'ba' }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('2: search for "ba"');
|
||||
|
||||
utils.act(() => dispatch({ type: 'SET_SEARCH_TEXT', payload: 'f' }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('3: search for "f"');
|
||||
|
||||
utils.act(() => dispatch({ type: 'SET_SEARCH_TEXT', payload: 'q' }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('4: search for "q"');
|
||||
});
|
||||
|
||||
it('should select the next and previous items within the search results', () => {
|
||||
const Foo = () => null;
|
||||
const Bar = () => null;
|
||||
const Baz = () => null;
|
||||
|
||||
utils.act(() =>
|
||||
ReactDOM.render(
|
||||
<React.Fragment>
|
||||
<Foo />
|
||||
<Baz />
|
||||
<Bar />
|
||||
<Baz />
|
||||
</React.Fragment>,
|
||||
document.createElement('div')
|
||||
)
|
||||
);
|
||||
|
||||
expect(store).toMatchSnapshot('0: mount');
|
||||
|
||||
let renderer;
|
||||
utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
|
||||
expect(state).toMatchSnapshot('1: initial state');
|
||||
|
||||
utils.act(() => dispatch({ type: 'SET_SEARCH_TEXT', payload: 'ba' }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('2: search for "ba"');
|
||||
|
||||
utils.act(() => dispatch({ type: 'GO_TO_NEXT_SEARCH_RESULT' }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('3: go to second result');
|
||||
|
||||
utils.act(() => dispatch({ type: 'GO_TO_NEXT_SEARCH_RESULT' }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('4: go to third result');
|
||||
|
||||
utils.act(() => dispatch({ type: 'GO_TO_PREVIOUS_SEARCH_RESULT' }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('5: go to second result');
|
||||
|
||||
utils.act(() => dispatch({ type: 'GO_TO_PREVIOUS_SEARCH_RESULT' }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('6: go to first result');
|
||||
|
||||
utils.act(() => dispatch({ type: 'GO_TO_PREVIOUS_SEARCH_RESULT' }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('7: wrap to last result');
|
||||
|
||||
utils.act(() => dispatch({ type: 'GO_TO_NEXT_SEARCH_RESULT' }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('8: wrap to first result');
|
||||
});
|
||||
|
||||
it('should add newly mounted elements to the search results set if they match the current text', async done => {
|
||||
const Foo = () => null;
|
||||
const Bar = () => null;
|
||||
const Baz = () => null;
|
||||
|
||||
const container = document.createElement('div');
|
||||
|
||||
utils.act(() =>
|
||||
ReactDOM.render(
|
||||
<React.Fragment>
|
||||
<Foo />
|
||||
<Bar />
|
||||
</React.Fragment>,
|
||||
container
|
||||
)
|
||||
);
|
||||
|
||||
expect(store).toMatchSnapshot('0: mount');
|
||||
|
||||
let renderer;
|
||||
utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
|
||||
expect(state).toMatchSnapshot('1: initial state');
|
||||
|
||||
utils.act(() => dispatch({ type: 'SET_SEARCH_TEXT', payload: 'ba' }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('2: search for "ba"');
|
||||
|
||||
await utils.actAsync(() =>
|
||||
ReactDOM.render(
|
||||
<React.Fragment>
|
||||
<Foo />
|
||||
<Bar />
|
||||
<Baz />
|
||||
</React.Fragment>,
|
||||
container
|
||||
)
|
||||
);
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('3: mount Baz');
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should remove unmounted elements from the search results set', async done => {
|
||||
const Foo = () => null;
|
||||
const Bar = () => null;
|
||||
const Baz = () => null;
|
||||
|
||||
const container = document.createElement('div');
|
||||
|
||||
utils.act(() =>
|
||||
ReactDOM.render(
|
||||
<React.Fragment>
|
||||
<Foo />
|
||||
<Bar />
|
||||
<Baz />
|
||||
</React.Fragment>,
|
||||
container
|
||||
)
|
||||
);
|
||||
|
||||
expect(store).toMatchSnapshot('0: mount');
|
||||
|
||||
let renderer;
|
||||
utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
|
||||
expect(state).toMatchSnapshot('1: initial state');
|
||||
|
||||
utils.act(() => dispatch({ type: 'SET_SEARCH_TEXT', payload: 'ba' }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('2: search for "ba"');
|
||||
|
||||
utils.act(() => dispatch({ type: 'GO_TO_NEXT_SEARCH_RESULT' }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('3: go to second result');
|
||||
|
||||
await utils.actAsync(() =>
|
||||
ReactDOM.render(
|
||||
<React.Fragment>
|
||||
<Foo />
|
||||
<Bar />
|
||||
</React.Fragment>,
|
||||
container
|
||||
)
|
||||
);
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('4: unmount Baz');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('owners state', () => {
|
||||
it('should support entering and existing the owners tree view', () => {
|
||||
const Grandparent = () => <Parent />;
|
||||
const Parent = () => (
|
||||
<React.Fragment>
|
||||
<Child />
|
||||
<Child />
|
||||
</React.Fragment>
|
||||
);
|
||||
const Child = () => null;
|
||||
|
||||
utils.act(() =>
|
||||
ReactDOM.render(<Grandparent />, document.createElement('div'))
|
||||
);
|
||||
|
||||
expect(store).toMatchSnapshot('0: mount');
|
||||
|
||||
let renderer;
|
||||
utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
|
||||
expect(state).toMatchSnapshot('1: initial state');
|
||||
|
||||
let parentID = ((store.getElementIDAtIndex(1): any): number);
|
||||
utils.act(() => dispatch({ type: 'SELECT_OWNER', payload: parentID }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('2: parent owners tree');
|
||||
|
||||
utils.act(() => dispatch({ type: 'RESET_OWNER_STACK' }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('3: final state');
|
||||
});
|
||||
|
||||
it('should remove an element from the owners list if it is unmounted', async done => {
|
||||
const Grandparent = ({ count }) => <Parent count={count} />;
|
||||
const Parent = ({ count }) =>
|
||||
new Array(count).fill(true).map((_, index) => <Child key={index} />);
|
||||
const Child = () => null;
|
||||
|
||||
const container = document.createElement('div');
|
||||
utils.act(() => ReactDOM.render(<Grandparent count={2} />, container));
|
||||
|
||||
expect(store).toMatchSnapshot('0: mount');
|
||||
|
||||
let renderer;
|
||||
utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
|
||||
expect(state).toMatchSnapshot('1: initial state');
|
||||
|
||||
let parentID = ((store.getElementIDAtIndex(1): any): number);
|
||||
utils.act(() => dispatch({ type: 'SELECT_OWNER', payload: parentID }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('2: parent owners tree');
|
||||
|
||||
await utils.actAsync(() =>
|
||||
ReactDOM.render(<Grandparent count={1} />, container)
|
||||
);
|
||||
expect(state).toMatchSnapshot('3: remove second child');
|
||||
|
||||
await utils.actAsync(() =>
|
||||
ReactDOM.render(<Grandparent count={0} />, container)
|
||||
);
|
||||
expect(state).toMatchSnapshot('4: remove first child');
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should exit the owners list if the current owner is unmounted', async done => {
|
||||
const Parent = props => props.children || null;
|
||||
const Child = () => null;
|
||||
|
||||
const container = document.createElement('div');
|
||||
utils.act(() =>
|
||||
ReactDOM.render(
|
||||
<Parent>
|
||||
<Child />
|
||||
</Parent>,
|
||||
container
|
||||
)
|
||||
);
|
||||
|
||||
expect(store).toMatchSnapshot('0: mount');
|
||||
|
||||
let renderer;
|
||||
utils.act(() => (renderer = TestRenderer.create(<Contexts />)));
|
||||
expect(state).toMatchSnapshot('1: initial state');
|
||||
|
||||
let childID = ((store.getElementIDAtIndex(1): any): number);
|
||||
utils.act(() => dispatch({ type: 'SELECT_OWNER', payload: childID }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('2: child owners tree');
|
||||
|
||||
await utils.actAsync(() => ReactDOM.render(<Parent />, container));
|
||||
expect(state).toMatchSnapshot('3: remove child');
|
||||
|
||||
let parentID = ((store.getElementIDAtIndex(0): any): number);
|
||||
utils.act(() => dispatch({ type: 'SELECT_OWNER', payload: parentID }));
|
||||
utils.act(() => renderer.update(<Contexts />));
|
||||
expect(state).toMatchSnapshot('4: parent owners tree');
|
||||
|
||||
await utils.actAsync(() => ReactDOM.unmountComponentAtNode(container));
|
||||
expect(state).toMatchSnapshot('5: unmount root');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
+98
-2
@@ -2,6 +2,11 @@
|
||||
|
||||
import typeof ReactTestRenderer from 'react-test-renderer';
|
||||
|
||||
import type Bridge from 'src/bridge';
|
||||
import type Store from 'src/devtools/store';
|
||||
import type { ProfilingDataFrontend } from 'src/devtools/views/Profiler/types';
|
||||
import type { ElementType } from 'src/types';
|
||||
|
||||
export function act(callback: Function): void {
|
||||
const TestUtils = require('react-dom/test-utils');
|
||||
TestUtils.act(() => {
|
||||
@@ -11,10 +16,10 @@ export function act(callback: Function): void {
|
||||
// Flush Bridge operations
|
||||
TestUtils.act(() => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
export async function actSuspense(cb: () => *): Promise<void> {
|
||||
export async function actAsync(cb: () => *): Promise<void> {
|
||||
const TestUtils = require('react-dom/test-utils');
|
||||
|
||||
// $FlowFixMe Flow doens't know about "await act()" yet
|
||||
@@ -44,6 +49,56 @@ export function beforeEachProfiling(): void {
|
||||
);
|
||||
}
|
||||
|
||||
export function createElementTypeFilter(
|
||||
elementType: ElementType,
|
||||
isEnabled: boolean = true
|
||||
) {
|
||||
const Types = require('src/types');
|
||||
return {
|
||||
type: Types.ComponentFilterElementType,
|
||||
isEnabled,
|
||||
value: elementType,
|
||||
};
|
||||
}
|
||||
|
||||
export function createDisplayNameFilter(
|
||||
source: string,
|
||||
isEnabled: boolean = true
|
||||
) {
|
||||
const Types = require('src/types');
|
||||
let isValid = true;
|
||||
try {
|
||||
new RegExp(source);
|
||||
} catch (error) {
|
||||
isValid = false;
|
||||
}
|
||||
return {
|
||||
type: Types.ComponentFilterDisplayName,
|
||||
isEnabled,
|
||||
isValid,
|
||||
value: source,
|
||||
};
|
||||
}
|
||||
|
||||
export function createLocationFilter(
|
||||
source: string,
|
||||
isEnabled: boolean = true
|
||||
) {
|
||||
const Types = require('src/types');
|
||||
let isValid = true;
|
||||
try {
|
||||
new RegExp(source);
|
||||
} catch (error) {
|
||||
isValid = false;
|
||||
}
|
||||
return {
|
||||
type: Types.ComponentFilterLocation,
|
||||
isEnabled,
|
||||
isValid,
|
||||
value: source,
|
||||
};
|
||||
}
|
||||
|
||||
export function getRendererID(): number {
|
||||
if (global.agent == null) {
|
||||
throw Error('Agent unavailable.');
|
||||
@@ -67,3 +122,44 @@ export function requireTestRenderer(): ReactTestRenderer {
|
||||
global.__REACT_DEVTOOLS_GLOBAL_HOOK__ = hook;
|
||||
}
|
||||
}
|
||||
|
||||
export function exportImportHelper(bridge: Bridge, store: Store): void {
|
||||
const { act } = require('./utils');
|
||||
const {
|
||||
prepareProfilingDataExport,
|
||||
prepareProfilingDataFrontendFromExport,
|
||||
} = require('src/devtools/views/Profiler/utils');
|
||||
|
||||
const { profilerStore } = store;
|
||||
|
||||
expect(profilerStore.profilingData).not.toBeNull();
|
||||
|
||||
const profilingDataFrontendInitial = ((profilerStore.profilingData: any): ProfilingDataFrontend);
|
||||
|
||||
const profilingDataExport = prepareProfilingDataExport(
|
||||
profilingDataFrontendInitial
|
||||
);
|
||||
|
||||
// Simulate writing/reading to disk.
|
||||
const serializedProfilingDataExport = JSON.stringify(
|
||||
profilingDataExport,
|
||||
null,
|
||||
2
|
||||
);
|
||||
const parsedProfilingDataExport = JSON.parse(serializedProfilingDataExport);
|
||||
|
||||
const profilingDataFrontend = prepareProfilingDataFrontendFromExport(
|
||||
(parsedProfilingDataExport: any)
|
||||
);
|
||||
|
||||
// Sanity check that profiling snapshots are serialized correctly.
|
||||
expect(profilingDataFrontendInitial).toEqual(profilingDataFrontend);
|
||||
|
||||
// Snapshot the JSON-parsed object, rather than the raw string, because Jest formats the diff nicer.
|
||||
expect(parsedProfilingDataExport).toMatchSnapshot('imported data');
|
||||
|
||||
act(() => {
|
||||
// Apply the new exported-then-reimported data so tests can re-run assertions.
|
||||
profilerStore.profilingData = profilingDataFrontend;
|
||||
});
|
||||
}
|
||||
+49
-126
@@ -16,6 +16,7 @@ import type {
|
||||
RendererID,
|
||||
RendererInterface,
|
||||
} from './types';
|
||||
import type { OwnersList } from 'src/devtools/views/Components/types';
|
||||
import type { Bridge, ComponentFilter } from '../types';
|
||||
|
||||
const debug = (methodName, ...args) => {
|
||||
@@ -29,7 +30,7 @@ const debug = (methodName, ...args) => {
|
||||
}
|
||||
};
|
||||
|
||||
type InspectSelectParams = {|
|
||||
type ElementAndRendererID = {|
|
||||
id: number,
|
||||
rendererID: number,
|
||||
|};
|
||||
@@ -92,13 +93,10 @@ export default class Agent extends EventEmitter {
|
||||
'clearHighlightedElementInDOM',
|
||||
this.clearHighlightedElementInDOM
|
||||
);
|
||||
bridge.addListener('exportProfilingSummary', this.exportProfilingSummary);
|
||||
bridge.addListener('getCommitDetails', this.getCommitDetails);
|
||||
bridge.addListener('getFiberCommits', this.getFiberCommits);
|
||||
bridge.addListener('getInteractions', this.getInteractions);
|
||||
bridge.addListener('getProfilingData', this.getProfilingData);
|
||||
bridge.addListener('getProfilingStatus', this.getProfilingStatus);
|
||||
bridge.addListener('getProfilingSummary', this.getProfilingSummary);
|
||||
bridge.addListener('highlightElementInDOM', this.highlightElementInDOM);
|
||||
bridge.addListener('getOwnersList', this.getOwnersList);
|
||||
bridge.addListener('inspectElement', this.inspectElement);
|
||||
bridge.addListener('logElementToConsole', this.logElementToConsole);
|
||||
bridge.addListener('overrideContext', this.overrideContext);
|
||||
@@ -143,120 +141,25 @@ export default class Agent extends EventEmitter {
|
||||
const renderer = ((this._rendererInterfaces[
|
||||
(rendererID: any)
|
||||
]: any): RendererInterface);
|
||||
return renderer.getFiberIDFromNative(node, true);
|
||||
return renderer.getFiberIDForNative(node, true);
|
||||
} catch (e) {}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
exportProfilingSummary = ({
|
||||
profilingOperations,
|
||||
profilingSnapshots,
|
||||
rendererID,
|
||||
rootID,
|
||||
}: {
|
||||
profilingOperations: Array<any>,
|
||||
profilingSnapshots: Array<any>,
|
||||
rendererID: number,
|
||||
rootID: number,
|
||||
}) => {
|
||||
getProfilingData = ({ rendererID }: {| rendererID: RendererID |}) => {
|
||||
const renderer = this._rendererInterfaces[rendererID];
|
||||
if (renderer == null) {
|
||||
console.warn(`Invalid renderer id "${rendererID}"`);
|
||||
} else {
|
||||
const rendererData = renderer.getProfilingDataForDownload(rootID);
|
||||
this._bridge.send('exportFile', {
|
||||
contents: JSON.stringify(
|
||||
{
|
||||
...rendererData,
|
||||
profilingOperations,
|
||||
profilingSnapshots,
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
filename: 'profile-data.json',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
getCommitDetails = ({
|
||||
commitIndex,
|
||||
rendererID,
|
||||
rootID,
|
||||
}: {
|
||||
commitIndex: number,
|
||||
rendererID: number,
|
||||
rootID: number,
|
||||
}) => {
|
||||
const renderer = this._rendererInterfaces[rendererID];
|
||||
if (renderer == null) {
|
||||
console.warn(`Invalid renderer id "${rendererID}"`);
|
||||
} else {
|
||||
this._bridge.send(
|
||||
'commitDetails',
|
||||
renderer.getCommitDetails(rootID, commitIndex)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
getFiberCommits = ({
|
||||
fiberID,
|
||||
rendererID,
|
||||
rootID,
|
||||
}: {
|
||||
fiberID: number,
|
||||
rendererID: number,
|
||||
rootID: number,
|
||||
}) => {
|
||||
const renderer = this._rendererInterfaces[rendererID];
|
||||
if (renderer == null) {
|
||||
console.warn(`Invalid renderer id "${rendererID}"`);
|
||||
} else {
|
||||
this._bridge.send(
|
||||
'fiberCommits',
|
||||
renderer.getFiberCommits(rootID, fiberID)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
getInteractions = ({
|
||||
rendererID,
|
||||
rootID,
|
||||
}: {
|
||||
rendererID: number,
|
||||
rootID: number,
|
||||
}) => {
|
||||
const renderer = this._rendererInterfaces[rendererID];
|
||||
if (renderer == null) {
|
||||
console.warn(`Invalid renderer id "${rendererID}"`);
|
||||
} else {
|
||||
this._bridge.send('interactions', renderer.getInteractions(rootID));
|
||||
}
|
||||
this._bridge.send('profilingData', renderer.getProfilingData());
|
||||
};
|
||||
|
||||
getProfilingStatus = () => {
|
||||
this._bridge.send('profilingStatus', this._isProfiling);
|
||||
};
|
||||
|
||||
getProfilingSummary = ({
|
||||
rendererID,
|
||||
rootID,
|
||||
}: {
|
||||
rendererID: number,
|
||||
rootID: number,
|
||||
}) => {
|
||||
const renderer = this._rendererInterfaces[rendererID];
|
||||
if (renderer == null) {
|
||||
console.warn(`Invalid renderer id "${rendererID}"`);
|
||||
} else {
|
||||
this._bridge.send(
|
||||
'profilingSummary',
|
||||
renderer.getProfilingSummary(rootID)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
clearHighlightedElementInDOM = () => {
|
||||
hideOverlay();
|
||||
};
|
||||
@@ -283,7 +186,9 @@ export default class Agent extends EventEmitter {
|
||||
|
||||
let nodes: ?Array<HTMLElement> = null;
|
||||
if (renderer !== null) {
|
||||
nodes = ((renderer.findNativeByFiberID(id): any): ?Array<HTMLElement>);
|
||||
nodes = ((renderer.findNativeNodesForFiberID(
|
||||
id
|
||||
): any): ?Array<HTMLElement>);
|
||||
}
|
||||
|
||||
if (nodes != null && nodes[0] != null) {
|
||||
@@ -303,7 +208,17 @@ export default class Agent extends EventEmitter {
|
||||
}
|
||||
};
|
||||
|
||||
inspectElement = ({ id, rendererID }: InspectSelectParams) => {
|
||||
getOwnersList = ({ id, rendererID }: ElementAndRendererID) => {
|
||||
const renderer = this._rendererInterfaces[rendererID];
|
||||
if (renderer == null) {
|
||||
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
|
||||
} else {
|
||||
const owners = renderer.getOwnersList(id);
|
||||
this._bridge.send('ownersList', ({ id, owners }: OwnersList));
|
||||
}
|
||||
};
|
||||
|
||||
inspectElement = ({ id, rendererID }: ElementAndRendererID) => {
|
||||
const renderer = this._rendererInterfaces[rendererID];
|
||||
if (renderer == null) {
|
||||
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
|
||||
@@ -312,7 +227,7 @@ export default class Agent extends EventEmitter {
|
||||
}
|
||||
};
|
||||
|
||||
logElementToConsole = ({ id, rendererID }: InspectSelectParams) => {
|
||||
logElementToConsole = ({ id, rendererID }: ElementAndRendererID) => {
|
||||
const renderer = this._rendererInterfaces[rendererID];
|
||||
if (renderer == null) {
|
||||
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
|
||||
@@ -340,13 +255,12 @@ export default class Agent extends EventEmitter {
|
||||
this._bridge.send('screenshotCaptured', { commitIndex, dataURL });
|
||||
};
|
||||
|
||||
selectElement = ({ id, rendererID }: InspectSelectParams) => {
|
||||
selectElement = ({ id, rendererID }: ElementAndRendererID) => {
|
||||
const renderer = this._rendererInterfaces[rendererID];
|
||||
if (renderer == null) {
|
||||
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
|
||||
} else {
|
||||
renderer.selectElement(id);
|
||||
this._bridge.send('selectElement');
|
||||
|
||||
// When user selects an element, stop trying to restore the selection,
|
||||
// and instead remember the current selection for the next reload.
|
||||
@@ -461,9 +375,12 @@ export default class Agent extends EventEmitter {
|
||||
|
||||
startInspectingDOM = () => {
|
||||
window.addEventListener('click', this._onClick, true);
|
||||
window.addEventListener('mousedown', this._onMouseDown, true);
|
||||
window.addEventListener('mouseup', this._onMouseUp, true);
|
||||
window.addEventListener('mouseover', this._onMouseOver, true);
|
||||
window.addEventListener('mousedown', this._onMouseEvent, true);
|
||||
window.addEventListener('mouseover', this._onMouseEvent, true);
|
||||
window.addEventListener('mouseup', this._onMouseEvent, true);
|
||||
window.addEventListener('pointerdown', this._onPointerDown, true);
|
||||
window.addEventListener('pointerover', this._onPointerOver, true);
|
||||
window.addEventListener('pointerup', this._onPointerUp, true);
|
||||
};
|
||||
|
||||
startProfiling = () => {
|
||||
@@ -481,9 +398,12 @@ export default class Agent extends EventEmitter {
|
||||
hideOverlay();
|
||||
|
||||
window.removeEventListener('click', this._onClick, true);
|
||||
window.removeEventListener('mousedown', this._onMouseDown, true);
|
||||
window.removeEventListener('mouseup', this._onMouseUp, true);
|
||||
window.removeEventListener('mouseover', this._onMouseOver, true);
|
||||
window.removeEventListener('mousedown', this._onMouseEvent, true);
|
||||
window.removeEventListener('mouseover', this._onMouseEvent, true);
|
||||
window.removeEventListener('mouseup', this._onMouseEvent, true);
|
||||
window.removeEventListener('pointerdown', this._onPointerDown, true);
|
||||
window.removeEventListener('pointerover', this._onPointerOver, true);
|
||||
window.removeEventListener('pointerup', this._onPointerUp, true);
|
||||
};
|
||||
|
||||
stopProfiling = () => {
|
||||
@@ -506,7 +426,7 @@ export default class Agent extends EventEmitter {
|
||||
}
|
||||
};
|
||||
|
||||
viewElementSource = ({ id, rendererID }: InspectSelectParams) => {
|
||||
viewElementSource = ({ id, rendererID }: ElementAndRendererID) => {
|
||||
const renderer = this._rendererInterfaces[rendererID];
|
||||
if (renderer == null) {
|
||||
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
|
||||
@@ -574,25 +494,23 @@ export default class Agent extends EventEmitter {
|
||||
event.stopPropagation();
|
||||
|
||||
this.stopInspectingDOM();
|
||||
|
||||
this._bridge.send('stopInspectingDOM', true);
|
||||
};
|
||||
|
||||
_onMouseDown = (event: MouseEvent) => {
|
||||
_onMouseEvent = (event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
_onPointerDown = (event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
this._selectFiberForNode(((event.target: any): HTMLElement));
|
||||
};
|
||||
|
||||
// While we don't do anything here, this makes choosing
|
||||
// the inspected element less invasive and less likely
|
||||
// to dismiss e.g. a context menu.
|
||||
_onMouseUp = (event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
_onMouseOver = (event: MouseEvent) => {
|
||||
_onPointerOver = (event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
@@ -605,6 +523,11 @@ export default class Agent extends EventEmitter {
|
||||
this._selectFiberForNode(target);
|
||||
};
|
||||
|
||||
_onPointerUp = (event: MouseEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
_selectFiberForNode = throttle(
|
||||
memoize((node: HTMLElement) => {
|
||||
const id = this.getIDForNode(node);
|
||||
|
||||
+17
-7
@@ -9,7 +9,7 @@ export function initBackend(
|
||||
hook: DevToolsHook,
|
||||
agent: Agent,
|
||||
global: Object
|
||||
): void {
|
||||
): () => void {
|
||||
const subs = [
|
||||
hook.sub(
|
||||
'renderer-attached',
|
||||
@@ -60,20 +60,30 @@ export function initBackend(
|
||||
});
|
||||
|
||||
// Connect any new renderers that injected themselves.
|
||||
hook.on(
|
||||
'renderer',
|
||||
({ id, renderer }: { id: number, renderer: ReactRenderer }) => {
|
||||
attachRenderer(id, renderer);
|
||||
}
|
||||
subs.push(
|
||||
hook.sub(
|
||||
'renderer',
|
||||
({ id, renderer }: { id: number, renderer: ReactRenderer }) => {
|
||||
attachRenderer(id, renderer);
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
hook.emit('react-devtools', agent);
|
||||
hook.reactDevtoolsAgent = agent;
|
||||
agent.addListener('shutdown', () => {
|
||||
const onAgentShutdown = () => {
|
||||
subs.forEach(fn => fn());
|
||||
hook.rendererInterfaces.forEach(rendererInterface => {
|
||||
rendererInterface.cleanup();
|
||||
});
|
||||
hook.reactDevtoolsAgent = null;
|
||||
};
|
||||
agent.addListener('shutdown', onAgentShutdown);
|
||||
subs.push(() => {
|
||||
agent.removeListener('shutdown', onAgentShutdown);
|
||||
});
|
||||
|
||||
return () => {
|
||||
subs.forEach(fn => fn());
|
||||
};
|
||||
}
|
||||
|
||||
+361
-278
File diff suppressed because it is too large
Load Diff
+37
-43
@@ -1,7 +1,11 @@
|
||||
// @flow
|
||||
|
||||
import type { ComponentFilter, ElementType } from 'src/types';
|
||||
import type { InspectedElement } from 'src/devtools/views/Components/types';
|
||||
import type {
|
||||
InspectedElement,
|
||||
Owner,
|
||||
} from 'src/devtools/views/Components/types';
|
||||
import type { Interaction } from 'src/devtools/views/Profiler/types';
|
||||
|
||||
type BundleType =
|
||||
| 0 // PROD
|
||||
@@ -112,41 +116,33 @@ export type ReactRenderer = {
|
||||
currentDispatcherRef?: {| current: null | Dispatcher |},
|
||||
};
|
||||
|
||||
export type InteractionBackend = {|
|
||||
id: number,
|
||||
name: string,
|
||||
export type CommitDataBackend = {|
|
||||
duration: number,
|
||||
// Tuple of fiber ID and actual duration
|
||||
fiberActualDurations: Array<[number, number]>,
|
||||
// Tuple of fiber ID and computed "self" duration
|
||||
fiberSelfDurations: Array<[number, number]>,
|
||||
interactionIDs: Array<number>,
|
||||
priorityLevel: string | null,
|
||||
timestamp: number,
|
||||
|};
|
||||
|
||||
export type CommitDetailsBackend = {|
|
||||
actualDurations: Array<number>,
|
||||
commitIndex: number,
|
||||
interactions: Array<InteractionBackend>,
|
||||
export type ProfilingDataForRootBackend = {|
|
||||
commitData: Array<CommitDataBackend>,
|
||||
displayName: string,
|
||||
// Tuple of Fiber ID and base duration
|
||||
initialTreeBaseDurations: Array<[number, number]>,
|
||||
// Tuple of Interaction ID and commit indices
|
||||
interactionCommits: Array<[number, Array<number>]>,
|
||||
interactions: Array<[number, Interaction]>,
|
||||
rootID: number,
|
||||
|};
|
||||
|
||||
export type FiberCommitsBackend = {|
|
||||
commitDurations: Array<number>,
|
||||
fiberID: number,
|
||||
rootID: number,
|
||||
|};
|
||||
|
||||
export type InteractionWithCommitsBackend = {|
|
||||
...InteractionBackend,
|
||||
commits: Array<number>,
|
||||
|};
|
||||
|
||||
export type InteractionsBackend = {|
|
||||
interactions: Array<InteractionWithCommitsBackend>,
|
||||
rootID: number,
|
||||
|};
|
||||
|
||||
export type ProfilingSummaryBackend = {|
|
||||
commitDurations: Array<number>,
|
||||
commitTimes: Array<number>,
|
||||
initialTreeBaseDurations: Array<number>,
|
||||
interactionCount: number,
|
||||
rootID: number,
|
||||
// Profiling data collected by the renderer interface.
|
||||
// This information will be passed to the frontend and combined with info it collects.
|
||||
export type ProfilingDataBackend = {|
|
||||
dataForRoots: Array<ProfilingDataForRootBackend>,
|
||||
rendererID: number,
|
||||
|};
|
||||
|
||||
export type PathFrame = {|
|
||||
@@ -162,25 +158,19 @@ export type PathMatch = {|
|
||||
|
||||
export type RendererInterface = {
|
||||
cleanup: () => void,
|
||||
findNativeByFiberID: (id: number) => ?Array<NativeType>,
|
||||
findNativeNodesForFiberID: (id: number) => ?Array<NativeType>,
|
||||
flushInitialOperations: () => void,
|
||||
getBestMatchForTrackedPath: () => PathMatch | null,
|
||||
getCommitDetails: (
|
||||
rootID: number,
|
||||
commitIndex: number
|
||||
) => CommitDetailsBackend,
|
||||
getFiberIDFromNative: (
|
||||
getFiberIDForNative: (
|
||||
component: NativeType,
|
||||
findNearestUnfilteredAncestor?: boolean
|
||||
) => number | null,
|
||||
getFiberCommits: (rootID: number, fiberID: number) => FiberCommitsBackend,
|
||||
getInteractions: (rootID: number) => InteractionsBackend,
|
||||
getProfilingDataForDownload: (rootID: number) => Object,
|
||||
getProfilingSummary: (rootID: number) => ProfilingSummaryBackend,
|
||||
getProfilingData(): ProfilingDataBackend,
|
||||
getOwnersList: (id: number) => Array<Owner> | null,
|
||||
getPathForElement: (id: number) => Array<PathFrame> | null,
|
||||
handleCommitFiberRoot: (fiber: Object) => void,
|
||||
handleCommitFiberRoot: (fiber: Object, commitPriority?: number) => void,
|
||||
handleCommitFiberUnmount: (fiber: Object) => void,
|
||||
inspectElement: (id: number) => InspectedElement | null,
|
||||
inspectElement: (id: number) => InspectedElement | number | null,
|
||||
logElementToConsole: (id: number) => void,
|
||||
overrideSuspense: (id: number, forceFallback: boolean) => void,
|
||||
prepareViewElementSource: (id: number) => void,
|
||||
@@ -219,7 +209,11 @@ export type DevToolsHook = {
|
||||
// React uses these methods.
|
||||
checkDCE: (fn: Function) => void,
|
||||
onCommitFiberUnmount: (rendererID: RendererID, fiber: Object) => void,
|
||||
onCommitFiberRoot: (rendererID: RendererID, fiber: Object) => void,
|
||||
onCommitFiberRoot: (
|
||||
rendererID: RendererID,
|
||||
fiber: Object,
|
||||
commitPriority?: number
|
||||
) => void,
|
||||
};
|
||||
|
||||
export type HooksNode = {
|
||||
|
||||
+3
-3
@@ -57,6 +57,9 @@ export default class Bridge extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
|
||||
// Queue the shutdown outgoing message for subscribers.
|
||||
this.send('shutdown');
|
||||
|
||||
// Mark this bridge as destroyed, i.e. disable its public API.
|
||||
this._isShutdown = true;
|
||||
|
||||
@@ -74,9 +77,6 @@ export default class Bridge extends EventEmitter {
|
||||
wallUnlisten();
|
||||
}
|
||||
|
||||
// Queue the shutdown outgoing message for subscribers.
|
||||
this.send('shutdown');
|
||||
|
||||
// Synchronously flush all queued outgoing messages.
|
||||
// At this step the subscribers' code may run in this call stack.
|
||||
do {
|
||||
|
||||
+1
-1
@@ -16,4 +16,4 @@ export const SESSION_STORAGE_LAST_SELECTION_KEY =
|
||||
|
||||
export const __DEBUG__ = false;
|
||||
|
||||
export const PROFILER_EXPORT_VERSION = 2;
|
||||
export const PROFILER_EXPORT_VERSION = 4;
|
||||
|
||||
@@ -0,0 +1,376 @@
|
||||
// @flow
|
||||
|
||||
import EventEmitter from 'events';
|
||||
import memoize from 'memoize-one';
|
||||
import throttle from 'lodash.throttle';
|
||||
import { prepareProfilingDataFrontendFromBackendAndStore } from './views/Profiler/utils';
|
||||
import ProfilingCache from './ProfilingCache';
|
||||
import Store from './store';
|
||||
|
||||
import type { ProfilingDataBackend } from 'src/backend/types';
|
||||
import type {
|
||||
CommitDataFrontend,
|
||||
ProfilingDataForRootFrontend,
|
||||
ProfilingDataFrontend,
|
||||
SnapshotNode,
|
||||
} from './views/Profiler/types';
|
||||
import type { Bridge } from '../types';
|
||||
|
||||
const THROTTLE_CAPTURE_SCREENSHOT_DURATION = 500;
|
||||
|
||||
export default class ProfilerStore extends EventEmitter {
|
||||
_bridge: Bridge;
|
||||
|
||||
// Suspense cache for lazily calculating derived profiling data.
|
||||
_cache: ProfilingCache;
|
||||
|
||||
// Temporary store of profiling data from the backend renderer(s).
|
||||
// This data will be converted to the ProfilingDataFrontend format after being collected from all renderers.
|
||||
_dataBackends: Array<ProfilingDataBackend> = [];
|
||||
|
||||
// Data from the most recently completed profiling session,
|
||||
// or data that has been imported from a previously exported session.
|
||||
// This object contains all necessary data to drive the Profiler UI interface,
|
||||
// even though some of it is lazily parsed/derived via the ProfilingCache.
|
||||
_dataFrontend: ProfilingDataFrontend | null = null;
|
||||
|
||||
// Snapshot of all attached renderer IDs.
|
||||
// Once profiling is finished, this snapshot will be used to query renderers for profiling data.
|
||||
//
|
||||
// This map is initialized when profiling starts and updated when a new root is added while profiling;
|
||||
// Upon completion, it is converted into the exportable ProfilingDataFrontend format.
|
||||
_initialRendererIDs: Set<number> = new Set();
|
||||
|
||||
// Snapshot of the state of the main Store (including all roots) when profiling started.
|
||||
// Once profiling is finished, this snapshot can be used along with "operations" messages emitted during profiling,
|
||||
// to reconstruct the state of each root for each commit.
|
||||
// It's okay to use a single root to store this information because node IDs are unique across all roots.
|
||||
//
|
||||
// This map is initialized when profiling starts and updated when a new root is added while profiling;
|
||||
// Upon completion, it is converted into the exportable ProfilingDataFrontend format.
|
||||
_initialSnapshotsByRootID: Map<number, Map<number, SnapshotNode>> = new Map();
|
||||
|
||||
// Map of root (id) to a list of tree mutation that occur during profiling.
|
||||
// Once profiling is finished, these mutations can be used, along with the initial tree snapshots,
|
||||
// to reconstruct the state of each root for each commit.
|
||||
//
|
||||
// This map is only updated while profiling is in progress;
|
||||
// Upon completion, it is converted into the exportable ProfilingDataFrontend format.
|
||||
_inProgressOperationsByRootID: Map<number, Array<Uint32Array>> = new Map();
|
||||
|
||||
// Map of root (id) to a Map of screenshots by commit ID.
|
||||
// Stores screenshots for each commit (when profiling).
|
||||
//
|
||||
// This map is only updated while profiling is in progress;
|
||||
// Upon completion, it is converted into the exportable ProfilingDataFrontend format.
|
||||
_inProgressScreenshotsByRootID: Map<number, Map<number, string>> = new Map();
|
||||
|
||||
// The backend is currently profiling.
|
||||
// When profiling is in progress, operations are stored so that we can later reconstruct past commit trees.
|
||||
_isProfiling: boolean = false;
|
||||
|
||||
// After profiling, data is requested from each attached renderer using this queue.
|
||||
// So long as this queue is not empty, the store is retrieving and processing profiling data from the backend.
|
||||
_rendererQueue: Set<number> = new Set();
|
||||
|
||||
_store: Store;
|
||||
|
||||
constructor(bridge: Bridge, store: Store, defaultIsProfiling: boolean) {
|
||||
super();
|
||||
|
||||
this._bridge = bridge;
|
||||
this._isProfiling = defaultIsProfiling;
|
||||
this._store = store;
|
||||
|
||||
bridge.addListener('operations', this.onBridgeOperations);
|
||||
bridge.addListener('profilingData', this.onBridgeProfilingData);
|
||||
bridge.addListener('profilingStatus', this.onProfilingStatus);
|
||||
bridge.addListener('shutdown', this.onBridgeShutdown);
|
||||
|
||||
// It's possible that profiling has already started (e.g. "reload and start profiling")
|
||||
// so the frontend needs to ask the backend for its status after mounting.
|
||||
bridge.send('getProfilingStatus');
|
||||
|
||||
this._cache = new ProfilingCache(this);
|
||||
}
|
||||
|
||||
getCommitData(rootID: number, commitIndex: number): CommitDataFrontend {
|
||||
if (this._dataFrontend !== null) {
|
||||
const dataForRoot = this._dataFrontend.dataForRoots.get(rootID);
|
||||
if (dataForRoot != null) {
|
||||
const commitDatum = dataForRoot.commitData[commitIndex];
|
||||
if (commitDatum != null) {
|
||||
return commitDatum;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw Error(
|
||||
`Could not find commit data for root "${rootID}" and commit ${commitIndex}`
|
||||
);
|
||||
}
|
||||
|
||||
getDataForRoot(rootID: number): ProfilingDataForRootFrontend {
|
||||
if (this._dataFrontend !== null) {
|
||||
const dataForRoot = this._dataFrontend.dataForRoots.get(rootID);
|
||||
if (dataForRoot != null) {
|
||||
return dataForRoot;
|
||||
}
|
||||
}
|
||||
|
||||
throw Error(`Could not find commit data for root "${rootID}"`);
|
||||
}
|
||||
|
||||
// Profiling data has been recorded for at least one root.
|
||||
get didRecordCommits(): boolean {
|
||||
return (
|
||||
this._dataFrontend !== null && this._dataFrontend.dataForRoots.size > 0
|
||||
);
|
||||
}
|
||||
|
||||
get isProcessingData(): boolean {
|
||||
return this._rendererQueue.size > 0 || this._dataBackends.length > 0;
|
||||
}
|
||||
|
||||
get isProfiling(): boolean {
|
||||
return this._isProfiling;
|
||||
}
|
||||
|
||||
get profilingCache(): ProfilingCache {
|
||||
return this._cache;
|
||||
}
|
||||
|
||||
get profilingData(): ProfilingDataFrontend | null {
|
||||
return this._dataFrontend;
|
||||
}
|
||||
set profilingData(value: ProfilingDataFrontend | null): void {
|
||||
if (this._isProfiling) {
|
||||
console.warn(
|
||||
'Profiling data cannot be updated while profiling is in progress.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this._dataBackends.splice(0);
|
||||
this._dataFrontend = value;
|
||||
this._initialRendererIDs.clear();
|
||||
this._initialSnapshotsByRootID.clear();
|
||||
this._inProgressOperationsByRootID.clear();
|
||||
this._inProgressScreenshotsByRootID.clear();
|
||||
this._cache.invalidate();
|
||||
|
||||
this.emit('profilingData');
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this._dataBackends.splice(0);
|
||||
this._dataFrontend = null;
|
||||
this._initialRendererIDs.clear();
|
||||
this._initialSnapshotsByRootID.clear();
|
||||
this._inProgressOperationsByRootID.clear();
|
||||
this._inProgressScreenshotsByRootID.clear();
|
||||
this._rendererQueue.clear();
|
||||
|
||||
// Invalidate suspense cache if profiling data is being (re-)recorded.
|
||||
// Note that we clear now because any existing data is "stale".
|
||||
this._cache.invalidate();
|
||||
|
||||
this.emit('profilingData');
|
||||
}
|
||||
|
||||
startProfiling(): void {
|
||||
this._bridge.send('startProfiling');
|
||||
|
||||
// Don't actually update the local profiling boolean yet!
|
||||
// Wait for onProfilingStatus() to confirm the status has changed.
|
||||
// This ensures the frontend and backend are in sync wrt which commits were profiled.
|
||||
// We do this to avoid mismatches on e.g. CommitTreeBuilder that would cause errors.
|
||||
}
|
||||
|
||||
stopProfiling(): void {
|
||||
this._bridge.send('stopProfiling');
|
||||
|
||||
// Don't actually update the local profiling boolean yet!
|
||||
// Wait for onProfilingStatus() to confirm the status has changed.
|
||||
// This ensures the frontend and backend are in sync wrt which commits were profiled.
|
||||
// We do this to avoid mismatches on e.g. CommitTreeBuilder that would cause errors.
|
||||
}
|
||||
|
||||
_captureScreenshot = throttle(
|
||||
memoize((rootID: number, commitIndex: number) => {
|
||||
this._bridge.send('captureScreenshot', { commitIndex, rootID });
|
||||
}),
|
||||
THROTTLE_CAPTURE_SCREENSHOT_DURATION
|
||||
);
|
||||
|
||||
_takeProfilingSnapshotRecursive = (
|
||||
elementID: number,
|
||||
profilingSnapshots: Map<number, SnapshotNode>
|
||||
) => {
|
||||
const element = this._store.getElementByID(elementID);
|
||||
if (element !== null) {
|
||||
const snapshotNode: SnapshotNode = {
|
||||
id: elementID,
|
||||
children: element.children.slice(0),
|
||||
displayName: element.displayName,
|
||||
key: element.key,
|
||||
type: element.type,
|
||||
};
|
||||
profilingSnapshots.set(elementID, snapshotNode);
|
||||
|
||||
element.children.forEach(childID =>
|
||||
this._takeProfilingSnapshotRecursive(childID, profilingSnapshots)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
onBridgeOperations = (operations: Uint32Array) => {
|
||||
if (!(operations instanceof Uint32Array)) {
|
||||
// $FlowFixMe TODO HACK Temporary workaround for the fact that Chrome is not transferring the typed array.
|
||||
operations = Uint32Array.from(Object.values(operations));
|
||||
}
|
||||
|
||||
// The first two values are always rendererID and rootID
|
||||
const rendererID = operations[0];
|
||||
const rootID = operations[1];
|
||||
|
||||
if (this._isProfiling) {
|
||||
let profilingOperations = this._inProgressOperationsByRootID.get(rootID);
|
||||
if (profilingOperations == null) {
|
||||
profilingOperations = [operations];
|
||||
this._inProgressOperationsByRootID.set(rootID, profilingOperations);
|
||||
} else {
|
||||
profilingOperations.push(operations);
|
||||
}
|
||||
|
||||
if (!this._initialRendererIDs.has(rendererID)) {
|
||||
this._initialRendererIDs.add(rendererID);
|
||||
}
|
||||
|
||||
if (!this._initialSnapshotsByRootID.has(rootID)) {
|
||||
this._initialSnapshotsByRootID.set(rootID, new Map());
|
||||
}
|
||||
|
||||
if (this._store.captureScreenshots) {
|
||||
const commitIndex = profilingOperations.length - 1;
|
||||
this._captureScreenshot(rootID, commitIndex);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onBridgeProfilingData = (dataBackend: ProfilingDataBackend) => {
|
||||
if (this._isProfiling) {
|
||||
// This should never happen, but if it does- ignore previous profiling data.
|
||||
return;
|
||||
}
|
||||
|
||||
const { rendererID } = dataBackend;
|
||||
|
||||
if (!this._rendererQueue.has(rendererID)) {
|
||||
throw Error(
|
||||
`Unexpected profiling data update from renderer "${rendererID}"`
|
||||
);
|
||||
}
|
||||
|
||||
this._dataBackends.push(dataBackend);
|
||||
this._rendererQueue.delete(rendererID);
|
||||
|
||||
if (this._rendererQueue.size === 0) {
|
||||
this._dataFrontend = prepareProfilingDataFrontendFromBackendAndStore(
|
||||
this._dataBackends,
|
||||
this._inProgressOperationsByRootID,
|
||||
this._inProgressScreenshotsByRootID,
|
||||
this._initialSnapshotsByRootID
|
||||
);
|
||||
|
||||
this._dataBackends.splice(0);
|
||||
|
||||
this.emit('isProcessingData');
|
||||
}
|
||||
};
|
||||
|
||||
onBridgeShutdown = () => {
|
||||
this._bridge.removeListener('operations', this.onBridgeOperations);
|
||||
this._bridge.removeListener('profilingData', this.onBridgeProfilingData);
|
||||
this._bridge.removeListener('profilingStatus', this.onProfilingStatus);
|
||||
this._bridge.removeListener('shutdown', this.onBridgeShutdown);
|
||||
};
|
||||
|
||||
onProfilingStatus = (isProfiling: boolean) => {
|
||||
if (isProfiling) {
|
||||
this._dataBackends.splice(0);
|
||||
this._dataFrontend = null;
|
||||
this._initialRendererIDs.clear();
|
||||
this._initialSnapshotsByRootID.clear();
|
||||
this._inProgressOperationsByRootID.clear();
|
||||
this._inProgressScreenshotsByRootID.clear();
|
||||
this._rendererQueue.clear();
|
||||
|
||||
// Record all renderer IDs initially too (in case of unmount)
|
||||
for (let rendererID of this._store.rootIDToRendererID.values()) {
|
||||
if (!this._initialRendererIDs.has(rendererID)) {
|
||||
this._initialRendererIDs.add(rendererID);
|
||||
}
|
||||
}
|
||||
|
||||
// Record snapshot of tree at the time profiling is started.
|
||||
// This info is required to handle cases of e.g. nodes being removed during profiling.
|
||||
this._store.roots.forEach(rootID => {
|
||||
const profilingSnapshots = new Map();
|
||||
this._initialSnapshotsByRootID.set(rootID, profilingSnapshots);
|
||||
this._takeProfilingSnapshotRecursive(rootID, profilingSnapshots);
|
||||
});
|
||||
}
|
||||
|
||||
if (this._isProfiling !== isProfiling) {
|
||||
this._isProfiling = isProfiling;
|
||||
|
||||
// Invalidate suspense cache if profiling data is being (re-)recorded.
|
||||
// Note that we clear again, in case any views read from the cache while profiling.
|
||||
// (That would have resolved a now-stale value without any profiling data.)
|
||||
this._cache.invalidate();
|
||||
|
||||
this.emit('isProfiling');
|
||||
|
||||
// If we've just finished a profiling session, we need to fetch data stored in each renderer interface
|
||||
// and re-assemble it on the front-end into a format (ProfilingDataFrontend) that can power the Profiler UI.
|
||||
// During this time, DevTools UI should probably not be interactive.
|
||||
if (!isProfiling) {
|
||||
this._dataBackends.splice(0);
|
||||
this._rendererQueue.clear();
|
||||
|
||||
this._initialRendererIDs.forEach(rendererID => {
|
||||
if (!this._rendererQueue.has(rendererID)) {
|
||||
this._rendererQueue.add(rendererID);
|
||||
|
||||
this._bridge.send('getProfilingData', { rendererID });
|
||||
}
|
||||
});
|
||||
|
||||
this.emit('isProcessingData');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onScreenshotCaptured = ({
|
||||
commitIndex,
|
||||
dataURL,
|
||||
rootID,
|
||||
}: {|
|
||||
commitIndex: number,
|
||||
dataURL: string,
|
||||
rootID: number,
|
||||
|}) => {
|
||||
let screenshotsForRootByCommitIndex = this._inProgressScreenshotsByRootID.get(
|
||||
rootID
|
||||
);
|
||||
if (!screenshotsForRootByCommitIndex) {
|
||||
screenshotsForRootByCommitIndex = new Map();
|
||||
this._inProgressScreenshotsByRootID.set(
|
||||
rootID,
|
||||
screenshotsForRootByCommitIndex
|
||||
);
|
||||
}
|
||||
screenshotsForRootByCommitIndex.set(commitIndex, dataURL);
|
||||
};
|
||||
}
|
||||
+54
-345
@@ -1,7 +1,6 @@
|
||||
// @flow
|
||||
|
||||
import { createResource } from './cache';
|
||||
import Store from './store';
|
||||
import ProfilerStore from './ProfilerStore';
|
||||
import {
|
||||
getCommitTree,
|
||||
invalidateCommitTrees,
|
||||
@@ -19,395 +18,105 @@ import {
|
||||
invalidateChartData as invalidateRankedChartData,
|
||||
} from 'src/devtools/views/Profiler/RankedChartBuilder';
|
||||
|
||||
import type { Resource } from './cache';
|
||||
import type { Bridge } from '../types';
|
||||
import type {
|
||||
CommitDetailsBackend,
|
||||
FiberCommitsBackend,
|
||||
InteractionsBackend,
|
||||
ProfilingSummaryBackend,
|
||||
} from 'src/backend/types';
|
||||
import type {
|
||||
CommitDetailsFrontend,
|
||||
FiberCommitsFrontend,
|
||||
InteractionsFrontend,
|
||||
InteractionWithCommitsFrontend,
|
||||
CommitTreeFrontend,
|
||||
ProfilingSummaryFrontend,
|
||||
} from 'src/devtools/views/Profiler/types';
|
||||
import type { CommitTree } from 'src/devtools/views/Profiler/types';
|
||||
import type { ChartData as FlamegraphChartData } from 'src/devtools/views/Profiler/FlamegraphChartBuilder';
|
||||
import type { ChartData as InteractionsChartData } from 'src/devtools/views/Profiler/InteractionsChartBuilder';
|
||||
import type { ChartData as RankedChartData } from 'src/devtools/views/Profiler/RankedChartBuilder';
|
||||
|
||||
type CommitDetailsParams = {|
|
||||
commitIndex: number,
|
||||
rendererID: number,
|
||||
rootID: number,
|
||||
|};
|
||||
|
||||
type FiberCommitsParams = {|
|
||||
fiberID: number,
|
||||
rendererID: number,
|
||||
rootID: number,
|
||||
|};
|
||||
|
||||
type InteractionsParams = {|
|
||||
rendererID: number,
|
||||
rootID: number,
|
||||
|};
|
||||
|
||||
type GetCommitTreeParams = {|
|
||||
commitIndex: number,
|
||||
profilingSummary: ProfilingSummaryFrontend,
|
||||
|};
|
||||
|
||||
type ProfilingSummaryParams = {|
|
||||
rendererID: number,
|
||||
rootID: number,
|
||||
|};
|
||||
|
||||
export default class ProfilingCache {
|
||||
_bridge: Bridge;
|
||||
_store: Store;
|
||||
_fiberCommits: Map<number, Array<number>> = new Map();
|
||||
_profilerStore: ProfilerStore;
|
||||
|
||||
_pendingCommitDetailsMap: Map<
|
||||
string,
|
||||
(commitDetails: CommitDetailsFrontend) => void
|
||||
> = new Map();
|
||||
|
||||
_pendingFiberCommitsMap: Map<
|
||||
string,
|
||||
(fiberCommits: FiberCommitsFrontend) => void
|
||||
> = new Map();
|
||||
|
||||
_pendingInteractionsMap: Map<
|
||||
number,
|
||||
(interactions: InteractionsFrontend) => void
|
||||
> = new Map();
|
||||
|
||||
_pendingProfileSummaryMap: Map<
|
||||
number,
|
||||
(profilingSummary: ProfilingSummaryFrontend) => void
|
||||
> = new Map();
|
||||
|
||||
CommitDetails: Resource<
|
||||
CommitDetailsParams,
|
||||
string,
|
||||
CommitDetailsFrontend
|
||||
> = createResource(
|
||||
({ commitIndex, rendererID, rootID }: CommitDetailsParams) => {
|
||||
return new Promise(resolve => {
|
||||
const importedProfilingData = this._store.importedProfilingData;
|
||||
if (importedProfilingData !== null) {
|
||||
const { commitDetails } = (importedProfilingData: any);
|
||||
if (commitDetails != null && commitIndex < commitDetails.length) {
|
||||
const response = commitDetails[commitIndex];
|
||||
this._pendingCommitDetailsMap.set(
|
||||
`${response.rootID}-${commitIndex}`,
|
||||
resolve
|
||||
);
|
||||
this.onCommitDetails(response);
|
||||
return;
|
||||
}
|
||||
} else if (this._store.profilingOperations.has(rootID)) {
|
||||
this._pendingCommitDetailsMap.set(
|
||||
`${rootID}-${commitIndex}`,
|
||||
resolve
|
||||
);
|
||||
this._bridge.send('getCommitDetails', {
|
||||
commitIndex,
|
||||
rendererID,
|
||||
rootID,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// If no profiling data was recorded for this root, skip the round trip.
|
||||
resolve({
|
||||
rootID,
|
||||
commitIndex,
|
||||
actualDurations: new Map(),
|
||||
interactions: [],
|
||||
});
|
||||
});
|
||||
},
|
||||
({ commitIndex, rendererID, rootID }: CommitDetailsParams) =>
|
||||
`${rootID}-${commitIndex}`
|
||||
);
|
||||
|
||||
FiberCommits: Resource<
|
||||
FiberCommitsParams,
|
||||
string,
|
||||
FiberCommitsFrontend
|
||||
> = createResource(
|
||||
({ fiberID, rendererID, rootID }: FiberCommitsParams) => {
|
||||
return new Promise(resolve => {
|
||||
const importedProfilingData = this._store.importedProfilingData;
|
||||
if (importedProfilingData !== null) {
|
||||
const { commitDetails } = (importedProfilingData: any);
|
||||
if (commitDetails != null) {
|
||||
const commitDurations = [];
|
||||
commitDetails.forEach(({ actualDurations }, commitIndex) => {
|
||||
for (let i = 0; i < actualDurations.length; i += 2) {
|
||||
if (actualDurations[i] === fiberID) {
|
||||
commitDurations.push(commitIndex, actualDurations[i + 1]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
this._pendingFiberCommitsMap.set(`${rootID}-${fiberID}`, resolve);
|
||||
this.onFiberCommits({
|
||||
commitDurations,
|
||||
fiberID,
|
||||
rootID,
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else if (this._store.profilingOperations.has(rootID)) {
|
||||
this._pendingFiberCommitsMap.set(`${rootID}-${fiberID}`, resolve);
|
||||
this._bridge.send('getFiberCommits', {
|
||||
fiberID,
|
||||
rendererID,
|
||||
rootID,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// If no profiling data was recorded for this root, skip the round trip.
|
||||
resolve({
|
||||
commitDurations: [],
|
||||
fiberID,
|
||||
rootID,
|
||||
});
|
||||
});
|
||||
},
|
||||
({ fiberID, rendererID, rootID }: FiberCommitsParams) =>
|
||||
`${rootID}-${fiberID}`
|
||||
);
|
||||
|
||||
Interactions: Resource<
|
||||
InteractionsParams,
|
||||
number,
|
||||
InteractionsFrontend
|
||||
> = createResource(
|
||||
({ rendererID, rootID }: InteractionsParams) => {
|
||||
return new Promise(resolve => {
|
||||
const importedProfilingData = this._store.importedProfilingData;
|
||||
if (importedProfilingData !== null) {
|
||||
const { interactions } = (importedProfilingData: any);
|
||||
if (interactions != null) {
|
||||
this._pendingInteractionsMap.set(interactions.rootID, resolve);
|
||||
this.onInteractions(interactions);
|
||||
return;
|
||||
}
|
||||
} else if (this._store.profilingOperations.has(rootID)) {
|
||||
this._pendingInteractionsMap.set(rootID, resolve);
|
||||
this._bridge.send('getInteractions', {
|
||||
rendererID,
|
||||
rootID,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// If no profiling data was recorded for this root, skip the round trip.
|
||||
resolve([]);
|
||||
});
|
||||
},
|
||||
({ rendererID, rootID }: ProfilingSummaryParams) => rootID
|
||||
);
|
||||
|
||||
ProfilingSummary: Resource<
|
||||
ProfilingSummaryParams,
|
||||
number,
|
||||
ProfilingSummaryFrontend
|
||||
> = createResource(
|
||||
({ rendererID, rootID }: ProfilingSummaryParams) => {
|
||||
return new Promise(resolve => {
|
||||
const importedProfilingData = this._store.importedProfilingData;
|
||||
if (importedProfilingData !== null) {
|
||||
const { profilingSummary } = (importedProfilingData: any);
|
||||
if (profilingSummary != null) {
|
||||
this._pendingProfileSummaryMap.set(
|
||||
profilingSummary.rootID,
|
||||
resolve
|
||||
);
|
||||
this.onProfileSummary(profilingSummary);
|
||||
return;
|
||||
}
|
||||
} else if (this._store.profilingOperations.has(rootID)) {
|
||||
this._pendingProfileSummaryMap.set(rootID, resolve);
|
||||
this._bridge.send('getProfilingSummary', { rendererID, rootID });
|
||||
return;
|
||||
}
|
||||
|
||||
// If no profiling data was recorded for this root, skip the round trip.
|
||||
resolve({
|
||||
rootID,
|
||||
commitDurations: [],
|
||||
commitTimes: [],
|
||||
initialTreeBaseDurations: new Map(),
|
||||
interactionCount: 0,
|
||||
});
|
||||
});
|
||||
},
|
||||
({ rendererID, rootID }: ProfilingSummaryParams) => rootID
|
||||
);
|
||||
|
||||
constructor(bridge: Bridge, store: Store) {
|
||||
this._bridge = bridge;
|
||||
this._store = store;
|
||||
|
||||
bridge.addListener('commitDetails', this.onCommitDetails);
|
||||
bridge.addListener('fiberCommits', this.onFiberCommits);
|
||||
bridge.addListener('interactions', this.onInteractions);
|
||||
bridge.addListener('profilingSummary', this.onProfileSummary);
|
||||
constructor(profilerStore: ProfilerStore) {
|
||||
this._profilerStore = profilerStore;
|
||||
}
|
||||
|
||||
getCommitTree = ({ commitIndex, profilingSummary }: GetCommitTreeParams) =>
|
||||
getCommitTree = ({
|
||||
commitIndex,
|
||||
rootID,
|
||||
}: {|
|
||||
commitIndex: number,
|
||||
rootID: number,
|
||||
|}) =>
|
||||
getCommitTree({
|
||||
commitIndex,
|
||||
profilingSummary,
|
||||
store: this._store,
|
||||
profilerStore: this._profilerStore,
|
||||
rootID,
|
||||
});
|
||||
|
||||
getFiberCommits = ({
|
||||
fiberID,
|
||||
rootID,
|
||||
}: {|
|
||||
fiberID: number,
|
||||
rootID: number,
|
||||
|}): Array<number> => {
|
||||
const cachedFiberCommits = this._fiberCommits.get(fiberID);
|
||||
if (cachedFiberCommits != null) {
|
||||
return cachedFiberCommits;
|
||||
}
|
||||
|
||||
const fiberCommits = [];
|
||||
const dataForRoot = this._profilerStore.getDataForRoot(rootID);
|
||||
dataForRoot.commitData.forEach((commitDatum, commitIndex) => {
|
||||
if (commitDatum.fiberActualDurations.has(fiberID)) {
|
||||
fiberCommits.push(commitIndex);
|
||||
}
|
||||
});
|
||||
|
||||
this._fiberCommits.set(fiberID, fiberCommits);
|
||||
|
||||
return fiberCommits;
|
||||
};
|
||||
|
||||
getFlamegraphChartData = ({
|
||||
commitDetails,
|
||||
commitIndex,
|
||||
commitTree,
|
||||
rootID,
|
||||
}: {|
|
||||
commitDetails: CommitDetailsFrontend,
|
||||
commitIndex: number,
|
||||
commitTree: CommitTreeFrontend,
|
||||
commitTree: CommitTree,
|
||||
rootID: number,
|
||||
|}): FlamegraphChartData =>
|
||||
getFlamegraphChartData({
|
||||
commitDetails,
|
||||
commitIndex,
|
||||
commitTree,
|
||||
profilerStore: this._profilerStore,
|
||||
rootID,
|
||||
});
|
||||
|
||||
getInteractionsChartData = ({
|
||||
interactions,
|
||||
profilingSummary,
|
||||
rootID,
|
||||
}: {|
|
||||
interactions: Array<InteractionWithCommitsFrontend>,
|
||||
profilingSummary: ProfilingSummaryFrontend,
|
||||
rootID: number,
|
||||
|}): InteractionsChartData =>
|
||||
getInteractionsChartData({
|
||||
interactions,
|
||||
profilingSummary,
|
||||
profilerStore: this._profilerStore,
|
||||
rootID,
|
||||
});
|
||||
|
||||
getRankedChartData = ({
|
||||
commitDetails,
|
||||
commitIndex,
|
||||
commitTree,
|
||||
rootID,
|
||||
}: {|
|
||||
commitDetails: CommitDetailsFrontend,
|
||||
commitIndex: number,
|
||||
commitTree: CommitTreeFrontend,
|
||||
commitTree: CommitTree,
|
||||
rootID: number,
|
||||
|}): RankedChartData =>
|
||||
getRankedChartData({
|
||||
commitDetails,
|
||||
commitIndex,
|
||||
commitTree,
|
||||
profilerStore: this._profilerStore,
|
||||
rootID,
|
||||
});
|
||||
|
||||
invalidate() {
|
||||
// Invalidate Suspense caches.
|
||||
this.CommitDetails.clear();
|
||||
this.FiberCommits.clear();
|
||||
this.Interactions.clear();
|
||||
this.ProfilingSummary.clear();
|
||||
this._fiberCommits.clear();
|
||||
|
||||
// Invalidate non-Suspense caches too.
|
||||
invalidateCommitTrees();
|
||||
invalidateFlamegraphChartData();
|
||||
invalidateInteractionsChartData();
|
||||
invalidateRankedChartData();
|
||||
|
||||
this._pendingCommitDetailsMap.clear();
|
||||
this._pendingProfileSummaryMap.clear();
|
||||
}
|
||||
|
||||
onCommitDetails = ({
|
||||
commitIndex,
|
||||
actualDurations,
|
||||
interactions,
|
||||
rootID,
|
||||
}: CommitDetailsBackend) => {
|
||||
const key = `${rootID}-${commitIndex}`;
|
||||
const resolve = this._pendingCommitDetailsMap.get(key);
|
||||
if (resolve != null) {
|
||||
this._pendingCommitDetailsMap.delete(key);
|
||||
|
||||
const actualDurationsMap = new Map();
|
||||
for (let i = 0; i < actualDurations.length; i += 2) {
|
||||
actualDurationsMap.set(actualDurations[i], actualDurations[i + 1]);
|
||||
}
|
||||
|
||||
resolve({
|
||||
rootID,
|
||||
commitIndex,
|
||||
actualDurations: actualDurationsMap,
|
||||
interactions,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onFiberCommits = ({
|
||||
commitDurations,
|
||||
fiberID,
|
||||
rootID,
|
||||
}: FiberCommitsBackend) => {
|
||||
const key = `${rootID}-${fiberID}`;
|
||||
const resolve = this._pendingFiberCommitsMap.get(key);
|
||||
if (resolve != null) {
|
||||
this._pendingFiberCommitsMap.delete(key);
|
||||
|
||||
resolve({
|
||||
commitDurations,
|
||||
fiberID,
|
||||
rootID,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onInteractions = ({ interactions, rootID }: InteractionsBackend) => {
|
||||
const resolve = this._pendingInteractionsMap.get(rootID);
|
||||
if (resolve != null) {
|
||||
this._pendingInteractionsMap.delete(rootID);
|
||||
|
||||
resolve(interactions);
|
||||
}
|
||||
};
|
||||
|
||||
onProfileSummary = ({
|
||||
commitDurations,
|
||||
commitTimes,
|
||||
initialTreeBaseDurations,
|
||||
interactionCount,
|
||||
rootID,
|
||||
}: ProfilingSummaryBackend) => {
|
||||
const resolve = this._pendingProfileSummaryMap.get(rootID);
|
||||
if (resolve != null) {
|
||||
this._pendingProfileSummaryMap.delete(rootID);
|
||||
|
||||
const initialTreeBaseDurationsMap = new Map();
|
||||
for (let i = 0; i < initialTreeBaseDurations.length; i += 2) {
|
||||
initialTreeBaseDurationsMap.set(
|
||||
initialTreeBaseDurations[i],
|
||||
initialTreeBaseDurations[i + 1]
|
||||
);
|
||||
}
|
||||
|
||||
resolve({
|
||||
rootID,
|
||||
commitDurations,
|
||||
commitTimes,
|
||||
initialTreeBaseDurations: initialTreeBaseDurationsMap,
|
||||
interactionCount,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
+20
-243
@@ -1,8 +1,6 @@
|
||||
// @flow
|
||||
|
||||
import EventEmitter from 'events';
|
||||
import memoize from 'memoize-one';
|
||||
import throttle from 'lodash.throttle';
|
||||
import { inspect } from 'util';
|
||||
import {
|
||||
TREE_OPERATION_ADD,
|
||||
@@ -17,14 +15,10 @@ import {
|
||||
utfDecodeString,
|
||||
} from '../utils';
|
||||
import { __DEBUG__ } from '../constants';
|
||||
import ProfilingCache from './ProfilingCache';
|
||||
import { printStore } from 'src/__tests__/storeSerializer';
|
||||
import ProfilerStore from './ProfilerStore';
|
||||
|
||||
import type { Element } from './views/Components/types';
|
||||
import type {
|
||||
ImportedProfilingData,
|
||||
ProfilingSnapshotNode,
|
||||
} from './views/Profiler/types';
|
||||
import type { Bridge, ComponentFilter, ElementType } from '../types';
|
||||
|
||||
const debug = (methodName, ...args) => {
|
||||
@@ -43,12 +37,9 @@ const LOCAL_STORAGE_CAPTURE_SCREENSHOTS_KEY =
|
||||
const LOCAL_STORAGE_COLLAPSE_ROOTS_BY_DEFAULT_KEY =
|
||||
'React::DevTools::collapseNodesByDefault';
|
||||
|
||||
const THROTTLE_CAPTURE_SCREENSHOT_DURATION = 500;
|
||||
|
||||
type Config = {|
|
||||
isProfiling?: boolean,
|
||||
supportsCaptureScreenshots?: boolean,
|
||||
supportsFileDownloads?: boolean,
|
||||
supportsReloadAndProfile?: boolean,
|
||||
supportsProfiling?: boolean,
|
||||
|};
|
||||
@@ -80,37 +71,11 @@ export default class Store extends EventEmitter {
|
||||
// The InspectedElementContext also relies on this mutability for its WeakMap usage.
|
||||
_idToElement: Map<number, Element> = new Map();
|
||||
|
||||
// The user has imported a previously exported profiling session.
|
||||
_importedProfilingData: ImportedProfilingData | null = null;
|
||||
|
||||
// The backend is currently profiling.
|
||||
// When profiling is in progress, operations are stored so that we can later reconstruct past commit trees.
|
||||
_isProfiling: boolean = false;
|
||||
|
||||
// Map of element (id) to the set of elements (ids) it owns.
|
||||
// This map enables getOwnersListForElement() to avoid traversing the entire tree.
|
||||
_ownersMap: Map<number, Set<number>> = new Map();
|
||||
|
||||
// Suspense cache for reading profiling data.
|
||||
_profilingCache: ProfilingCache;
|
||||
|
||||
// Map of root (id) to a list of tree mutation that occur during profiling.
|
||||
// Once profiling is finished, these mutations can be used, along with the initial tree snapshots,
|
||||
// to reconstruct the state of each root for each commit.
|
||||
_profilingOperationsByRootID: Map<number, Array<Uint32Array>> = new Map();
|
||||
|
||||
// Map of root (id) to a Map of screenshots by commit ID.
|
||||
// Stores screenshots for each commit (when profiling).
|
||||
_profilingScreenshotsByRootID: Map<number, Map<number, string>> = new Map();
|
||||
|
||||
// Snapshot of the state of the main Store (including all roots) when profiling started.
|
||||
// Once profiling is finished, this snapshot can be used along with "operations" messages emitted during profiling,
|
||||
// to reconstruct the state of each root for each commit.
|
||||
// It's okay to use a single root to store this information because node IDs are unique across all roots.
|
||||
_profilingSnapshotsByRootID: Map<
|
||||
number,
|
||||
Map<number, ProfilingSnapshotNode>
|
||||
> = new Map();
|
||||
_profilerStore: ProfilerStore;
|
||||
|
||||
// Incremented each time the store is mutated.
|
||||
// This enables a passive effect to detect a mutation between render and commit phase.
|
||||
@@ -128,7 +93,6 @@ export default class Store extends EventEmitter {
|
||||
// These options may be initially set by a confiugraiton option when constructing the Store.
|
||||
// In the case of "supportsProfiling", the option may be updated based on the injected renderers.
|
||||
_supportsCaptureScreenshots: boolean = false;
|
||||
_supportsFileDownloads: boolean = false;
|
||||
_supportsProfiling: boolean = false;
|
||||
_supportsReloadAndProfile: boolean = false;
|
||||
|
||||
@@ -150,26 +114,21 @@ export default class Store extends EventEmitter {
|
||||
|
||||
this._componentFilters = getSavedComponentFilters();
|
||||
|
||||
let isProfiling = false;
|
||||
if (config != null) {
|
||||
isProfiling = config.isProfiling === true;
|
||||
|
||||
const {
|
||||
isProfiling,
|
||||
supportsCaptureScreenshots,
|
||||
supportsFileDownloads,
|
||||
supportsProfiling,
|
||||
supportsReloadAndProfile,
|
||||
} = config;
|
||||
if (isProfiling) {
|
||||
this._isProfiling = true;
|
||||
}
|
||||
if (supportsCaptureScreenshots) {
|
||||
this._supportsCaptureScreenshots = true;
|
||||
this._captureScreenshots =
|
||||
localStorage.getItem(LOCAL_STORAGE_CAPTURE_SCREENSHOTS_KEY) ===
|
||||
'true';
|
||||
}
|
||||
if (supportsFileDownloads) {
|
||||
this._supportsFileDownloads = true;
|
||||
}
|
||||
if (supportsProfiling) {
|
||||
this._supportsProfiling = true;
|
||||
}
|
||||
@@ -180,15 +139,9 @@ export default class Store extends EventEmitter {
|
||||
|
||||
this._bridge = bridge;
|
||||
bridge.addListener('operations', this.onBridgeOperations);
|
||||
bridge.addListener('profilingStatus', this.onProfilingStatus);
|
||||
bridge.addListener('screenshotCaptured', this.onScreenshotCaptured);
|
||||
bridge.addListener('shutdown', this.onBridgeShutdown);
|
||||
|
||||
// It's possible that profiling has already started (e.g. "reload and start profiling")
|
||||
// so the frontend needs to ask the backend for its status after mounting.
|
||||
bridge.send('getProfilingStatus');
|
||||
|
||||
this._profilingCache = new ProfilingCache(bridge, this);
|
||||
this._profilerStore = new ProfilerStore(bridge, this, isProfiling);
|
||||
}
|
||||
|
||||
// This is only used in tests to avoid memory leaks.
|
||||
@@ -197,23 +150,6 @@ export default class Store extends EventEmitter {
|
||||
// The only safe time to assert these maps are empty is when the store is empty.
|
||||
this.assertMapSizeMatchesRootCount(this._idToElement, '_idToElement');
|
||||
this.assertMapSizeMatchesRootCount(this._ownersMap, '_ownersMap');
|
||||
|
||||
// These maps will be empty unless profiling mode has been started.
|
||||
// After this, their size should always match the number of roots,
|
||||
// but unless we want to track additional metadata about profiling history,
|
||||
// the only safe time to assert this is when the store is empty.
|
||||
this.assertMapSizeMatchesRootCount(
|
||||
this._profilingOperationsByRootID,
|
||||
'_profilingOperationsByRootID'
|
||||
);
|
||||
this.assertMapSizeMatchesRootCount(
|
||||
this._profilingScreenshotsByRootID,
|
||||
'_profilingScreenshotsByRootID'
|
||||
);
|
||||
this.assertMapSizeMatchesRootCount(
|
||||
this._profilingSnapshotsByRootID,
|
||||
'_profilingSnapshotsByRootID'
|
||||
);
|
||||
}
|
||||
|
||||
// These maps should always be the same size as the number of roots
|
||||
@@ -273,7 +209,7 @@ export default class Store extends EventEmitter {
|
||||
return this._componentFilters;
|
||||
}
|
||||
set componentFilters(value: Array<ComponentFilter>): void {
|
||||
if (this._isProfiling) {
|
||||
if (this._profilerStore.isProfiling) {
|
||||
// Re-mounting a tree while profiling is in progress might break a lot of assumptions.
|
||||
// If necessary, we could support this- but it doesn't seem like a necessary use case.
|
||||
throw Error('Cannot modify filter preferences while profiling');
|
||||
@@ -295,55 +231,22 @@ export default class Store extends EventEmitter {
|
||||
return this._hasOwnerMetadata;
|
||||
}
|
||||
|
||||
// Profiling data has been recorded for at least one root.
|
||||
get hasProfilingData(): boolean {
|
||||
return (
|
||||
this._importedProfilingData !== null ||
|
||||
this._profilingOperationsByRootID.size > 0
|
||||
);
|
||||
}
|
||||
|
||||
get importedProfilingData(): ImportedProfilingData | null {
|
||||
return this._importedProfilingData;
|
||||
}
|
||||
set importedProfilingData(value: ImportedProfilingData | null): void {
|
||||
this._importedProfilingData = value;
|
||||
this._profilingOperationsByRootID = new Map();
|
||||
this._profilingScreenshotsByRootID = new Map();
|
||||
this._profilingSnapshotsByRootID = new Map();
|
||||
this._profilingCache.invalidate();
|
||||
|
||||
this.emit('importedProfilingData');
|
||||
}
|
||||
|
||||
get isProfiling(): boolean {
|
||||
return this._isProfiling;
|
||||
}
|
||||
|
||||
get numElements(): number {
|
||||
return this._weightAcrossRoots;
|
||||
}
|
||||
|
||||
get profilingCache(): ProfilingCache {
|
||||
return this._profilingCache;
|
||||
}
|
||||
|
||||
get profilingOperations(): Map<number, Array<Uint32Array>> {
|
||||
return this._profilingOperationsByRootID;
|
||||
}
|
||||
|
||||
get profilingScreenshots(): Map<number, Map<number, string>> {
|
||||
return this._profilingScreenshotsByRootID;
|
||||
}
|
||||
|
||||
get profilingSnapshots(): Map<number, Map<number, ProfilingSnapshotNode>> {
|
||||
return this._profilingSnapshotsByRootID;
|
||||
get profilerStore(): ProfilerStore {
|
||||
return this._profilerStore;
|
||||
}
|
||||
|
||||
get revision(): number {
|
||||
return this._revision;
|
||||
}
|
||||
|
||||
get rootIDToRendererID(): Map<number, number> {
|
||||
return this._rootIDToRendererID;
|
||||
}
|
||||
|
||||
get roots(): $ReadOnlyArray<number> {
|
||||
return this._roots;
|
||||
}
|
||||
@@ -352,10 +255,6 @@ export default class Store extends EventEmitter {
|
||||
return this._supportsCaptureScreenshots;
|
||||
}
|
||||
|
||||
get supportsFileDownloads(): boolean {
|
||||
return this._supportsFileDownloads;
|
||||
}
|
||||
|
||||
get supportsProfiling(): boolean {
|
||||
return this._supportsProfiling;
|
||||
}
|
||||
@@ -364,19 +263,6 @@ export default class Store extends EventEmitter {
|
||||
return this._supportsReloadAndProfile;
|
||||
}
|
||||
|
||||
clearProfilingData(): void {
|
||||
this._importedProfilingData = null;
|
||||
this._profilingOperationsByRootID = new Map();
|
||||
this._profilingScreenshotsByRootID = new Map();
|
||||
this._profilingSnapshotsByRootID = new Map();
|
||||
|
||||
// Invalidate suspense cache if profiling data is being (re-)recorded.
|
||||
// Note that we clear now because any existing data is "stale".
|
||||
this._profilingCache.invalidate();
|
||||
|
||||
this.emit('isProfiling');
|
||||
}
|
||||
|
||||
containsElement(id: number): boolean {
|
||||
return this._idToElement.get(id) != null;
|
||||
}
|
||||
@@ -602,24 +488,6 @@ export default class Store extends EventEmitter {
|
||||
return false;
|
||||
}
|
||||
|
||||
startProfiling(): void {
|
||||
this._bridge.send('startProfiling');
|
||||
|
||||
// Don't actually update the local profiling boolean yet!
|
||||
// Wait for onProfilingStatus() to confirm the status has changed.
|
||||
// This ensures the frontend and backend are in sync wrt which commits were profiled.
|
||||
// We do this to avoid mismatches on e.g. CommitTreeBuilder that would cause errors.
|
||||
}
|
||||
|
||||
stopProfiling(): void {
|
||||
this._bridge.send('stopProfiling');
|
||||
|
||||
// Don't actually update the local profiling boolean yet!
|
||||
// Wait for onProfilingStatus() to confirm the status has changed.
|
||||
// This ensures the frontend and backend are in sync wrt which commits were profiled.
|
||||
// We do this to avoid mismatches on e.g. CommitTreeBuilder that would cause errors.
|
||||
}
|
||||
|
||||
// TODO Maybe split this into two methods: expand() and collapse()
|
||||
toggleIsCollapsed(id: number, isCollapsed: boolean): void {
|
||||
let didMutate = false;
|
||||
@@ -702,32 +570,6 @@ export default class Store extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
_captureScreenshot = throttle(
|
||||
memoize((rootID: number, commitIndex: number) => {
|
||||
this._bridge.send('captureScreenshot', { commitIndex, rootID });
|
||||
}),
|
||||
THROTTLE_CAPTURE_SCREENSHOT_DURATION
|
||||
);
|
||||
|
||||
_takeProfilingSnapshotRecursive = (
|
||||
elementID: number,
|
||||
profilingSnapshots: Map<number, ProfilingSnapshotNode>
|
||||
) => {
|
||||
const element = this.getElementByID(elementID);
|
||||
if (element !== null) {
|
||||
profilingSnapshots.set(elementID, {
|
||||
id: elementID,
|
||||
children: element.children.slice(0),
|
||||
displayName: element.displayName,
|
||||
key: element.key,
|
||||
});
|
||||
|
||||
element.children.forEach(childID =>
|
||||
this._takeProfilingSnapshotRecursive(childID, profilingSnapshots)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
_adjustParentTreeWeight = (
|
||||
parentElement: Element | null,
|
||||
weightDelta: number
|
||||
@@ -768,23 +610,8 @@ export default class Store extends EventEmitter {
|
||||
|
||||
let haveRootsChanged = false;
|
||||
|
||||
// The first two values are always rendererID and rootID
|
||||
const rendererID = operations[0];
|
||||
const rootID = operations[1];
|
||||
|
||||
if (this._isProfiling) {
|
||||
let profilingOperations = this._profilingOperationsByRootID.get(rootID);
|
||||
if (profilingOperations == null) {
|
||||
profilingOperations = [operations];
|
||||
this._profilingOperationsByRootID.set(rootID, profilingOperations);
|
||||
} else {
|
||||
profilingOperations.push(operations);
|
||||
}
|
||||
|
||||
if (this._captureScreenshots) {
|
||||
const commitIndex = profilingOperations.length - 1;
|
||||
this._captureScreenshot(rootID, commitIndex);
|
||||
}
|
||||
}
|
||||
|
||||
const addedElementIDs: Array<number> = [];
|
||||
// This is a mapping of removed ID -> parent ID:
|
||||
@@ -856,10 +683,6 @@ export default class Store extends EventEmitter {
|
||||
weight: 0,
|
||||
});
|
||||
|
||||
if (this._isProfiling) {
|
||||
this._profilingSnapshotsByRootID.set(id, new Map());
|
||||
}
|
||||
|
||||
haveRootsChanged = true;
|
||||
} else {
|
||||
parentID = ((operations[i]: any): number);
|
||||
@@ -955,10 +778,6 @@ export default class Store extends EventEmitter {
|
||||
this._rootIDToRendererID.delete(id);
|
||||
this._rootIDToCapabilities.delete(id);
|
||||
|
||||
this._profilingOperationsByRootID.delete(id);
|
||||
this._profilingScreenshotsByRootID.delete(id);
|
||||
this._profilingSnapshotsByRootID.delete(id);
|
||||
|
||||
haveRootsChanged = true;
|
||||
} else {
|
||||
if (__DEBUG__) {
|
||||
@@ -1040,6 +859,8 @@ export default class Store extends EventEmitter {
|
||||
this._revision++;
|
||||
|
||||
if (haveRootsChanged) {
|
||||
const prevSupportsProfiling = this._supportsProfiling;
|
||||
|
||||
this._hasOwnerMetadata = false;
|
||||
this._supportsProfiling = false;
|
||||
this._rootIDToCapabilities.forEach(
|
||||
@@ -1054,6 +875,10 @@ export default class Store extends EventEmitter {
|
||||
);
|
||||
|
||||
this.emit('roots');
|
||||
|
||||
if (this._supportsProfiling !== prevSupportsProfiling) {
|
||||
this.emit('supportsProfiling');
|
||||
}
|
||||
}
|
||||
|
||||
if (__DEBUG__) {
|
||||
@@ -1064,60 +889,12 @@ export default class Store extends EventEmitter {
|
||||
this.emit('mutated', [addedElementIDs, removedElementIDs]);
|
||||
};
|
||||
|
||||
onProfilingStatus = (isProfiling: boolean) => {
|
||||
if (isProfiling) {
|
||||
this._importedProfilingData = null;
|
||||
this._profilingOperationsByRootID = new Map();
|
||||
this._profilingScreenshotsByRootID = new Map();
|
||||
this._profilingSnapshotsByRootID = new Map();
|
||||
this.roots.forEach(rootID => {
|
||||
const profilingSnapshots = new Map();
|
||||
this._profilingSnapshotsByRootID.set(rootID, profilingSnapshots);
|
||||
this._takeProfilingSnapshotRecursive(rootID, profilingSnapshots);
|
||||
});
|
||||
}
|
||||
|
||||
if (this._isProfiling !== isProfiling) {
|
||||
this._isProfiling = isProfiling;
|
||||
|
||||
// Invalidate suspense cache if profiling data is being (re-)recorded.
|
||||
// Note that we clear again, in case any views read from the cache while profiling.
|
||||
// (That would have resolved a now-stale value without any profiling data.)
|
||||
this._profilingCache.invalidate();
|
||||
|
||||
this.emit('isProfiling');
|
||||
}
|
||||
};
|
||||
|
||||
onScreenshotCaptured = ({
|
||||
commitIndex,
|
||||
dataURL,
|
||||
rootID,
|
||||
}: {|
|
||||
commitIndex: number,
|
||||
dataURL: string,
|
||||
rootID: number,
|
||||
|}) => {
|
||||
let profilingScreenshotsForRootByCommitIndex = this._profilingScreenshotsByRootID.get(
|
||||
rootID
|
||||
);
|
||||
if (!profilingScreenshotsForRootByCommitIndex) {
|
||||
profilingScreenshotsForRootByCommitIndex = new Map();
|
||||
this._profilingScreenshotsByRootID.set(
|
||||
rootID,
|
||||
profilingScreenshotsForRootByCommitIndex
|
||||
);
|
||||
}
|
||||
profilingScreenshotsForRootByCommitIndex.set(commitIndex, dataURL);
|
||||
};
|
||||
|
||||
onBridgeShutdown = () => {
|
||||
if (__DEBUG__) {
|
||||
debug('onBridgeShutdown', 'unsubscribing from Bridge');
|
||||
}
|
||||
|
||||
this._bridge.removeListener('operations', this.onBridgeOperations);
|
||||
this._bridge.removeListener('profilingStatus', this.onProfilingStatus);
|
||||
this._bridge.removeListener('shutdown', this.onBridgeShutdown);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ import type {
|
||||
|
||||
export default function ComponentFiltersModalWrapper(_: {||}) {
|
||||
const store = useContext(StoreContext);
|
||||
const { profilerStore } = store;
|
||||
|
||||
const { isModalShowing, setIsModalShowing } = useContext(
|
||||
ComponentFiltersModalContext
|
||||
@@ -50,13 +51,13 @@ export default function ComponentFiltersModalWrapper(_: {||}) {
|
||||
// If necessary, we could support this- but it doesn't seem like a necessary use case.
|
||||
const isProfilingSubscription = useMemo(
|
||||
() => ({
|
||||
getCurrentValue: () => store.isProfiling,
|
||||
getCurrentValue: () => profilerStore.isProfiling,
|
||||
subscribe: (callback: Function) => {
|
||||
store.addListener('isProfiling', callback);
|
||||
return () => store.removeListener('isProfiling', callback);
|
||||
profilerStore.addListener('isProfiling', callback);
|
||||
return () => profilerStore.removeListener('isProfiling', callback);
|
||||
},
|
||||
}),
|
||||
[store]
|
||||
[profilerStore]
|
||||
);
|
||||
const isProfiling = useSubscription<boolean, Store>(isProfilingSubscription);
|
||||
if (isProfiling && isModalShowing) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import React, { Suspense } from 'react';
|
||||
import Tree from './Tree';
|
||||
import SelectedElement from './SelectedElement';
|
||||
import { InspectedElementContextController } from './InspectedElementContext';
|
||||
import { OwnersListContextController } from './OwnersListContext';
|
||||
import portaledContent from '../portaledContent';
|
||||
import { ModalDialog } from '../ModalDialog';
|
||||
|
||||
@@ -12,19 +13,21 @@ import styles from './Components.css';
|
||||
function Components(_: {||}) {
|
||||
// TODO Flex wrappers below should be user resizable.
|
||||
return (
|
||||
<div className={styles.Components}>
|
||||
<div className={styles.TreeWrapper}>
|
||||
<Tree />
|
||||
</div>
|
||||
<div className={styles.SelectedElementWrapper}>
|
||||
<InspectedElementContextController>
|
||||
<Suspense fallback={<Loading />}>
|
||||
<SelectedElement />
|
||||
</Suspense>
|
||||
</InspectedElementContextController>
|
||||
</div>
|
||||
<ModalDialog />
|
||||
</div>
|
||||
<OwnersListContextController>
|
||||
<InspectedElementContextController>
|
||||
<div className={styles.Components}>
|
||||
<div className={styles.TreeWrapper}>
|
||||
<Tree />
|
||||
</div>
|
||||
<div className={styles.SelectedElementWrapper}>
|
||||
<Suspense fallback={<Loading />}>
|
||||
<SelectedElement />
|
||||
</Suspense>
|
||||
</div>
|
||||
<ModalDialog />
|
||||
</div>
|
||||
</InspectedElementContextController>
|
||||
</OwnersListContextController>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,9 @@
|
||||
|
||||
/* Invert colors */
|
||||
--color-component-name: var(--color-component-name-inverted);
|
||||
--color-component-badge-background: var(
|
||||
--color-component-badge-background-inverted
|
||||
);
|
||||
--color-jsx-arrow-brackets: var(--color-jsx-arrow-brackets-inverted);
|
||||
--color-attribute-name: var(--color-background-hover);
|
||||
--color-attribute-value: var(--color-component-name-inverted);
|
||||
@@ -73,3 +76,13 @@
|
||||
height: 1rem;
|
||||
color: var(--color-expand-collapse-toggle);
|
||||
}
|
||||
|
||||
.Badge {
|
||||
background-color: var(--color-component-badge-background);
|
||||
padding: 0.125rem 0.25rem;
|
||||
line-height: normal;
|
||||
border-radius: 0.125rem;
|
||||
margin-left: 0.25rem;
|
||||
font-family: var(--font-family-monospace);
|
||||
font-size: var(--font-size-monospace-small);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,12 @@ import React, {
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { ElementTypeClass, ElementTypeFunction } from 'src/types';
|
||||
import {
|
||||
ElementTypeClass,
|
||||
ElementTypeFunction,
|
||||
ElementTypeMemo,
|
||||
ElementTypeForwardRef,
|
||||
} from 'src/types';
|
||||
import Store from 'src/devtools/store';
|
||||
import ButtonIcon from '../ButtonIcon';
|
||||
import { createRegExp } from '../utils';
|
||||
@@ -29,7 +34,7 @@ type Props = {
|
||||
|
||||
export default function ElementView({ data, index, style }: Props) {
|
||||
const store = useContext(StoreContext);
|
||||
const { ownerFlatTree, ownerStack, selectedElementID } = useContext(
|
||||
const { ownerFlatTree, ownerID, selectedElementID } = useContext(
|
||||
TreeStateContext
|
||||
);
|
||||
const dispatch = useContext(TreeDispatcherContext);
|
||||
@@ -134,6 +139,7 @@ export default function ElementView({ data, index, style }: Props) {
|
||||
|
||||
const showDollarR =
|
||||
isSelected && (type === ElementTypeClass || type === ElementTypeFunction);
|
||||
const showBadge = type === ElementTypeMemo || type === ElementTypeForwardRef;
|
||||
|
||||
let className = styles.Element;
|
||||
if (isSelected) {
|
||||
@@ -168,7 +174,7 @@ export default function ElementView({ data, index, style }: Props) {
|
||||
}}
|
||||
>
|
||||
<span className={styles.ScrollAnchor} ref={scrollAnchorStartRef} />
|
||||
{ownerStack.length === 0 ? (
|
||||
{ownerID === null ? (
|
||||
<ExpandCollapseToggle element={element} store={store} />
|
||||
) : null}
|
||||
<span className={styles.Component}>
|
||||
@@ -182,6 +188,11 @@ export default function ElementView({ data, index, style }: Props) {
|
||||
</span>
|
||||
{showDollarR && <span className={styles.DollarR}> == $r</span>}
|
||||
<span className={styles.ScrollAnchor} ref={scrollAnchorEndRef} />
|
||||
{showBadge && (
|
||||
<span className={styles.Badge}>
|
||||
{type === ElementTypeMemo ? 'Memo' : 'ForwardRef'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
.EventsTree {
|
||||
padding: 0.25rem;
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
.EventsTree:first-of-type {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.HeaderRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.Header {
|
||||
flex: 1 1;
|
||||
font-family: var(--font-family-sans);
|
||||
}
|
||||
|
||||
.NameValueRow {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.Empty {
|
||||
color: var(--color-dimmer);
|
||||
font-style: italic;
|
||||
padding-left: 0.75rem;
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { copy } from 'clipboard-js';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import styles from './EventsTree.css';
|
||||
import Button from '../Button';
|
||||
import ButtonIcon from '../ButtonIcon';
|
||||
import KeyValue from './KeyValue';
|
||||
import ExpandCollapseToggle from './ExpandCollapseToggle';
|
||||
import { serializeDataForCopy } from '../utils';
|
||||
|
||||
type Props = {|
|
||||
events: Object,
|
||||
|};
|
||||
|
||||
function EventsTreeView({ events }: Props) {
|
||||
const handleCopy = useCallback(() => copy(serializeDataForCopy(events)), [
|
||||
events,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className={styles.EventsTree}>
|
||||
<div className={styles.HeaderRow}>
|
||||
<div className={styles.Header}>events</div>
|
||||
{
|
||||
<Button onClick={handleCopy} title="Copy to clipboard">
|
||||
<ButtonIcon type="copy" />
|
||||
</Button>
|
||||
}
|
||||
</div>
|
||||
<InnerEventsTreeView events={events} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function InnerEventsTreeView({ events }: Props) {
|
||||
return events.map((event, index) => (
|
||||
<EventComponentView
|
||||
key={index}
|
||||
displayName={event.displayName}
|
||||
props={event.props}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
type EventComponentViewProps = {|
|
||||
displayName: string,
|
||||
props: null | Object,
|
||||
|};
|
||||
|
||||
function EventComponentView({ displayName, props }: EventComponentViewProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
let eventComponentProps = null;
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
let children;
|
||||
|
||||
if (props !== null) {
|
||||
// We don't want children, so extract it out
|
||||
({ children, ...eventComponentProps } = props);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.NameValueRow}>
|
||||
<ExpandCollapseToggle isOpen={isOpen} setIsOpen={setIsOpen} />
|
||||
<span onClick={() => {}} className={styles.Name}>
|
||||
{displayName}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.Children} hidden={!isOpen}>
|
||||
{eventComponentProps === null && (
|
||||
<div className={styles.Empty}>None</div>
|
||||
)}
|
||||
{eventComponentProps !== null &&
|
||||
Object.keys((eventComponentProps: any)).map(name => (
|
||||
<KeyValue
|
||||
key={name}
|
||||
depth={1}
|
||||
name={name}
|
||||
path={[name]}
|
||||
value={(eventComponentProps: any)[name]}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// $FlowFixMe
|
||||
export default React.memo(EventsTreeView);
|
||||
@@ -83,13 +83,22 @@ function InspectedElementContextController({ children }: Props) {
|
||||
|
||||
// This effect handler invalidates the suspense cache and schedules rendering updates with React.
|
||||
useEffect(() => {
|
||||
const onInspectedElement = (inspectedElement: InspectedElement | null) => {
|
||||
if (inspectedElement !== null) {
|
||||
const onInspectedElement = (
|
||||
inspectedElement: InspectedElement | number | null
|
||||
) => {
|
||||
// A null value means that the element no longer exists in the backend.
|
||||
// If it's the same element that's currently selected, that selection will be removed once the Store updates.
|
||||
// If it's not- then we can just ignore it anyway.
|
||||
// Either way there is nothing we need to do in this case.
|
||||
// A numeric value indicates that the element hasn't changed since we last requested its data,
|
||||
// in which case we don't need to invalidate the cache and re-render anything in the DevTools.
|
||||
if (inspectedElement !== null && typeof inspectedElement === 'object') {
|
||||
const id = inspectedElement.id;
|
||||
|
||||
inspectedElement = (({
|
||||
...inspectedElement,
|
||||
context: hydrateHelper(inspectedElement.context),
|
||||
events: hydrateHelper(inspectedElement.events),
|
||||
hooks: hydrateHelper(inspectedElement.hooks),
|
||||
props: hydrateHelper(inspectedElement.props),
|
||||
state: hydrateHelper(inspectedElement.state),
|
||||
@@ -140,13 +149,19 @@ function InspectedElementContextController({ children }: Props) {
|
||||
// Update the $r variable.
|
||||
bridge.send('selectElement', { id: selectedElementID, rendererID });
|
||||
|
||||
const onInspectedElement = (inspectedElement: InspectedElement | null) => {
|
||||
if (
|
||||
inspectedElement !== null &&
|
||||
inspectedElement.id === selectedElementID
|
||||
) {
|
||||
const onInspectedElement = (
|
||||
inspectedElement: InspectedElement | number | null
|
||||
) => {
|
||||
if (inspectedElement !== null) {
|
||||
// If this is the element we requested, wait a little bit and then ask for an update.
|
||||
timeoutID = setTimeout(sendRequest, 1000);
|
||||
if (inspectedElement === selectedElementID) {
|
||||
timeoutID = setTimeout(sendRequest, 1000);
|
||||
} else if (
|
||||
typeof inspectedElement === 'object' &&
|
||||
inspectedElement.id === selectedElementID
|
||||
) {
|
||||
timeoutID = setTimeout(sendRequest, 1000);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
// @flow
|
||||
|
||||
import React, {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
} from 'react';
|
||||
import { createResource } from '../../cache';
|
||||
import { BridgeContext, StoreContext } from '../context';
|
||||
import { TreeStateContext } from './TreeContext';
|
||||
|
||||
import type {
|
||||
Element,
|
||||
Owner,
|
||||
OwnersList,
|
||||
} from 'src/devtools/views/Components/types';
|
||||
import type { Resource, Thenable } from '../../cache';
|
||||
|
||||
type Context = (id: number) => Array<Owner> | null;
|
||||
|
||||
const OwnersListContext = createContext<Context>(((null: any): Context));
|
||||
OwnersListContext.displayName = 'OwnersListContext';
|
||||
|
||||
type ResolveFn = (ownersList: Array<Owner> | null) => void;
|
||||
type InProgressRequest = {|
|
||||
promise: Thenable<Array<Owner>>,
|
||||
resolveFn: ResolveFn,
|
||||
|};
|
||||
|
||||
const inProgressRequests: WeakMap<Element, InProgressRequest> = new WeakMap();
|
||||
const resource: Resource<Element, Element, Array<Owner>> = createResource(
|
||||
(element: Element) => {
|
||||
let request = inProgressRequests.get(element);
|
||||
if (request != null) {
|
||||
return request.promise;
|
||||
}
|
||||
|
||||
let resolveFn = ((null: any): ResolveFn);
|
||||
const promise = new Promise(resolve => {
|
||||
resolveFn = resolve;
|
||||
});
|
||||
|
||||
inProgressRequests.set(element, { promise, resolveFn });
|
||||
|
||||
return promise;
|
||||
},
|
||||
(element: Element) => element,
|
||||
{ useWeakMap: true }
|
||||
);
|
||||
|
||||
type Props = {|
|
||||
children: React$Node,
|
||||
|};
|
||||
|
||||
function OwnersListContextController({ children }: Props) {
|
||||
const bridge = useContext(BridgeContext);
|
||||
const store = useContext(StoreContext);
|
||||
const { ownerID } = useContext(TreeStateContext);
|
||||
|
||||
const read = useCallback(
|
||||
(id: number) => {
|
||||
const element = store.getElementByID(id);
|
||||
if (element !== null) {
|
||||
return resource.read(element);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
[store]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const onOwnersList = (ownersList: OwnersList) => {
|
||||
const id = ownersList.id;
|
||||
|
||||
const element = store.getElementByID(id);
|
||||
if (element !== null) {
|
||||
const request = inProgressRequests.get(element);
|
||||
if (request != null) {
|
||||
inProgressRequests.delete(element);
|
||||
request.resolveFn(ownersList.owners);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
bridge.addListener('ownersList', onOwnersList);
|
||||
return () => bridge.removeListener('ownersList', onOwnersList);
|
||||
}, [bridge, store]);
|
||||
|
||||
// This effect requests an updated owners list any time the selected owner changes
|
||||
useEffect(() => {
|
||||
if (ownerID !== null) {
|
||||
const rendererID = store.getRendererIDForElement(ownerID);
|
||||
|
||||
bridge.send('getOwnersList', { id: ownerID, rendererID });
|
||||
}
|
||||
|
||||
return () => {};
|
||||
}, [bridge, ownerID, store]);
|
||||
|
||||
return (
|
||||
<OwnersListContext.Provider value={read}>
|
||||
{children}
|
||||
</OwnersListContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export { OwnersListContext, OwnersListContextController };
|
||||
@@ -91,3 +91,8 @@
|
||||
font-family: var(--font-family-monospace);
|
||||
font-size: var(--font-size-monospace-normal);
|
||||
}
|
||||
|
||||
.NotInStore,
|
||||
.NotInStore:hover {
|
||||
color: var(--color-dimmest);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import React, {
|
||||
useCallback,
|
||||
useContext,
|
||||
useLayoutEffect,
|
||||
useReducer,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
@@ -12,22 +13,113 @@ import { Menu, MenuList, MenuButton, MenuItem } from '@reach/menu-button';
|
||||
import Button from '../Button';
|
||||
import ButtonIcon from '../ButtonIcon';
|
||||
import Toggle from '../Toggle';
|
||||
import { OwnersListContext } from './OwnersListContext';
|
||||
import { TreeDispatcherContext, TreeStateContext } from './TreeContext';
|
||||
import { StoreContext } from '../context';
|
||||
import { useIsOverflowing } from '../hooks';
|
||||
import { StoreContext } from '../context';
|
||||
|
||||
import type { Element } from './types';
|
||||
import type { Owner } from './types';
|
||||
|
||||
import styles from './OwnersStack.css';
|
||||
|
||||
type SelectOwner = (owner: Owner | null) => void;
|
||||
|
||||
type ACTION_UPDATE_OWNER_ID = {|
|
||||
type: 'UPDATE_OWNER_ID',
|
||||
ownerID: number | null,
|
||||
owners: Array<Owner>,
|
||||
|};
|
||||
type ACTION_UPDATE_SELECTED_INDEX = {|
|
||||
type: 'UPDATE_SELECTED_INDEX',
|
||||
selectedIndex: number,
|
||||
|};
|
||||
|
||||
type Action = ACTION_UPDATE_OWNER_ID | ACTION_UPDATE_SELECTED_INDEX;
|
||||
|
||||
type State = {|
|
||||
ownerID: number | null,
|
||||
owners: Array<Owner>,
|
||||
selectedIndex: number,
|
||||
|};
|
||||
|
||||
function dialogReducer(state, action) {
|
||||
switch (action.type) {
|
||||
case 'UPDATE_OWNER_ID':
|
||||
const selectedIndex = action.owners.findIndex(
|
||||
owner => owner.id === action.ownerID
|
||||
);
|
||||
return {
|
||||
ownerID: action.ownerID,
|
||||
owners: action.owners,
|
||||
selectedIndex,
|
||||
};
|
||||
case 'UPDATE_SELECTED_INDEX':
|
||||
return {
|
||||
...state,
|
||||
selectedIndex: action.selectedIndex,
|
||||
};
|
||||
default:
|
||||
throw new Error(`Invalid action "${action.type}"`);
|
||||
}
|
||||
}
|
||||
|
||||
export default function OwnerStack() {
|
||||
const { ownerStack, ownerStackIndex } = useContext(TreeStateContext);
|
||||
const dispatch = useContext(TreeDispatcherContext);
|
||||
const read = useContext(OwnersListContext);
|
||||
const { ownerID } = useContext(TreeStateContext);
|
||||
const treeDispatch = useContext(TreeDispatcherContext);
|
||||
|
||||
const [state, dispatch] = useReducer<State, Action>(dialogReducer, {
|
||||
ownerID: null,
|
||||
owners: [],
|
||||
selectedIndex: 0,
|
||||
});
|
||||
|
||||
// When an owner is selected, we either need to update the selected index, or we need to fetch a new list of owners.
|
||||
// We use a reducer here so that we can avoid fetching a new list unless the owner ID has actually changed.
|
||||
if (ownerID === null) {
|
||||
dispatch({
|
||||
type: 'UPDATE_OWNER_ID',
|
||||
ownerID: null,
|
||||
owners: [],
|
||||
});
|
||||
} else if (ownerID !== state.ownerID) {
|
||||
const isInStore =
|
||||
state.owners.findIndex(owner => owner.id === ownerID) >= 0;
|
||||
dispatch({
|
||||
type: 'UPDATE_OWNER_ID',
|
||||
ownerID,
|
||||
owners: isInStore ? state.owners : read(ownerID) || [],
|
||||
});
|
||||
}
|
||||
|
||||
const { owners, selectedIndex } = state;
|
||||
|
||||
const selectOwner = useCallback<SelectOwner>(
|
||||
(owner: Owner | null) => {
|
||||
if (owner !== null) {
|
||||
const index = owners.indexOf(owner);
|
||||
dispatch({
|
||||
type: 'UPDATE_SELECTED_INDEX',
|
||||
selectedIndex: index >= 0 ? index : 0,
|
||||
});
|
||||
treeDispatch({ type: 'SELECT_OWNER', payload: owner.id });
|
||||
} else {
|
||||
dispatch({
|
||||
type: 'UPDATE_SELECTED_INDEX',
|
||||
selectedIndex: 0,
|
||||
});
|
||||
treeDispatch({ type: 'RESET_OWNER_STACK' });
|
||||
}
|
||||
},
|
||||
[owners, treeDispatch]
|
||||
);
|
||||
|
||||
const [elementsTotalWidth, setElementsTotalWidth] = useState(0);
|
||||
const elementsBarRef = useRef<HTMLDivElement | null>(null);
|
||||
const isOverflowing = useIsOverflowing(elementsBarRef, elementsTotalWidth);
|
||||
|
||||
const selectedOwner = owners[selectedIndex];
|
||||
|
||||
useLayoutEffect(() => {
|
||||
// If we're already overflowing, then we don't need to re-measure items.
|
||||
// That's because once the owners stack is open, it can only get larger (by driling in).
|
||||
@@ -37,7 +129,7 @@ export default function OwnerStack() {
|
||||
}
|
||||
|
||||
let elementsTotalWidth = 0;
|
||||
for (let i = 0; i < ownerStack.length; i++) {
|
||||
for (let i = 0; i < owners.length; i++) {
|
||||
const element = elementsBarRef.current.children[i];
|
||||
const computedStyle = getComputedStyle(element);
|
||||
|
||||
@@ -48,7 +140,7 @@ export default function OwnerStack() {
|
||||
}
|
||||
|
||||
setElementsTotalWidth(elementsTotalWidth);
|
||||
}, [elementsBarRef, isOverflowing, ownerStack.length]);
|
||||
}, [elementsBarRef, isOverflowing, owners.length]);
|
||||
|
||||
return (
|
||||
<div className={styles.OwnerStack}>
|
||||
@@ -56,28 +148,38 @@ export default function OwnerStack() {
|
||||
{isOverflowing && (
|
||||
<Fragment>
|
||||
<ElementsDropdown
|
||||
ownerStack={ownerStack}
|
||||
ownerStackIndex={ownerStackIndex}
|
||||
owners={owners}
|
||||
selectedIndex={selectedIndex}
|
||||
selectOwner={selectOwner}
|
||||
/>
|
||||
<BackToOwnerButton
|
||||
ownerStack={ownerStack}
|
||||
ownerStackIndex={ownerStackIndex}
|
||||
/>
|
||||
<ElementView
|
||||
id={ownerStack[((ownerStackIndex: any): number)]}
|
||||
index={ownerStackIndex}
|
||||
owners={owners}
|
||||
selectedIndex={selectedIndex}
|
||||
selectOwner={selectOwner}
|
||||
/>
|
||||
{selectedOwner != null && (
|
||||
<ElementView
|
||||
owner={selectedOwner}
|
||||
isSelected
|
||||
selectOwner={selectOwner}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
)}
|
||||
{!isOverflowing &&
|
||||
ownerStack.map((id, index) => (
|
||||
<ElementView key={id} id={id} index={index} />
|
||||
owners.map((owner, index) => (
|
||||
<ElementView
|
||||
key={index}
|
||||
owner={owner}
|
||||
isSelected={index === selectedIndex}
|
||||
selectOwner={selectOwner}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className={styles.VRule} />
|
||||
<Button
|
||||
className={styles.IconButton}
|
||||
onClick={() => dispatch({ type: 'RESET_OWNER_STACK' })}
|
||||
onClick={() => selectOwner(null)}
|
||||
title="Back to tree view"
|
||||
>
|
||||
<ButtonIcon type="close" />
|
||||
@@ -87,26 +189,28 @@ export default function OwnerStack() {
|
||||
}
|
||||
|
||||
type ElementsDropdownProps = {
|
||||
ownerStack: Array<number>,
|
||||
ownerStackIndex: number | null,
|
||||
owners: Array<Owner>,
|
||||
selectedIndex: number,
|
||||
selectOwner: SelectOwner,
|
||||
};
|
||||
function ElementsDropdown({
|
||||
ownerStack,
|
||||
ownerStackIndex,
|
||||
owners,
|
||||
selectedIndex,
|
||||
selectOwner,
|
||||
}: ElementsDropdownProps) {
|
||||
const store = useContext(StoreContext);
|
||||
const dispatch = useContext(TreeDispatcherContext);
|
||||
|
||||
const menuItems = [];
|
||||
for (let index = ownerStack.length - 1; index >= 0; index--) {
|
||||
const id = ownerStack[index];
|
||||
for (let index = owners.length - 1; index >= 0; index--) {
|
||||
const owner = owners[index];
|
||||
const isInStore = store.containsElement(owner.id);
|
||||
menuItems.push(
|
||||
<MenuItem
|
||||
key={id}
|
||||
className={styles.Component}
|
||||
onSelect={() => dispatch({ type: 'SELECT_OWNER', payload: id })}
|
||||
key={owner.id}
|
||||
className={`${styles.Component} ${isInStore ? '' : styles.NotInStore}`}
|
||||
onSelect={() => (isInStore ? selectOwner(owner) : null)}
|
||||
>
|
||||
{((store.getElementByID(id): any): Element).displayName}
|
||||
{owner.displayName}
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
@@ -126,28 +230,26 @@ function ElementsDropdown({
|
||||
}
|
||||
|
||||
type ElementViewProps = {
|
||||
id: number,
|
||||
index: number | null,
|
||||
isSelected: boolean,
|
||||
owner: Owner,
|
||||
selectOwner: SelectOwner,
|
||||
};
|
||||
function ElementView({ id, index }: ElementViewProps) {
|
||||
function ElementView({ isSelected, owner, selectOwner }: ElementViewProps) {
|
||||
const store = useContext(StoreContext);
|
||||
const { ownerStackIndex } = useContext(TreeStateContext);
|
||||
const dispatch = useContext(TreeDispatcherContext);
|
||||
|
||||
const { displayName } = ((store.getElementByID(id): any): Element);
|
||||
|
||||
const isChecked = ownerStackIndex === index;
|
||||
const { displayName } = owner;
|
||||
const isInStore = store.containsElement(owner.id);
|
||||
|
||||
const handleChange = useCallback(() => {
|
||||
if (!isChecked) {
|
||||
dispatch({ type: 'SELECT_OWNER', payload: id });
|
||||
if (isInStore) {
|
||||
selectOwner(owner);
|
||||
}
|
||||
}, [dispatch, id, isChecked]);
|
||||
}, [isInStore, selectOwner, owner]);
|
||||
|
||||
return (
|
||||
<Toggle
|
||||
className={styles.Component}
|
||||
isChecked={isChecked}
|
||||
className={`${styles.Component} ${isInStore ? '' : styles.NotInStore}`}
|
||||
isChecked={isSelected}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{displayName}
|
||||
@@ -156,32 +258,32 @@ function ElementView({ id, index }: ElementViewProps) {
|
||||
}
|
||||
|
||||
type BackToOwnerButtonProps = {|
|
||||
ownerStack: Array<number>,
|
||||
ownerStackIndex: number | null,
|
||||
owners: Array<Owner>,
|
||||
selectedIndex: number,
|
||||
selectOwner: SelectOwner,
|
||||
|};
|
||||
function BackToOwnerButton({
|
||||
ownerStack,
|
||||
ownerStackIndex,
|
||||
owners,
|
||||
selectedIndex,
|
||||
selectOwner,
|
||||
}: BackToOwnerButtonProps) {
|
||||
const store = useContext(StoreContext);
|
||||
const dispatch = useContext(TreeDispatcherContext);
|
||||
|
||||
if (ownerStackIndex === null || ownerStackIndex === 0) {
|
||||
if (selectedIndex <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ownerID = ownerStack[ownerStackIndex - 1];
|
||||
const owner = store.getElementByID(ownerID);
|
||||
const owner = owners[selectedIndex - 1];
|
||||
if (owner == null) {
|
||||
debugger;
|
||||
}
|
||||
const isInStore = store.containsElement(owner.id);
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={() =>
|
||||
dispatch({
|
||||
type: 'SELECT_OWNER',
|
||||
payload: ownerID,
|
||||
})
|
||||
}
|
||||
title={`Up to ${(owner !== null && owner.displayName) || 'owner'}`}
|
||||
className={isInStore ? undefined : styles.NotInStore}
|
||||
onClick={() => (isInStore ? selectOwner(owner) : null)}
|
||||
title={`Up to ${owner.displayName || 'owner'}`}
|
||||
>
|
||||
<ButtonIcon type="previous" />
|
||||
</Button>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { BridgeContext, StoreContext } from '../context';
|
||||
import Button from '../Button';
|
||||
import ButtonIcon from '../ButtonIcon';
|
||||
import HooksTree from './HooksTree';
|
||||
import EventsTree from './EventsTree';
|
||||
import { ModalDialogContext } from '../ModalDialog';
|
||||
import InspectedElementTree from './InspectedElementTree';
|
||||
import { InspectedElementContext } from './InspectedElementContext';
|
||||
@@ -221,13 +222,14 @@ function InspectedElementView({
|
||||
canEditHooks,
|
||||
canToggleSuspense,
|
||||
context,
|
||||
events,
|
||||
hooks,
|
||||
owners,
|
||||
props,
|
||||
state,
|
||||
} = inspectedElement;
|
||||
|
||||
const { ownerStack } = useContext(TreeStateContext);
|
||||
const { ownerID } = useContext(TreeStateContext);
|
||||
const bridge = useContext(BridgeContext);
|
||||
const store = useContext(StoreContext);
|
||||
|
||||
@@ -297,14 +299,15 @@ function InspectedElementView({
|
||||
data={context}
|
||||
overrideValueFn={overrideContextFn}
|
||||
/>
|
||||
{events !== null && events.length > 0 && <EventsTree events={events} />}
|
||||
|
||||
{ownerStack.length === 0 && owners !== null && owners.length > 0 && (
|
||||
{ownerID === null && owners !== null && owners.length > 0 && (
|
||||
<div className={styles.Owners}>
|
||||
<div className={styles.OwnersHeader}>rendered by</div>
|
||||
{owners.map(owner => (
|
||||
<OwnerView
|
||||
key={owner.id}
|
||||
displayName={owner.displayName}
|
||||
displayName={owner.displayName || 'Anonymous'}
|
||||
id={owner.id}
|
||||
isInStore={store.containsElement(owner.id)}
|
||||
/>
|
||||
|
||||
@@ -14,6 +14,7 @@ import type { ComponentFilter } from 'src/types';
|
||||
|
||||
export default function ToggleComponentFiltersModalButton() {
|
||||
const store = useContext(StoreContext);
|
||||
const { profilerStore } = store;
|
||||
|
||||
const { isModalShowing, setIsModalShowing } = useContext(
|
||||
ComponentFiltersModalContext
|
||||
@@ -23,13 +24,13 @@ export default function ToggleComponentFiltersModalButton() {
|
||||
// If necessary, we could support this- but it doesn't seem like a necessary use case.
|
||||
const isProfilingSubscription = useMemo(
|
||||
() => ({
|
||||
getCurrentValue: () => store.isProfiling,
|
||||
getCurrentValue: () => profilerStore.isProfiling,
|
||||
subscribe: (callback: Function) => {
|
||||
store.addListener('isProfiling', callback);
|
||||
return () => store.removeListener('isProfiling', callback);
|
||||
profilerStore.addListener('isProfiling', callback);
|
||||
return () => profilerStore.removeListener('isProfiling', callback);
|
||||
},
|
||||
}),
|
||||
[store]
|
||||
[profilerStore]
|
||||
);
|
||||
const isProfiling = useSubscription<boolean, Store>(isProfilingSubscription);
|
||||
|
||||
|
||||
@@ -37,3 +37,14 @@
|
||||
margin: 0 0.5rem;
|
||||
background-color: var(--color-border);
|
||||
}
|
||||
|
||||
.Loading {
|
||||
height: 100%;
|
||||
padding-left: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
justify-content: flex-start;
|
||||
font-size: var(--font-size-sans-large);
|
||||
color: var(--color-dim);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// @flow
|
||||
|
||||
import React, {
|
||||
Suspense,
|
||||
useState,
|
||||
useCallback,
|
||||
useContext,
|
||||
@@ -38,7 +39,7 @@ export default function Tree(props: Props) {
|
||||
const dispatch = useContext(TreeDispatcherContext);
|
||||
const {
|
||||
numElements,
|
||||
ownerStack,
|
||||
ownerID,
|
||||
searchIndex,
|
||||
searchResults,
|
||||
selectedElementID,
|
||||
@@ -277,7 +278,9 @@ export default function Tree(props: Props) {
|
||||
<div className={styles.SearchInput}>
|
||||
<InspectHostNodesToggle />
|
||||
<div className={styles.VRule} />
|
||||
{ownerStack.length > 0 ? <OwnersStack /> : <SearchInput />}
|
||||
<Suspense fallback={<Loading />}>
|
||||
{ownerID !== null ? <OwnersStack /> : <SearchInput />}
|
||||
</Suspense>
|
||||
<div className={styles.VRule} />
|
||||
<ToggleComponentFiltersModalButton />
|
||||
</div>
|
||||
@@ -317,7 +320,7 @@ export default function Tree(props: Props) {
|
||||
}
|
||||
|
||||
function InnerElementType({ style, ...rest }) {
|
||||
const { ownerStack } = useContext(TreeStateContext);
|
||||
const { ownerID } = useContext(TreeStateContext);
|
||||
|
||||
// The list may need to scroll horizontally due to deeply nested elements.
|
||||
// We don't know the maximum scroll width up front, because we're windowing.
|
||||
@@ -346,10 +349,9 @@ function InnerElementType({ style, ...rest }) {
|
||||
|
||||
// We shouldn't retain this width across different conceptual trees though,
|
||||
// so when the user opens the "owners tree" view, we should discard the previous width.
|
||||
const hasOwnerStack = ownerStack.length > 0;
|
||||
const [prevHasOwnerStack, setPrevHasOwnerStack] = useState(hasOwnerStack);
|
||||
if (hasOwnerStack !== prevHasOwnerStack) {
|
||||
setPrevHasOwnerStack(hasOwnerStack);
|
||||
const [prevOwnerID, setPrevOwnerID] = useState(ownerID);
|
||||
if (ownerID !== prevOwnerID) {
|
||||
setPrevOwnerID(ownerID);
|
||||
setMinWidth(null);
|
||||
}
|
||||
|
||||
@@ -371,3 +373,7 @@ function InnerElementType({ style, ...rest }) {
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function Loading() {
|
||||
return <div className={styles.Loading}>Loading...</div>;
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ import Store from '../../store';
|
||||
|
||||
import type { Element } from './types';
|
||||
|
||||
type StateContext = {|
|
||||
export type StateContext = {|
|
||||
// Tree
|
||||
numElements: number,
|
||||
selectedElementID: number | null,
|
||||
@@ -50,9 +50,8 @@ type StateContext = {|
|
||||
searchText: string,
|
||||
|
||||
// Owners
|
||||
ownerID: number | null,
|
||||
ownerFlatTree: Array<Element> | null,
|
||||
ownerStack: Array<number>,
|
||||
ownerStackIndex: number | null,
|
||||
|
||||
// Inspection element panel
|
||||
inspectedElementID: number | null,
|
||||
@@ -118,7 +117,7 @@ type Action =
|
||||
| ACTION_SET_SEARCH_TEXT
|
||||
| ACTION_UPDATE_INSPECTED_ELEMENT_ID;
|
||||
|
||||
type DispatcherContext = (action: Action) => void;
|
||||
export type DispatcherContext = (action: Action) => void;
|
||||
|
||||
const TreeStateContext = createContext<StateContext>(
|
||||
((null: any): StateContext)
|
||||
@@ -142,8 +141,7 @@ type State = {|
|
||||
searchText: string,
|
||||
|
||||
// Owners
|
||||
ownerStack: Array<number>,
|
||||
ownerStackIndex: number | null,
|
||||
ownerID: number | null,
|
||||
ownerFlatTree: Array<Element> | null,
|
||||
|
||||
// Inspection element panel
|
||||
@@ -151,17 +149,12 @@ type State = {|
|
||||
|};
|
||||
|
||||
function reduceTreeState(store: Store, state: State, action: Action): State {
|
||||
let {
|
||||
numElements,
|
||||
ownerStack,
|
||||
selectedElementIndex,
|
||||
selectedElementID,
|
||||
} = state;
|
||||
let { numElements, ownerID, selectedElementIndex, selectedElementID } = state;
|
||||
|
||||
let lookupIDForIndex = true;
|
||||
|
||||
// Base tree should ignore selected element changes when the owner's tree is active.
|
||||
if (ownerStack.length === 0) {
|
||||
if (ownerID === null) {
|
||||
switch (action.type) {
|
||||
case 'HANDLE_STORE_MUTATION':
|
||||
numElements = store.numElements;
|
||||
@@ -276,7 +269,7 @@ function reduceTreeState(store: Store, state: State, action: Action): State {
|
||||
|
||||
function reduceSearchState(store: Store, state: State, action: Action): State {
|
||||
let {
|
||||
ownerStack,
|
||||
ownerID,
|
||||
searchIndex,
|
||||
searchResults,
|
||||
searchText,
|
||||
@@ -295,7 +288,7 @@ function reduceSearchState(store: Store, state: State, action: Action): State {
|
||||
let didRequestSearch = false;
|
||||
|
||||
// Search isn't supported when the owner's tree is active.
|
||||
if (ownerStack.length === 0) {
|
||||
if (ownerID === null) {
|
||||
switch (action.type) {
|
||||
case 'GO_TO_NEXT_SEARCH_RESULT':
|
||||
if (numPrevSearchResults > 0) {
|
||||
@@ -442,9 +435,8 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
|
||||
numElements,
|
||||
selectedElementID,
|
||||
selectedElementIndex,
|
||||
ownerID,
|
||||
ownerFlatTree,
|
||||
ownerStack,
|
||||
ownerStackIndex,
|
||||
searchIndex,
|
||||
searchResults,
|
||||
searchText,
|
||||
@@ -454,30 +446,20 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
|
||||
|
||||
switch (action.type) {
|
||||
case 'HANDLE_STORE_MUTATION':
|
||||
if (ownerStack.length > 0) {
|
||||
let indexOfRemovedItem = -1;
|
||||
for (let i = 0; i < ownerStack.length; i++) {
|
||||
if (store.getElementByID(ownerStack[i]) === null) {
|
||||
indexOfRemovedItem = i;
|
||||
break;
|
||||
if (ownerID !== null) {
|
||||
if (!store.containsElement(ownerID)) {
|
||||
ownerID = null;
|
||||
ownerFlatTree = null;
|
||||
selectedElementID = null;
|
||||
} else {
|
||||
ownerFlatTree = store.getOwnersListForElement(ownerID);
|
||||
if (selectedElementID !== null) {
|
||||
// Mutation might have caused the index of this ID to shift.
|
||||
selectedElementIndex = ownerFlatTree.findIndex(
|
||||
element => element.id === selectedElementID
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (indexOfRemovedItem >= 0) {
|
||||
ownerStack = ownerStack.slice(0, indexOfRemovedItem);
|
||||
if (ownerStack.length === 0) {
|
||||
ownerFlatTree = null;
|
||||
ownerStackIndex = null;
|
||||
} else {
|
||||
ownerStackIndex = ownerStack.length - 1;
|
||||
}
|
||||
}
|
||||
if (selectedElementID !== null && ownerFlatTree !== null) {
|
||||
// Mutation might have caused the index of this ID to shift.
|
||||
selectedElementIndex = ownerFlatTree.findIndex(
|
||||
element => element.id === selectedElementID
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (selectedElementID !== null) {
|
||||
// Mutation might have caused the index of this ID to shift.
|
||||
@@ -491,13 +473,12 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
|
||||
}
|
||||
break;
|
||||
case 'RESET_OWNER_STACK':
|
||||
ownerStack = [];
|
||||
ownerStackIndex = null;
|
||||
ownerID = null;
|
||||
ownerFlatTree = null;
|
||||
selectedElementIndex =
|
||||
selectedElementID !== null
|
||||
? store.getIndexOfElementID(selectedElementID)
|
||||
: null;
|
||||
ownerFlatTree = null;
|
||||
break;
|
||||
case 'SELECT_ELEMENT_AT_INDEX':
|
||||
if (ownerFlatTree !== null) {
|
||||
@@ -533,33 +514,12 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
|
||||
// If the Store doesn't have any owners metadata, don't drill into an empty stack.
|
||||
// This is a confusing user experience.
|
||||
if (store.hasOwnerMetadata) {
|
||||
const id = (action: ACTION_SELECT_OWNER).payload;
|
||||
ownerStackIndex = ownerStack.indexOf(id);
|
||||
ownerID = (action: ACTION_SELECT_OWNER).payload;
|
||||
ownerFlatTree = store.getOwnersListForElement(ownerID);
|
||||
|
||||
// Always force reset selection to be the top of the new owner tree.
|
||||
selectedElementIndex = 0;
|
||||
prevSelectedElementIndex = null;
|
||||
|
||||
// If this owner is already in the current stack, just select it.
|
||||
// Otherwise, create a new stack.
|
||||
if (ownerStackIndex < 0) {
|
||||
// Add this new owner, and fill in the owners above it as well.
|
||||
ownerStack = [];
|
||||
let currentOwnerID = id;
|
||||
while (currentOwnerID !== 0) {
|
||||
ownerStack.unshift(currentOwnerID);
|
||||
currentOwnerID = ((store.getElementByID(
|
||||
currentOwnerID
|
||||
): any): Element).ownerID;
|
||||
}
|
||||
ownerStackIndex = ownerStack.length - 1;
|
||||
|
||||
if (searchText !== '') {
|
||||
searchIndex = null;
|
||||
searchResults = [];
|
||||
searchText = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
@@ -569,17 +529,12 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
|
||||
|
||||
// Changes in the selected owner require re-calculating the owners tree.
|
||||
if (
|
||||
ownerStackIndex !== state.ownerStackIndex ||
|
||||
ownerStack !== state.ownerStack ||
|
||||
ownerFlatTree !== state.ownerFlatTree ||
|
||||
action.type === 'HANDLE_STORE_MUTATION'
|
||||
) {
|
||||
if (ownerStackIndex === null) {
|
||||
ownerFlatTree = null;
|
||||
if (ownerFlatTree === null) {
|
||||
numElements = store.numElements;
|
||||
} else {
|
||||
ownerFlatTree = store.getOwnersListForElement(
|
||||
ownerStack[ownerStackIndex]
|
||||
);
|
||||
numElements = ownerFlatTree.length;
|
||||
}
|
||||
}
|
||||
@@ -588,9 +543,10 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
|
||||
if (selectedElementIndex !== prevSelectedElementIndex) {
|
||||
if (selectedElementIndex === null) {
|
||||
selectedElementID = null;
|
||||
} else if (ownerFlatTree !== null) {
|
||||
selectedElementID =
|
||||
ownerFlatTree[((selectedElementIndex: any): number)].id;
|
||||
} else {
|
||||
if (ownerFlatTree !== null) {
|
||||
selectedElementID = ownerFlatTree[selectedElementIndex].id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -605,8 +561,7 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
|
||||
searchResults,
|
||||
searchText,
|
||||
|
||||
ownerStack,
|
||||
ownerStackIndex,
|
||||
ownerID,
|
||||
ownerFlatTree,
|
||||
};
|
||||
}
|
||||
@@ -619,20 +574,37 @@ function reduceSuspenseState(
|
||||
const { type } = action;
|
||||
switch (type) {
|
||||
case 'UPDATE_INSPECTED_ELEMENT_ID':
|
||||
return {
|
||||
...state,
|
||||
inspectedElementID: state.selectedElementID,
|
||||
};
|
||||
if (state.inspectedElementID !== state.selectedElementID) {
|
||||
return {
|
||||
...state,
|
||||
inspectedElementID: state.selectedElementID,
|
||||
};
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// React can bailout of no-op updates.
|
||||
return state;
|
||||
break;
|
||||
}
|
||||
|
||||
// React can bailout of no-op updates.
|
||||
return state;
|
||||
}
|
||||
|
||||
type Props = {| children: React$Node |};
|
||||
type Props = {|
|
||||
children: React$Node,
|
||||
|
||||
// Used for automated testing
|
||||
defaultOwnerID?: ?number,
|
||||
defaultSelectedElementID?: ?number,
|
||||
defaultSelectedElementIndex?: ?number,
|
||||
|};
|
||||
|
||||
// TODO Remove TreeContextController wrapper element once global ConsearchText.write API exists.
|
||||
function TreeContextController({ children }: Props) {
|
||||
function TreeContextController({
|
||||
children,
|
||||
defaultOwnerID,
|
||||
defaultSelectedElementID,
|
||||
defaultSelectedElementIndex,
|
||||
}: Props) {
|
||||
const bridge = useContext(BridgeContext);
|
||||
const store = useContext(StoreContext);
|
||||
|
||||
@@ -687,8 +659,10 @@ function TreeContextController({ children }: Props) {
|
||||
const [state, dispatch] = useReducer(reducer, {
|
||||
// Tree
|
||||
numElements: store.numElements,
|
||||
selectedElementIndex: null,
|
||||
selectedElementID: null,
|
||||
selectedElementID:
|
||||
defaultSelectedElementID == null ? null : defaultSelectedElementID,
|
||||
selectedElementIndex:
|
||||
defaultSelectedElementIndex == null ? null : defaultSelectedElementIndex,
|
||||
|
||||
// Search
|
||||
searchIndex: null,
|
||||
@@ -696,8 +670,7 @@ function TreeContextController({ children }: Props) {
|
||||
searchText: '',
|
||||
|
||||
// Owners
|
||||
ownerStack: [],
|
||||
ownerStackIndex: null,
|
||||
ownerID: defaultOwnerID == null ? null : defaultOwnerID,
|
||||
ownerFlatTree: null,
|
||||
|
||||
// Inspection element panel
|
||||
|
||||
@@ -31,10 +31,15 @@ export type Element = {|
|
||||
|};
|
||||
|
||||
export type Owner = {|
|
||||
displayName: string,
|
||||
displayName: string | null,
|
||||
id: number,
|
||||
|};
|
||||
|
||||
export type OwnersList = {|
|
||||
id: number,
|
||||
owners: Array<Owner> | null,
|
||||
|};
|
||||
|
||||
export type InspectedElement = {|
|
||||
id: number,
|
||||
|
||||
@@ -54,6 +59,7 @@ export type InspectedElement = {|
|
||||
|
||||
// Inspectable properties.
|
||||
context: Object | null,
|
||||
events: Object | null,
|
||||
hooks: Object | null,
|
||||
props: Object | null,
|
||||
state: Object | null,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import '@reach/menu-button/styles.css';
|
||||
import '@reach/tooltip/styles.css';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import Store from '../store';
|
||||
import { BridgeContext, StoreContext } from './context';
|
||||
import Components from './Components/Components';
|
||||
@@ -71,8 +71,7 @@ const settingsTab = {
|
||||
title: 'React Settings',
|
||||
};
|
||||
|
||||
const tabsWithProfiler = [componentsTab, profilerTab, settingsTab];
|
||||
const tabsWithoutProfiler = [componentsTab, settingsTab];
|
||||
const tabs = [componentsTab, profilerTab, settingsTab];
|
||||
|
||||
export default function DevTools({
|
||||
bridge,
|
||||
@@ -92,28 +91,6 @@ export default function DevTools({
|
||||
setTab(overrideTab);
|
||||
}
|
||||
|
||||
const [supportsProfiling, setSupportsProfiling] = useState(
|
||||
store.supportsProfiling
|
||||
);
|
||||
|
||||
// Show/hide the "Profiler" button depending on if profiling is supported.
|
||||
useEffect(() => {
|
||||
if (supportsProfiling !== store.supportsProfiling) {
|
||||
setSupportsProfiling(store.supportsProfiling);
|
||||
}
|
||||
|
||||
const handleRoots = () => {
|
||||
if (supportsProfiling !== store.supportsProfiling) {
|
||||
setSupportsProfiling(store.supportsProfiling);
|
||||
}
|
||||
};
|
||||
|
||||
store.addListener('roots', handleRoots);
|
||||
return () => {
|
||||
store.removeListener('roots', handleRoots);
|
||||
};
|
||||
}, [store, supportsProfiling]);
|
||||
|
||||
return (
|
||||
<BridgeContext.Provider value={bridge}>
|
||||
<StoreContext.Provider value={store}>
|
||||
@@ -140,11 +117,7 @@ export default function DevTools({
|
||||
id="DevTools"
|
||||
selectTab={setTab}
|
||||
size="large"
|
||||
tabs={
|
||||
supportsProfiling
|
||||
? tabsWithProfiler
|
||||
: tabsWithoutProfiler
|
||||
}
|
||||
tabs={tabs}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -158,10 +131,7 @@ export default function DevTools({
|
||||
className={styles.TabContent}
|
||||
hidden={tab !== 'profiler'}
|
||||
>
|
||||
<Profiler
|
||||
portalContainer={profilerPortalContainer}
|
||||
supportsProfiling={supportsProfiling}
|
||||
/>
|
||||
<Profiler portalContainer={profilerPortalContainer} />
|
||||
</div>
|
||||
<div
|
||||
className={styles.TabContent}
|
||||
|
||||
@@ -13,6 +13,7 @@ type Props = {|
|
||||
onClick: (event: SyntheticMouseEvent<*>) => mixed,
|
||||
onDoubleClick?: (event: SyntheticMouseEvent<*>) => mixed,
|
||||
placeLabelAboveNode?: boolean,
|
||||
textStyle?: Object,
|
||||
width: number,
|
||||
x: number,
|
||||
y: number,
|
||||
@@ -27,6 +28,7 @@ export default function ChartNode({
|
||||
label,
|
||||
onClick,
|
||||
onDoubleClick,
|
||||
textStyle,
|
||||
width,
|
||||
x,
|
||||
y,
|
||||
@@ -57,7 +59,9 @@ export default function ChartNode({
|
||||
}}
|
||||
y={height < textHeight ? -textHeight : 0}
|
||||
>
|
||||
<div className={styles.Div}>{label}</div>
|
||||
<div className={styles.Div} style={textStyle}>
|
||||
{label}
|
||||
</div>
|
||||
</foreignObject>
|
||||
)}
|
||||
</g>
|
||||
|
||||
@@ -8,13 +8,14 @@ import { StoreContext } from '../context';
|
||||
|
||||
export default function ClearProfilingDataButton() {
|
||||
const store = useContext(StoreContext);
|
||||
const { isProfiling } = useContext(ProfilerContext);
|
||||
const { didRecordCommits, isProfiling } = useContext(ProfilerContext);
|
||||
const { profilerStore } = store;
|
||||
|
||||
const clear = useCallback(() => store.clearProfilingData(), [store]);
|
||||
const clear = useCallback(() => profilerStore.clear(), [profilerStore]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
disabled={isProfiling || !store.hasProfilingData}
|
||||
disabled={isProfiling || !didRecordCommits}
|
||||
onClick={clear}
|
||||
title="Clear profiling data"
|
||||
>
|
||||
|
||||
@@ -8,6 +8,16 @@ import { useModalDismissSignal } from '../hooks';
|
||||
import styles from './CommitFilterModal.css';
|
||||
|
||||
export default function FilterModal(_: {||}) {
|
||||
const { isModalShowing } = useContext(CommitFilterModalContext);
|
||||
|
||||
if (!isModalShowing) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <FilterModalImpl />;
|
||||
}
|
||||
|
||||
function FilterModalImpl(_: {||}) {
|
||||
const {
|
||||
isCommitFilterEnabled,
|
||||
minCommitDuration,
|
||||
|
||||
@@ -3,3 +3,8 @@
|
||||
height: 100%;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.PatternPath {
|
||||
stroke: var(--color-commit-did-not-render-pattern);
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @flow
|
||||
|
||||
import React, { useCallback, useContext, useMemo } from 'react';
|
||||
import React, { forwardRef, useCallback, useContext, useMemo } from 'react';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import { FixedSizeList } from 'react-window';
|
||||
import { ProfilerContext } from './ProfilerContext';
|
||||
@@ -13,7 +13,7 @@ import { StoreContext } from '../context';
|
||||
import styles from './CommitFlamegraph.css';
|
||||
|
||||
import type { ChartData, ChartNode } from './FlamegraphChartBuilder';
|
||||
import type { CommitDetailsFrontend, CommitTreeFrontend } from './types';
|
||||
import type { CommitTree } from './types';
|
||||
|
||||
export type ItemData = {|
|
||||
chartData: ChartData,
|
||||
@@ -25,10 +25,11 @@ export type ItemData = {|
|
||||
|};
|
||||
|
||||
export default function CommitFlamegraphAutoSizer(_: {||}) {
|
||||
const { profilingCache } = useContext(StoreContext);
|
||||
const { rendererID, rootID, selectedCommitIndex, selectFiber } = useContext(
|
||||
const { profilerStore } = useContext(StoreContext);
|
||||
const { rootID, selectedCommitIndex, selectFiber } = useContext(
|
||||
ProfilerContext
|
||||
);
|
||||
const { profilingCache } = profilerStore;
|
||||
|
||||
const deselectCurrentFiber = useCallback(
|
||||
event => {
|
||||
@@ -38,39 +39,22 @@ export default function CommitFlamegraphAutoSizer(_: {||}) {
|
||||
[selectFiber]
|
||||
);
|
||||
|
||||
const profilingSummary = profilingCache.ProfilingSummary.read({
|
||||
rendererID: ((rendererID: any): number),
|
||||
rootID: ((rootID: any): number),
|
||||
});
|
||||
|
||||
let commitDetails: CommitDetailsFrontend | null = null;
|
||||
let commitTree: CommitTreeFrontend | null = null;
|
||||
let commitTree: CommitTree | null = null;
|
||||
let chartData: ChartData | null = null;
|
||||
if (selectedCommitIndex !== null) {
|
||||
commitDetails = profilingCache.CommitDetails.read({
|
||||
commitTree = profilingCache.getCommitTree({
|
||||
commitIndex: selectedCommitIndex,
|
||||
rendererID: ((rendererID: any): number),
|
||||
rootID: ((rootID: any): number),
|
||||
});
|
||||
|
||||
commitTree = profilingCache.getCommitTree({
|
||||
commitIndex: selectedCommitIndex,
|
||||
profilingSummary,
|
||||
});
|
||||
|
||||
chartData = profilingCache.getFlamegraphChartData({
|
||||
commitDetails,
|
||||
commitIndex: selectedCommitIndex,
|
||||
commitTree,
|
||||
rootID: ((rootID: any): number),
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
commitDetails != null &&
|
||||
commitTree != null &&
|
||||
chartData != null &&
|
||||
chartData.depth > 0
|
||||
) {
|
||||
if (commitTree != null && chartData != null && chartData.depth > 0) {
|
||||
return (
|
||||
<div className={styles.Container} onClick={deselectCurrentFiber}>
|
||||
<AutoSizer>
|
||||
@@ -79,8 +63,7 @@ export default function CommitFlamegraphAutoSizer(_: {||}) {
|
||||
// by the time this render prop function is called, the values of the `let` variables have not changed.
|
||||
<CommitFlamegraph
|
||||
chartData={((chartData: any): ChartData)}
|
||||
commitDetails={((commitDetails: any): CommitDetailsFrontend)}
|
||||
commitTree={((commitTree: any): CommitTreeFrontend)}
|
||||
commitTree={((commitTree: any): CommitTree)}
|
||||
height={height}
|
||||
width={width}
|
||||
/>
|
||||
@@ -95,19 +78,12 @@ export default function CommitFlamegraphAutoSizer(_: {||}) {
|
||||
|
||||
type Props = {|
|
||||
chartData: ChartData,
|
||||
commitDetails: CommitDetailsFrontend,
|
||||
commitTree: CommitTreeFrontend,
|
||||
commitTree: CommitTree,
|
||||
height: number,
|
||||
width: number,
|
||||
|};
|
||||
|
||||
function CommitFlamegraph({
|
||||
chartData,
|
||||
commitDetails,
|
||||
commitTree,
|
||||
height,
|
||||
width,
|
||||
}: Props) {
|
||||
function CommitFlamegraph({ chartData, commitTree, height, width }: Props) {
|
||||
const { selectFiber, selectedFiberID } = useContext(ProfilerContext);
|
||||
|
||||
const selectedChartNodeIndex = useMemo<number>(() => {
|
||||
@@ -123,9 +99,13 @@ function CommitFlamegraph({
|
||||
const selectedChartNode = useMemo(() => {
|
||||
let chartNode = null;
|
||||
if (selectedFiberID !== null) {
|
||||
chartNode = ((chartData.rows[selectedChartNodeIndex].find(
|
||||
const foundChartNode = chartData.rows[selectedChartNodeIndex].find(
|
||||
chartNode => chartNode.id === selectedFiberID
|
||||
): any): ChartNode);
|
||||
);
|
||||
|
||||
if (foundChartNode !== undefined) {
|
||||
chartNode = foundChartNode;
|
||||
}
|
||||
}
|
||||
return chartNode;
|
||||
}, [chartData, selectedFiberID, selectedChartNodeIndex]);
|
||||
@@ -152,7 +132,7 @@ function CommitFlamegraph({
|
||||
return (
|
||||
<FixedSizeList
|
||||
height={height}
|
||||
innerElementType="svg"
|
||||
innerElementType={InnerElementType}
|
||||
itemCount={chartData.depth}
|
||||
itemData={itemData}
|
||||
itemSize={barHeight}
|
||||
@@ -162,3 +142,22 @@ function CommitFlamegraph({
|
||||
</FixedSizeList>
|
||||
);
|
||||
}
|
||||
|
||||
const InnerElementType = forwardRef(({ children, ...rest }, ref) => (
|
||||
<svg ref={ref} {...rest}>
|
||||
<defs>
|
||||
<pattern
|
||||
id="didNotRenderPattern"
|
||||
patternUnits="userSpaceOnUse"
|
||||
width="4"
|
||||
height="4"
|
||||
>
|
||||
<path
|
||||
d="M-1,1 l2,-2 M0,4 l4,-4 M3,5 l2,-2"
|
||||
className={styles.PatternPath}
|
||||
/>
|
||||
</pattern>
|
||||
</defs>
|
||||
{children}
|
||||
</svg>
|
||||
));
|
||||
|
||||
@@ -23,7 +23,7 @@ function CommitFlamegraphListItem({ data, index, style }: Props) {
|
||||
selectFiber,
|
||||
width,
|
||||
} = data;
|
||||
const { maxSelfDuration, rows } = chartData;
|
||||
const { renderPathNodes, maxSelfDuration, rows } = chartData;
|
||||
|
||||
const handleClick = useCallback(
|
||||
(event: SyntheticMouseEvent<*>, id: number, name: string) => {
|
||||
@@ -76,9 +76,14 @@ function CommitFlamegraphListItem({ data, index, style }: Props) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let color = 'var(--color-commit-did-not-render)';
|
||||
let color = 'url(#didNotRenderPattern)';
|
||||
let textColor = 'var(--color-commit-did-not-render-pattern-text)';
|
||||
if (didRender) {
|
||||
color = getGradientColor(selfDuration / maxSelfDuration);
|
||||
textColor = 'var(--color-commit-gradient-text)';
|
||||
} else if (renderPathNodes.has(id)) {
|
||||
color = 'var(--color-commit-did-not-render-fill)';
|
||||
textColor = 'var(--color-commit-did-not-render-fill-text)';
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -89,6 +94,7 @@ function CommitFlamegraphListItem({ data, index, style }: Props) {
|
||||
key={id}
|
||||
label={label}
|
||||
onClick={event => handleClick(event, id, name)}
|
||||
textStyle={{ color: textColor }}
|
||||
width={nodeWidth}
|
||||
x={nodeOffset - selectedNodeOffset}
|
||||
y={top}
|
||||
|
||||
@@ -13,7 +13,7 @@ import { StoreContext } from '../context';
|
||||
import styles from './CommitRanked.css';
|
||||
|
||||
import type { ChartData } from './RankedChartBuilder';
|
||||
import type { CommitDetailsFrontend, CommitTreeFrontend } from './types';
|
||||
import type { CommitTree } from './types';
|
||||
|
||||
export type ItemData = {|
|
||||
chartData: ChartData,
|
||||
@@ -25,10 +25,11 @@ export type ItemData = {|
|
||||
|};
|
||||
|
||||
export default function CommitRankedAutoSizer(_: {||}) {
|
||||
const { profilingCache } = useContext(StoreContext);
|
||||
const { rendererID, rootID, selectedCommitIndex, selectFiber } = useContext(
|
||||
const { profilerStore } = useContext(StoreContext);
|
||||
const { rootID, selectedCommitIndex, selectFiber } = useContext(
|
||||
ProfilerContext
|
||||
);
|
||||
const { profilingCache } = profilerStore;
|
||||
|
||||
const deselectCurrentFiber = useCallback(
|
||||
event => {
|
||||
@@ -38,47 +39,29 @@ export default function CommitRankedAutoSizer(_: {||}) {
|
||||
[selectFiber]
|
||||
);
|
||||
|
||||
const profilingSummary = profilingCache.ProfilingSummary.read({
|
||||
rendererID: ((rendererID: any): number),
|
||||
rootID: ((rootID: any): number),
|
||||
});
|
||||
|
||||
let commitDetails: CommitDetailsFrontend | null = null;
|
||||
let commitTree: CommitTreeFrontend | null = null;
|
||||
let commitTree: CommitTree | null = null;
|
||||
let chartData: ChartData | null = null;
|
||||
if (selectedCommitIndex !== null) {
|
||||
commitDetails = profilingCache.CommitDetails.read({
|
||||
commitTree = profilingCache.getCommitTree({
|
||||
commitIndex: selectedCommitIndex,
|
||||
rendererID: ((rendererID: any): number),
|
||||
rootID: ((rootID: any): number),
|
||||
});
|
||||
|
||||
commitTree = profilingCache.getCommitTree({
|
||||
commitIndex: selectedCommitIndex,
|
||||
profilingSummary,
|
||||
});
|
||||
|
||||
chartData = profilingCache.getRankedChartData({
|
||||
commitDetails,
|
||||
commitIndex: selectedCommitIndex,
|
||||
commitTree,
|
||||
rootID: ((rootID: any): number),
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
commitDetails != null &&
|
||||
commitTree != null &&
|
||||
chartData != null &&
|
||||
chartData.nodes.length > 0
|
||||
) {
|
||||
if (commitTree != null && chartData != null && chartData.nodes.length > 0) {
|
||||
return (
|
||||
<div className={styles.Container} onClick={deselectCurrentFiber}>
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
<CommitRanked
|
||||
chartData={((chartData: any): ChartData)}
|
||||
commitDetails={((commitDetails: any): CommitDetailsFrontend)}
|
||||
commitTree={((commitTree: any): CommitTreeFrontend)}
|
||||
commitTree={((commitTree: any): CommitTree)}
|
||||
height={height}
|
||||
width={width}
|
||||
/>
|
||||
@@ -93,19 +76,12 @@ export default function CommitRankedAutoSizer(_: {||}) {
|
||||
|
||||
type Props = {|
|
||||
chartData: ChartData,
|
||||
commitDetails: CommitDetailsFrontend,
|
||||
commitTree: CommitTreeFrontend,
|
||||
commitTree: CommitTree,
|
||||
height: number,
|
||||
width: number,
|
||||
|};
|
||||
|
||||
function CommitRanked({
|
||||
chartData,
|
||||
commitDetails,
|
||||
commitTree,
|
||||
height,
|
||||
width,
|
||||
}: Props) {
|
||||
function CommitRanked({ chartData, commitTree, height, width }: Props) {
|
||||
const { selectedFiberID, selectFiber } = useContext(ProfilerContext);
|
||||
|
||||
const selectedFiberIndex = useMemo(
|
||||
|
||||
@@ -9,14 +9,13 @@ import {
|
||||
} from 'src/constants';
|
||||
import { utfDecodeString } from 'src/utils';
|
||||
import { ElementTypeRoot } from 'src/types';
|
||||
import Store from 'src/devtools/store';
|
||||
import ProfilerStore from 'src/devtools/ProfilerStore';
|
||||
|
||||
import type { ElementType } from 'src/types';
|
||||
import type {
|
||||
CommitTreeFrontend,
|
||||
CommitTreeNodeFrontend,
|
||||
ProfilingSnapshotNode,
|
||||
ProfilingSummaryFrontend,
|
||||
CommitTree,
|
||||
CommitTreeNode,
|
||||
ProfilingDataForRootFrontend,
|
||||
} from 'src/devtools/views/Profiler/types';
|
||||
|
||||
const debug = (methodName, ...args) => {
|
||||
@@ -30,36 +29,40 @@ const debug = (methodName, ...args) => {
|
||||
}
|
||||
};
|
||||
|
||||
const rootToCommitTreeMap: Map<number, Array<CommitTreeFrontend>> = new Map();
|
||||
const rootToCommitTreeMap: Map<number, Array<CommitTree>> = new Map();
|
||||
|
||||
export function getCommitTree({
|
||||
commitIndex,
|
||||
profilingSummary,
|
||||
store,
|
||||
profilerStore,
|
||||
rootID,
|
||||
}: {|
|
||||
commitIndex: number,
|
||||
profilingSummary: ProfilingSummaryFrontend,
|
||||
store: Store,
|
||||
|}): CommitTreeFrontend {
|
||||
const { rootID } = profilingSummary;
|
||||
|
||||
profilerStore: ProfilerStore,
|
||||
rootID: number,
|
||||
|}): CommitTree {
|
||||
if (!rootToCommitTreeMap.has(rootID)) {
|
||||
rootToCommitTreeMap.set(rootID, []);
|
||||
}
|
||||
|
||||
const commitTrees = ((rootToCommitTreeMap.get(
|
||||
rootID
|
||||
): any): Array<CommitTreeFrontend>);
|
||||
): any): Array<CommitTree>);
|
||||
|
||||
if (commitIndex < commitTrees.length) {
|
||||
return commitTrees[commitIndex];
|
||||
}
|
||||
|
||||
const { importedProfilingData } = store;
|
||||
const profilingOperations =
|
||||
importedProfilingData != null
|
||||
? importedProfilingData.profilingOperations
|
||||
: store.profilingOperations;
|
||||
const { profilingData } = profilerStore;
|
||||
if (profilingData === null) {
|
||||
throw Error(`No profiling data available`);
|
||||
}
|
||||
|
||||
const dataForRoot = profilingData.dataForRoots.get(rootID);
|
||||
if (dataForRoot == null) {
|
||||
throw Error(`Could not find profiling data for root "${rootID}"`);
|
||||
}
|
||||
|
||||
const { operations } = dataForRoot;
|
||||
|
||||
// Commits are generated sequentially and cached.
|
||||
// If this is the very first commit, start with the cached snapshot and apply the first mutation.
|
||||
@@ -67,32 +70,12 @@ export function getCommitTree({
|
||||
if (commitIndex === 0) {
|
||||
const nodes = new Map();
|
||||
|
||||
const { importedProfilingData } = store;
|
||||
const profilingSnapshots =
|
||||
importedProfilingData != null
|
||||
? importedProfilingData.profilingSnapshots.get(rootID)
|
||||
: store.profilingSnapshots.get(rootID);
|
||||
|
||||
if (profilingSnapshots == null) {
|
||||
throw Error(`Could not find profiling snapshot for root "${rootID}"`);
|
||||
}
|
||||
|
||||
// Construct the initial tree.
|
||||
recursivelyInitializeTree(
|
||||
rootID,
|
||||
0,
|
||||
nodes,
|
||||
profilingSummary.initialTreeBaseDurations,
|
||||
profilingSnapshots
|
||||
);
|
||||
recursivelyInitializeTree(rootID, 0, nodes, dataForRoot);
|
||||
|
||||
// Mutate the tree
|
||||
const commitOperations = profilingOperations.get(rootID);
|
||||
if (commitOperations != null && commitIndex < commitOperations.length) {
|
||||
const commitTree = updateTree(
|
||||
{ nodes, rootID },
|
||||
commitOperations[commitIndex]
|
||||
);
|
||||
if (operations != null && commitIndex < operations.length) {
|
||||
const commitTree = updateTree({ nodes, rootID }, operations[commitIndex]);
|
||||
|
||||
if (__DEBUG__) {
|
||||
__printTree(commitTree);
|
||||
@@ -104,14 +87,14 @@ export function getCommitTree({
|
||||
} else {
|
||||
const previousCommitTree = getCommitTree({
|
||||
commitIndex: commitIndex - 1,
|
||||
profilingSummary,
|
||||
store,
|
||||
profilerStore,
|
||||
rootID,
|
||||
});
|
||||
const commitOperations = profilingOperations.get(rootID);
|
||||
if (commitOperations != null && commitIndex < commitOperations.length) {
|
||||
|
||||
if (operations != null && commitIndex < operations.length) {
|
||||
const commitTree = updateTree(
|
||||
previousCommitTree,
|
||||
commitOperations[commitIndex]
|
||||
operations[commitIndex]
|
||||
);
|
||||
|
||||
if (__DEBUG__) {
|
||||
@@ -131,11 +114,10 @@ export function getCommitTree({
|
||||
function recursivelyInitializeTree(
|
||||
id: number,
|
||||
parentID: number,
|
||||
nodes: Map<number, CommitTreeNodeFrontend>,
|
||||
initialTreeBaseDurations: Map<number, number>,
|
||||
profilingSnapshots: Map<number, ProfilingSnapshotNode>
|
||||
nodes: Map<number, CommitTreeNode>,
|
||||
dataForRoot: ProfilingDataForRootFrontend
|
||||
): void {
|
||||
const node = profilingSnapshots.get(id);
|
||||
const node = dataForRoot.snapshots.get(id);
|
||||
if (node != null) {
|
||||
nodes.set(id, {
|
||||
id,
|
||||
@@ -143,34 +125,31 @@ function recursivelyInitializeTree(
|
||||
displayName: node.displayName,
|
||||
key: node.key,
|
||||
parentID,
|
||||
treeBaseDuration: ((initialTreeBaseDurations.get(id): any): number),
|
||||
treeBaseDuration: ((dataForRoot.initialTreeBaseDurations.get(
|
||||
id
|
||||
): any): number),
|
||||
type: node.type,
|
||||
});
|
||||
|
||||
node.children.forEach(childID =>
|
||||
recursivelyInitializeTree(
|
||||
childID,
|
||||
id,
|
||||
nodes,
|
||||
initialTreeBaseDurations,
|
||||
profilingSnapshots
|
||||
)
|
||||
recursivelyInitializeTree(childID, id, nodes, dataForRoot)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function updateTree(
|
||||
commitTree: CommitTreeFrontend,
|
||||
commitTree: CommitTree,
|
||||
operations: Uint32Array
|
||||
): CommitTreeFrontend {
|
||||
): CommitTree {
|
||||
// Clone the original tree so edits don't affect it.
|
||||
const nodes = new Map(commitTree.nodes);
|
||||
|
||||
// Clone nodes before mutating them so edits don't affect them.
|
||||
const getClonedNode = (id: number): CommitTreeNodeFrontend => {
|
||||
const getClonedNode = (id: number): CommitTreeNode => {
|
||||
const clonedNode = ((Object.assign(
|
||||
{},
|
||||
nodes.get(id)
|
||||
): any): CommitTreeNodeFrontend);
|
||||
): any): CommitTreeNode);
|
||||
nodes.set(id, clonedNode);
|
||||
return clonedNode;
|
||||
};
|
||||
@@ -218,13 +197,14 @@ function updateTree(
|
||||
debug('Add', `new root fiber ${id}`);
|
||||
}
|
||||
|
||||
const node: CommitTreeNodeFrontend = {
|
||||
const node: CommitTreeNode = {
|
||||
children: [],
|
||||
displayName: null,
|
||||
id,
|
||||
key: null,
|
||||
parentID: 0,
|
||||
treeBaseDuration: 0, // This will be updated by a subsequent operation
|
||||
type,
|
||||
};
|
||||
|
||||
nodes.set(id, node);
|
||||
@@ -252,13 +232,14 @@ function updateTree(
|
||||
const parentNode = getClonedNode(parentID);
|
||||
parentNode.children = parentNode.children.concat(id);
|
||||
|
||||
const node: CommitTreeNodeFrontend = {
|
||||
const node: CommitTreeNode = {
|
||||
children: [],
|
||||
displayName,
|
||||
id,
|
||||
key,
|
||||
parentID,
|
||||
treeBaseDuration: 0, // This will be updated by a subsequent operation
|
||||
type,
|
||||
};
|
||||
|
||||
nodes.set(id, node);
|
||||
@@ -286,10 +267,11 @@ function updateTree(
|
||||
|
||||
nodes.delete(id);
|
||||
|
||||
const parentNode = getClonedNode(parentID);
|
||||
if (parentNode == null) {
|
||||
if (!nodes.has(parentID)) {
|
||||
// No-op
|
||||
} else {
|
||||
const parentNode = getClonedNode(parentID);
|
||||
|
||||
if (__DEBUG__) {
|
||||
debug('Remove', `fiber ${id} from parent ${parentID}`);
|
||||
}
|
||||
@@ -352,7 +334,7 @@ export function invalidateCommitTrees(): void {
|
||||
}
|
||||
|
||||
// DEBUG
|
||||
const __printTree = (commitTree: CommitTreeFrontend) => {
|
||||
const __printTree = (commitTree: CommitTree) => {
|
||||
if (__DEBUG__) {
|
||||
const { nodes, rootID } = commitTree;
|
||||
console.group('__printTree()');
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// @flow
|
||||
|
||||
import { calculateSelfDuration, formatDuration } from './utils';
|
||||
import { ElementTypeForwardRef, ElementTypeMemo } from 'src/types';
|
||||
import { formatDuration } from './utils';
|
||||
import ProfilerStore from 'src/devtools/ProfilerStore';
|
||||
|
||||
import type { CommitDetailsFrontend, CommitTreeFrontend } from './types';
|
||||
import type { CommitTree } from './types';
|
||||
|
||||
export type ChartNode = {|
|
||||
actualDuration: number,
|
||||
@@ -20,21 +22,26 @@ export type ChartData = {|
|
||||
depth: number,
|
||||
idToDepthMap: Map<number, number>,
|
||||
maxSelfDuration: number,
|
||||
renderPathNodes: Set<number>,
|
||||
rows: Array<Array<ChartNode>>,
|
||||
|};
|
||||
|
||||
const cachedChartData: Map<string, ChartData> = new Map();
|
||||
|
||||
export function getChartData({
|
||||
commitDetails,
|
||||
commitIndex,
|
||||
commitTree,
|
||||
profilerStore,
|
||||
rootID,
|
||||
}: {|
|
||||
commitDetails: CommitDetailsFrontend,
|
||||
commitIndex: number,
|
||||
commitTree: CommitTreeFrontend,
|
||||
commitTree: CommitTree,
|
||||
profilerStore: ProfilerStore,
|
||||
rootID: number,
|
||||
|}): ChartData {
|
||||
const { actualDurations, rootID } = commitDetails;
|
||||
const commitDatum = profilerStore.getCommitData(rootID, commitIndex);
|
||||
|
||||
const { fiberActualDurations, fiberSelfDurations } = commitDatum;
|
||||
const { nodes } = commitTree;
|
||||
|
||||
const key = `${rootID}-${commitIndex}`;
|
||||
@@ -43,6 +50,7 @@ export function getChartData({
|
||||
}
|
||||
|
||||
const idToDepthMap: Map<number, number> = new Map();
|
||||
const renderPathNodes: Set<number> = new Set();
|
||||
const rows: Array<Array<ChartNode>> = [];
|
||||
|
||||
let maxDepth = 0;
|
||||
@@ -57,16 +65,23 @@ export function getChartData({
|
||||
throw Error(`Could not find node with id "${id}" in commit tree`);
|
||||
}
|
||||
|
||||
const { children, displayName, key, treeBaseDuration } = node;
|
||||
const { children, displayName, key, treeBaseDuration, type } = node;
|
||||
|
||||
const actualDuration = actualDurations.get(id) || 0;
|
||||
const selfDuration = calculateSelfDuration(id, commitTree, commitDetails);
|
||||
const didRender = actualDurations.has(id);
|
||||
const actualDuration = fiberActualDurations.get(id) || 0;
|
||||
const selfDuration = fiberSelfDurations.get(id) || 0;
|
||||
const didRender = fiberActualDurations.has(id);
|
||||
|
||||
const name = displayName || 'Unknown';
|
||||
const name = displayName || 'Anonymous';
|
||||
const maybeKey = key !== null ? ` key="${key}"` : '';
|
||||
|
||||
let label = `${name}${maybeKey}`;
|
||||
let maybeBadge = '';
|
||||
if (type === ElementTypeForwardRef) {
|
||||
maybeBadge = ' (ForwardRef)';
|
||||
} else if (type === ElementTypeMemo) {
|
||||
maybeBadge = ' (Memo)';
|
||||
}
|
||||
|
||||
let label = `${name}${maybeBadge}${maybeKey}`;
|
||||
if (didRender) {
|
||||
label += ` (${formatDuration(selfDuration)}ms of ${formatDuration(
|
||||
actualDuration
|
||||
@@ -102,23 +117,47 @@ export function getChartData({
|
||||
return chartNode;
|
||||
};
|
||||
|
||||
// Skip over the root; we don't want to show it in the flamegraph.
|
||||
const root = nodes.get(rootID);
|
||||
if (root == null) {
|
||||
throw Error(`Could not find root node with id "${rootID}" in commit tree`);
|
||||
}
|
||||
|
||||
// Don't assume a single root.
|
||||
// Component filters or Fragments might lead to multiple "roots" in a flame graph.
|
||||
let baseDuration = 0;
|
||||
for (let i = root.children.length - 1; i >= 0; i--) {
|
||||
const id = root.children[i];
|
||||
const node = nodes.get(id);
|
||||
if (node == null) {
|
||||
throw Error(`Could not find node with id "${id}" in commit tree`);
|
||||
|
||||
// Special case to handle unmounted roots.
|
||||
if (nodes.size > 0) {
|
||||
// Skip over the root; we don't want to show it in the flamegraph.
|
||||
const root = nodes.get(rootID);
|
||||
if (root == null) {
|
||||
throw Error(
|
||||
`Could not find root node with id "${rootID}" in commit tree`
|
||||
);
|
||||
}
|
||||
baseDuration += node.treeBaseDuration;
|
||||
walkTree(id, baseDuration, 1);
|
||||
|
||||
// Don't assume a single root.
|
||||
// Component filters or Fragments might lead to multiple "roots" in a flame graph.
|
||||
for (let i = root.children.length - 1; i >= 0; i--) {
|
||||
const id = root.children[i];
|
||||
const node = nodes.get(id);
|
||||
if (node == null) {
|
||||
throw Error(`Could not find node with id "${id}" in commit tree`);
|
||||
}
|
||||
baseDuration += node.treeBaseDuration;
|
||||
walkTree(id, baseDuration, 1);
|
||||
}
|
||||
|
||||
fiberActualDurations.forEach((duration, id) => {
|
||||
const node = nodes.get(id);
|
||||
if (node != null) {
|
||||
let currentID = node.parentID;
|
||||
while (currentID !== 0) {
|
||||
if (renderPathNodes.has(currentID)) {
|
||||
// We've already walked this path; we can skip it.
|
||||
break;
|
||||
} else {
|
||||
renderPathNodes.add(currentID);
|
||||
}
|
||||
|
||||
const node = nodes.get(currentID);
|
||||
currentID = node != null ? node.parentID : 0;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const chartData = {
|
||||
@@ -126,6 +165,7 @@ export function getChartData({
|
||||
depth: maxDepth,
|
||||
idToDepthMap,
|
||||
maxSelfDuration,
|
||||
renderPathNodes,
|
||||
rows,
|
||||
};
|
||||
|
||||
|
||||
@@ -28,7 +28,8 @@
|
||||
.InteractionLine {
|
||||
position: absolute;
|
||||
height: 3px;
|
||||
background-color: var(--color-commit-did-not-render);
|
||||
background-color: var(--color-commit-did-not-render-fill);
|
||||
color: var(--color-commit-did-not-render-fill-text);
|
||||
border-radius: 0.125rem;
|
||||
}
|
||||
|
||||
@@ -36,6 +37,7 @@
|
||||
position: absolute;
|
||||
width: var(--interaction-commit-size);
|
||||
height: var(--interaction-commit-size);
|
||||
background-color: var(--color-commit-did-not-render);
|
||||
background-color: var(--color-commit-did-not-render-fill);
|
||||
color: var(--color-commit-did-not-render-fill-text);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -17,9 +17,8 @@ type Props = {
|
||||
function InteractionListItem({ data: itemData, index, style }: Props) {
|
||||
const {
|
||||
chartData,
|
||||
interactions,
|
||||
dataForRoot,
|
||||
labelWidth,
|
||||
profilingSummary,
|
||||
scaleX,
|
||||
selectedInteractionID,
|
||||
selectCommitIndex,
|
||||
@@ -27,20 +26,22 @@ function InteractionListItem({ data: itemData, index, style }: Props) {
|
||||
selectTab,
|
||||
} = itemData;
|
||||
|
||||
const { maxCommitDuration } = chartData;
|
||||
const { commitDurations, commitTimes } = profilingSummary;
|
||||
const { commitData, interactionCommits } = dataForRoot;
|
||||
const { interactions, lastInteractionTime, maxCommitDuration } = chartData;
|
||||
|
||||
const interaction = interactions[index];
|
||||
if (interaction == null) {
|
||||
throw Error(`Could not find interaction #${index}`);
|
||||
}
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
selectInteraction(interaction.id);
|
||||
}, [interaction, selectInteraction]);
|
||||
|
||||
const commits = interactionCommits.get(interaction.id) || [];
|
||||
|
||||
const startTime = interaction.timestamp;
|
||||
const stopTime =
|
||||
interaction.commits.length > 0
|
||||
? commitTimes[interaction.commits[interaction.commits.length - 1]]
|
||||
: interaction.timestamp;
|
||||
const stopTime = lastInteractionTime;
|
||||
|
||||
const viewCommit = (commitIndex: number) => {
|
||||
selectTab('flame-chart');
|
||||
@@ -71,7 +72,7 @@ function InteractionListItem({ data: itemData, index, style }: Props) {
|
||||
width: scaleX(stopTime - startTime, 0),
|
||||
}}
|
||||
/>
|
||||
{interaction.commits.map(commitIndex => (
|
||||
{commits.map(commitIndex => (
|
||||
<div
|
||||
className={styles.CommitBox}
|
||||
key={commitIndex}
|
||||
@@ -80,10 +81,13 @@ function InteractionListItem({ data: itemData, index, style }: Props) {
|
||||
backgroundColor: getGradientColor(
|
||||
Math.min(
|
||||
1,
|
||||
Math.max(0, commitDurations[commitIndex] / maxCommitDuration)
|
||||
Math.max(
|
||||
0,
|
||||
commitData[commitIndex].duration / maxCommitDuration
|
||||
)
|
||||
) || 0
|
||||
),
|
||||
left: labelWidth + scaleX(commitTimes[commitIndex], 0),
|
||||
left: labelWidth + scaleX(commitData[commitIndex].timestamp, 0),
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -11,18 +11,14 @@ import { scale } from './utils';
|
||||
|
||||
import styles from './Interactions.css';
|
||||
|
||||
import type { ProfilingDataForRootFrontend } from './types';
|
||||
import type { ChartData } from './InteractionsChartBuilder';
|
||||
import type { TabID } from './ProfilerContext';
|
||||
import type {
|
||||
InteractionWithCommitsFrontend,
|
||||
ProfilingSummaryFrontend,
|
||||
} from './types';
|
||||
|
||||
export type ItemData = {|
|
||||
chartData: ChartData,
|
||||
interactions: Array<InteractionWithCommitsFrontend>,
|
||||
dataForRoot: ProfilingDataForRootFrontend,
|
||||
labelWidth: number,
|
||||
profilingSummary: ProfilingSummaryFrontend,
|
||||
scaleX: (value: number, fallbackValue: number) => number,
|
||||
selectedInteractionID: number | null,
|
||||
selectCommitIndex: (id: number | null) => void,
|
||||
@@ -42,30 +38,23 @@ export default function InteractionsAutoSizer(_: {||}) {
|
||||
|
||||
function Interactions({ height, width }: {| height: number, width: number |}) {
|
||||
const {
|
||||
rendererID,
|
||||
rootID,
|
||||
selectedInteractionID,
|
||||
selectInteraction,
|
||||
selectCommitIndex,
|
||||
selectTab,
|
||||
} = useContext(ProfilerContext);
|
||||
const { profilingCache } = useContext(StoreContext);
|
||||
const { profilerStore } = useContext(StoreContext);
|
||||
const { profilingCache } = profilerStore;
|
||||
|
||||
const interactions = profilingCache.Interactions.read({
|
||||
rendererID: ((rendererID: any): number),
|
||||
rootID: ((rootID: any): number),
|
||||
});
|
||||
|
||||
const profilingSummary = profilingCache.ProfilingSummary.read({
|
||||
rendererID: ((rendererID: any): number),
|
||||
rootID: ((rootID: any): number),
|
||||
});
|
||||
const dataForRoot = profilerStore.getDataForRoot(((rootID: any): number));
|
||||
|
||||
const chartData = profilingCache.getInteractionsChartData({
|
||||
interactions,
|
||||
profilingSummary,
|
||||
rootID: ((rootID: any): number),
|
||||
});
|
||||
|
||||
const { interactions } = chartData;
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
event => {
|
||||
let index;
|
||||
@@ -110,9 +99,8 @@ function Interactions({ height, width }: {| height: number, width: number |}) {
|
||||
|
||||
return {
|
||||
chartData,
|
||||
interactions,
|
||||
dataForRoot,
|
||||
labelWidth,
|
||||
profilingSummary,
|
||||
scaleX: scale(0, chartData.lastInteractionTime, 0, timelineWidth),
|
||||
selectedInteractionID,
|
||||
selectCommitIndex,
|
||||
@@ -121,8 +109,7 @@ function Interactions({ height, width }: {| height: number, width: number |}) {
|
||||
};
|
||||
}, [
|
||||
chartData,
|
||||
interactions,
|
||||
profilingSummary,
|
||||
dataForRoot,
|
||||
selectedInteractionID,
|
||||
selectCommitIndex,
|
||||
selectInteraction,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// @flow
|
||||
|
||||
import type {
|
||||
InteractionWithCommitsFrontend,
|
||||
ProfilingSummaryFrontend,
|
||||
} from './types';
|
||||
import ProfilerStore from 'src/devtools/ProfilerStore';
|
||||
|
||||
import type { Interaction } from './types';
|
||||
|
||||
export type ChartData = {|
|
||||
interactions: Array<Interaction>,
|
||||
lastInteractionTime: number,
|
||||
maxCommitDuration: number,
|
||||
|};
|
||||
@@ -13,30 +13,37 @@ export type ChartData = {|
|
||||
const cachedChartData: Map<number, ChartData> = new Map();
|
||||
|
||||
export function getChartData({
|
||||
interactions,
|
||||
profilingSummary,
|
||||
profilerStore,
|
||||
rootID,
|
||||
}: {|
|
||||
interactions: Array<InteractionWithCommitsFrontend>,
|
||||
profilingSummary: ProfilingSummaryFrontend,
|
||||
profilerStore: ProfilerStore,
|
||||
rootID: number,
|
||||
|}): ChartData {
|
||||
const { rootID } = profilingSummary;
|
||||
|
||||
if (cachedChartData.has(rootID)) {
|
||||
return ((cachedChartData.get(rootID): any): ChartData);
|
||||
}
|
||||
|
||||
const { commitDurations, commitTimes } = profilingSummary;
|
||||
const dataForRoot = profilerStore.getDataForRoot(rootID);
|
||||
if (dataForRoot == null) {
|
||||
throw Error(`Could not find profiling data for root "${rootID}"`);
|
||||
}
|
||||
|
||||
const { commitData, interactions } = dataForRoot;
|
||||
|
||||
const lastInteractionTime =
|
||||
commitTimes.length > 0 ? commitTimes[commitTimes.length - 1] : 0;
|
||||
commitData.length > 0 ? commitData[commitData.length - 1].timestamp : 0;
|
||||
|
||||
let maxCommitDuration = 0;
|
||||
|
||||
commitDurations.forEach(commitDuration => {
|
||||
maxCommitDuration = Math.max(maxCommitDuration, commitDuration);
|
||||
commitData.forEach(commitDatum => {
|
||||
maxCommitDuration = Math.max(maxCommitDuration, commitDatum.duration);
|
||||
});
|
||||
|
||||
const chartData = { lastInteractionTime, maxCommitDuration };
|
||||
const chartData = {
|
||||
interactions: Array.from(interactions.values()),
|
||||
lastInteractionTime,
|
||||
maxCommitDuration,
|
||||
};
|
||||
|
||||
cachedChartData.set(rootID, chartData);
|
||||
|
||||
|
||||
@@ -1,37 +1,19 @@
|
||||
// @flow
|
||||
|
||||
import React, { useContext } from 'react';
|
||||
import { ProfilerContext } from './ProfilerContext';
|
||||
import React from 'react';
|
||||
import ToggleCommitFilterModalButton from './ToggleCommitFilterModalButton';
|
||||
|
||||
import styles from './NoCommitData.css';
|
||||
|
||||
export default function NoCommitData(_: {||}) {
|
||||
const { rootHasProfilingData } = useContext(ProfilerContext);
|
||||
|
||||
if (rootHasProfilingData) {
|
||||
return (
|
||||
<div className={styles.NoCommitData}>
|
||||
<div className={styles.Header}>
|
||||
There is no data matching the current filter criteria.
|
||||
</div>
|
||||
<div className={styles.FilterMessage}>
|
||||
Try adjusting the commit filter <ToggleCommitFilterModalButton />
|
||||
</div>
|
||||
return (
|
||||
<div className={styles.NoCommitData}>
|
||||
<div className={styles.Header}>
|
||||
There is no data matching the current filter criteria.
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className={styles.NoCommitData}>
|
||||
<div className={styles.Header}>
|
||||
There is no timing data to display for the currently selected commit.
|
||||
</div>
|
||||
<div>
|
||||
This can indicate that a render occurred too quickly for the timing
|
||||
API to measure. Try selecting another commit in the upper, right-hand
|
||||
corner.
|
||||
</div>
|
||||
<div className={styles.FilterMessage}>
|
||||
Try adjusting the commit filter <ToggleCommitFilterModalButton />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
// @flow
|
||||
|
||||
import React, { Suspense, useContext } from 'react';
|
||||
import {
|
||||
CommitFilterModalContext,
|
||||
CommitFilterModalContextController,
|
||||
} from './CommitFilterModalContext';
|
||||
import React, { useContext } from 'react';
|
||||
import { CommitFilterModalContextController } from './CommitFilterModalContext';
|
||||
import { ModalDialog } from '../ModalDialog';
|
||||
import { ProfilerContext } from './ProfilerContext';
|
||||
import TabBar from '../TabBar';
|
||||
@@ -13,6 +10,7 @@ import CommitFlamegraph from './CommitFlamegraph';
|
||||
import CommitRanked from './CommitRanked';
|
||||
import CommitFilterModal from './CommitFilterModal';
|
||||
import Interactions from './Interactions';
|
||||
import RootSelector from './RootSelector';
|
||||
import RecordToggle from './RecordToggle';
|
||||
import ReloadAndProfileButton from './ReloadAndProfileButton';
|
||||
import ProfilingImportExportButtons from './ProfilingImportExportButtons';
|
||||
@@ -25,168 +23,93 @@ import portaledContent from '../portaledContent';
|
||||
|
||||
import styles from './Profiler.css';
|
||||
|
||||
export type Props = {|
|
||||
supportsProfiling: boolean,
|
||||
|};
|
||||
function Profiler(_: {||}) {
|
||||
const {
|
||||
didRecordCommits,
|
||||
isProcessingData,
|
||||
isProfiling,
|
||||
selectedFiberID,
|
||||
selectedTabID,
|
||||
selectTab,
|
||||
supportsProfiling,
|
||||
} = useContext(ProfilerContext);
|
||||
|
||||
function Profiler({ supportsProfiling }: Props) {
|
||||
const { hasProfilingData, isProfiling, rootHasProfilingData } = useContext(
|
||||
ProfilerContext
|
||||
);
|
||||
|
||||
if (isProfiling || !rootHasProfilingData) {
|
||||
return (
|
||||
<NonSuspendingProfiler
|
||||
hasProfilingData={hasProfilingData}
|
||||
isProfiling={isProfiling}
|
||||
supportsProfiling={supportsProfiling}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<CommitFilterModalContextController>
|
||||
<SuspendingProfiler />
|
||||
</CommitFilterModalContextController>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// This view is rendered when there is no profiler data (either we haven't profiled yet or we're currently profiling).
|
||||
// Nothing in this view's subtree suspends.
|
||||
// By not suspending while profiling is in progress, we avoid potential cache invalidation trickiness.
|
||||
// NOTE that the structure of this UI should mirror SuspendingProfiler.
|
||||
function NonSuspendingProfiler({
|
||||
hasProfilingData,
|
||||
isProfiling,
|
||||
supportsProfiling,
|
||||
}: {|
|
||||
hasProfilingData: boolean,
|
||||
isProfiling: boolean,
|
||||
supportsProfiling: boolean,
|
||||
|}) {
|
||||
let view = null;
|
||||
if (!supportsProfiling) {
|
||||
view = <ProfilingNotSupported />;
|
||||
if (didRecordCommits) {
|
||||
switch (selectedTabID) {
|
||||
case 'flame-chart':
|
||||
view = <CommitFlamegraph />;
|
||||
break;
|
||||
case 'ranked-chart':
|
||||
view = <CommitRanked />;
|
||||
break;
|
||||
case 'interactions':
|
||||
view = <Interactions />;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} else if (isProfiling) {
|
||||
view = <RecortdingInProgress />;
|
||||
} else if (!hasProfilingData) {
|
||||
view = <RecordingInProgress />;
|
||||
} else if (isProcessingData) {
|
||||
view = <ProcessingData />;
|
||||
} else if (supportsProfiling) {
|
||||
view = <NoProfilingData />;
|
||||
} else {
|
||||
view = <NoProfilingDataForRoot />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.Profiler}>
|
||||
<div className={styles.LeftColumn}>
|
||||
<div className={styles.Toolbar}>
|
||||
<RecordToggle disabled={!supportsProfiling} />
|
||||
<ReloadAndProfileButton />
|
||||
<ClearProfilingDataButton />
|
||||
<ProfilingImportExportButtons />
|
||||
<div className={styles.VRule} />
|
||||
<TabBar
|
||||
currentTab={null}
|
||||
disabled
|
||||
id="Profiler"
|
||||
selectTab={() => {}}
|
||||
size="small"
|
||||
tabs={tabs}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.Content}>
|
||||
{view}
|
||||
<ModalDialog />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ContentFallback() {
|
||||
return <div className={styles.ContentFallback}>Loading...</div>;
|
||||
}
|
||||
|
||||
function SnapshotSelectorFallback() {
|
||||
return <div className={styles.SnapshotSelectorFallback}>Loading...</div>;
|
||||
}
|
||||
|
||||
// This view is rendered when there is profiler data (even though there may not be any for the currently selected root).
|
||||
// This view's subtree uses suspense to request profiler data from the backend.
|
||||
// NOTE that the structure of this UI should mirror NonSuspendingProfiler.
|
||||
function SuspendingProfiler() {
|
||||
const { selectedFiberID, selectedTabID, selectTab } = useContext(
|
||||
ProfilerContext
|
||||
);
|
||||
|
||||
const { isModalShowing: isFilterModalShowing } = useContext(
|
||||
CommitFilterModalContext
|
||||
);
|
||||
|
||||
let view = null;
|
||||
switch (selectedTabID) {
|
||||
case 'flame-chart':
|
||||
view = <CommitFlamegraph />;
|
||||
break;
|
||||
case 'ranked-chart':
|
||||
view = <CommitRanked />;
|
||||
break;
|
||||
case 'interactions':
|
||||
view = <Interactions />;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
view = <ProfilingNotSupported />;
|
||||
}
|
||||
|
||||
let sidebar = null;
|
||||
switch (selectedTabID) {
|
||||
case 'interactions':
|
||||
sidebar = <SidebarInteractions />;
|
||||
break;
|
||||
case 'flame-chart':
|
||||
case 'ranked-chart':
|
||||
if (selectedFiberID !== null) {
|
||||
sidebar = <SidebarSelectedFiberInfo />;
|
||||
} else {
|
||||
sidebar = <SidebarCommitInfo />;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
if (!isProfiling && !isProcessingData && didRecordCommits) {
|
||||
switch (selectedTabID) {
|
||||
case 'interactions':
|
||||
sidebar = <SidebarInteractions />;
|
||||
break;
|
||||
case 'flame-chart':
|
||||
case 'ranked-chart':
|
||||
if (selectedFiberID !== null) {
|
||||
sidebar = <SidebarSelectedFiberInfo />;
|
||||
} else {
|
||||
sidebar = <SidebarCommitInfo />;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.Profiler}>
|
||||
<div className={styles.LeftColumn}>
|
||||
<div className={styles.Toolbar}>
|
||||
<RecordToggle />
|
||||
<ReloadAndProfileButton />
|
||||
<ClearProfilingDataButton />
|
||||
<ProfilingImportExportButtons />
|
||||
<div className={styles.VRule} />
|
||||
<TabBar
|
||||
currentTab={selectedTabID}
|
||||
id="Profiler"
|
||||
selectTab={selectTab}
|
||||
size="small"
|
||||
tabs={tabs}
|
||||
/>
|
||||
<div className={styles.Spacer} />
|
||||
<ToggleCommitFilterModalButton />
|
||||
<div className={styles.VRule} />
|
||||
<Suspense fallback={<SnapshotSelectorFallback />}>
|
||||
<SnapshotSelector />
|
||||
</Suspense>
|
||||
</div>
|
||||
<div className={styles.Content}>
|
||||
<Suspense fallback={<ContentFallback />}>{view}</Suspense>
|
||||
{isFilterModalShowing && <CommitFilterModal />}
|
||||
<ModalDialog />
|
||||
<CommitFilterModalContextController>
|
||||
<div className={styles.Profiler}>
|
||||
<div className={styles.LeftColumn}>
|
||||
<div className={styles.Toolbar}>
|
||||
<RecordToggle disabled={!supportsProfiling} />
|
||||
<ReloadAndProfileButton />
|
||||
<ClearProfilingDataButton />
|
||||
<ProfilingImportExportButtons />
|
||||
<div className={styles.VRule} />
|
||||
<TabBar
|
||||
currentTab={selectedTabID}
|
||||
id="Profiler"
|
||||
selectTab={selectTab}
|
||||
size="small"
|
||||
tabs={tabs}
|
||||
/>
|
||||
<RootSelector />
|
||||
<div className={styles.Spacer} />
|
||||
<ToggleCommitFilterModalButton />
|
||||
<div className={styles.VRule} />
|
||||
{didRecordCommits && <SnapshotSelector />}
|
||||
</div>
|
||||
<div className={styles.Content}>
|
||||
{view}
|
||||
<CommitFilterModal />
|
||||
<ModalDialog />
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.RightColumn}>{sidebar}</div>
|
||||
</div>
|
||||
<div className={styles.RightColumn}>
|
||||
<Suspense fallback={<ContentFallback />}>{sidebar}</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
</CommitFilterModalContextController>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -220,18 +143,6 @@ const NoProfilingData = () => (
|
||||
</div>
|
||||
);
|
||||
|
||||
const NoProfilingDataForRoot = () => (
|
||||
<div className={styles.Column}>
|
||||
<div className={styles.Header}>
|
||||
No profiling data has been recorded for the selected root.
|
||||
</div>
|
||||
<div className={styles.Row}>
|
||||
Select a different root in the elements panel, or click the record button{' '}
|
||||
<RecordToggle /> to start recording.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const ProfilingNotSupported = () => (
|
||||
<div className={styles.Column}>
|
||||
<div className={styles.Header}>Profiling not supported.</div>
|
||||
@@ -256,7 +167,14 @@ const ProfilingNotSupported = () => (
|
||||
</div>
|
||||
);
|
||||
|
||||
const RecortdingInProgress = () => (
|
||||
const ProcessingData = () => (
|
||||
<div className={styles.Column}>
|
||||
<div className={styles.Header}>Processing data...</div>
|
||||
<div className={styles.Row}>This should only take a minute.</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const RecordingInProgress = () => (
|
||||
<div className={styles.Column}>
|
||||
<div className={styles.Header}>Profiling is in progress...</div>
|
||||
<div className={styles.Row}>
|
||||
|
||||
@@ -16,31 +16,33 @@ import {
|
||||
import { StoreContext } from '../context';
|
||||
import Store from '../../store';
|
||||
|
||||
import type { ImportedProfilingData } from './types';
|
||||
import type { ProfilingDataFrontend } from './types';
|
||||
|
||||
export type TabID = 'flame-chart' | 'ranked-chart' | 'interactions';
|
||||
|
||||
type Context = {|
|
||||
export type Context = {|
|
||||
// Which tab is selexted in the Profiler UI?
|
||||
selectedTabID: TabID,
|
||||
selectTab(id: TabID): void,
|
||||
|
||||
// Have we recorded any profiling data?
|
||||
// Are we currently profiling?
|
||||
// This value may be modified by the record button in the Profiler toolbar,
|
||||
// Store subscription based values.
|
||||
// The isProfiling value may be modified by the record button in the Profiler toolbar,
|
||||
// or from the backend itself (after a reload-and-profile action).
|
||||
// It is synced between the backend and frontend via a Store subscription.
|
||||
hasProfilingData: boolean,
|
||||
didRecordCommits: boolean,
|
||||
isProcessingData: boolean,
|
||||
isProfiling: boolean,
|
||||
profilingData: ProfilingDataFrontend | null,
|
||||
startProfiling(value: boolean): void,
|
||||
stopProfiling(value: boolean): void,
|
||||
supportsProfiling: boolean,
|
||||
|
||||
// Which renderer and root should profiling data be shown for?
|
||||
// Often this will correspond to the selected renderer and root in the Elements panel.
|
||||
// If nothing is selected though, this will default to the first root.
|
||||
rendererID: number | null,
|
||||
// Which root should profiling data be shown for?
|
||||
// This value should be initialized to either:
|
||||
// 1. The selected root in the Components tree (if it has any profiling data) or
|
||||
// 2. The first root in the list with profiling data.
|
||||
rootID: number | null,
|
||||
rootHasProfilingData: boolean,
|
||||
setRootID: (id: number) => void,
|
||||
|
||||
// Controls whether commits are filtered by duration.
|
||||
// This value is controlled by a filter toggle UI in the Profiler toolbar.
|
||||
@@ -71,9 +73,11 @@ const ProfilerContext = createContext<Context>(((null: any): Context));
|
||||
ProfilerContext.displayName = 'ProfilerContext';
|
||||
|
||||
type StoreProfilingState = {|
|
||||
hasProfilingData: boolean,
|
||||
importedProfilingData: ImportedProfilingData | null,
|
||||
didRecordCommits: boolean,
|
||||
isProcessingData: boolean,
|
||||
isProfiling: boolean,
|
||||
profilingData: ProfilingDataFrontend | null,
|
||||
supportsProfiling: boolean,
|
||||
|};
|
||||
|
||||
type Props = {|
|
||||
@@ -85,49 +89,75 @@ function ProfilerContextController({ children }: Props) {
|
||||
const { selectedElementID } = useContext(TreeStateContext);
|
||||
const dispatch = useContext(TreeDispatcherContext);
|
||||
|
||||
const { profilerStore } = store;
|
||||
|
||||
const subscription = useMemo(
|
||||
() => ({
|
||||
getCurrentValue: () => ({
|
||||
hasProfilingData: store.hasProfilingData,
|
||||
importedProfilingData: store.importedProfilingData,
|
||||
isProfiling: store.isProfiling,
|
||||
didRecordCommits: profilerStore.didRecordCommits,
|
||||
isProcessingData: profilerStore.isProcessingData,
|
||||
isProfiling: profilerStore.isProfiling,
|
||||
profilingData: profilerStore.profilingData,
|
||||
supportsProfiling: store.supportsProfiling,
|
||||
}),
|
||||
subscribe: (callback: Function) => {
|
||||
store.addListener('importedProfilingData', callback);
|
||||
store.addListener('isProfiling', callback);
|
||||
profilerStore.addListener('profilingData', callback);
|
||||
profilerStore.addListener('isProcessingData', callback);
|
||||
profilerStore.addListener('isProfiling', callback);
|
||||
store.addListener('supportsProfiling', callback);
|
||||
return () => {
|
||||
store.removeListener('importedProfilingData', callback);
|
||||
store.removeListener('isProfiling', callback);
|
||||
profilerStore.removeListener('profilingData', callback);
|
||||
profilerStore.removeListener('isProcessingData', callback);
|
||||
profilerStore.removeListener('isProfiling', callback);
|
||||
store.removeListener('supportsProfiling', callback);
|
||||
};
|
||||
},
|
||||
}),
|
||||
[store]
|
||||
[profilerStore, store]
|
||||
);
|
||||
const {
|
||||
didRecordCommits,
|
||||
isProcessingData,
|
||||
isProfiling,
|
||||
hasProfilingData,
|
||||
importedProfilingData,
|
||||
profilingData,
|
||||
supportsProfiling,
|
||||
} = useSubscription<StoreProfilingState, Store>(subscription);
|
||||
|
||||
let rendererID = null;
|
||||
let rootID = null;
|
||||
let rootHasProfilingData = false;
|
||||
if (importedProfilingData !== null) {
|
||||
rootHasProfilingData = true;
|
||||
} else if (selectedElementID !== null) {
|
||||
rendererID = store.getRendererIDForElement(selectedElementID);
|
||||
rootID = store.getRootIDForElement(selectedElementID);
|
||||
rootHasProfilingData =
|
||||
rootID === null ? false : store.profilingOperations.has(rootID);
|
||||
} else if (store.roots.length > 0) {
|
||||
// If no root is selected, assume the first root; many React apps are single root anyway.
|
||||
rootID = store.roots[0];
|
||||
rootHasProfilingData = store.profilingOperations.has(rootID);
|
||||
rendererID = store.getRendererIDForElement(rootID);
|
||||
const [prevProfilingData, setPrevProfilingData] = useState();
|
||||
const [rootID, setRootID] = useState<number | null>(null);
|
||||
|
||||
if (prevProfilingData !== profilingData) {
|
||||
setPrevProfilingData(profilingData);
|
||||
|
||||
const dataForRoots =
|
||||
profilingData !== null ? profilingData.dataForRoots : null;
|
||||
if (dataForRoots != null) {
|
||||
const firstRootID = dataForRoots.keys().next().value || null;
|
||||
|
||||
if (rootID === null || !dataForRoots.has(rootID)) {
|
||||
let selectedElementRootID = null;
|
||||
if (selectedElementID !== null) {
|
||||
selectedElementRootID = store.getRootIDForElement(selectedElementID);
|
||||
}
|
||||
if (
|
||||
selectedElementRootID !== null &&
|
||||
dataForRoots.has(selectedElementRootID)
|
||||
) {
|
||||
setRootID(selectedElementRootID);
|
||||
} else {
|
||||
setRootID(firstRootID);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const startProfiling = useCallback(() => store.startProfiling(), [store]);
|
||||
const stopProfiling = useCallback(() => store.stopProfiling(), [store]);
|
||||
const startProfiling = useCallback(
|
||||
() => store.profilerStore.startProfiling(),
|
||||
[store]
|
||||
);
|
||||
const stopProfiling = useCallback(() => store.profilerStore.stopProfiling(), [
|
||||
store,
|
||||
]);
|
||||
|
||||
const [
|
||||
isCommitFilterEnabled,
|
||||
@@ -152,10 +182,14 @@ function ProfilerContextController({ children }: Props) {
|
||||
(id: number | null, name: string | null) => {
|
||||
selectFiberID(id);
|
||||
selectFiberName(name);
|
||||
|
||||
// Sync selection to the Components tab for convenience.
|
||||
if (id !== null) {
|
||||
// If this element is still in the store, then select it in the Components tab as well.
|
||||
const element = store.getElementByID(id);
|
||||
if (element !== null) {
|
||||
|
||||
// Keep in mind that profiling data may be from a previous session.
|
||||
// In that case, IDs may match up arbitrarily; to be safe, compare both ID and display name.
|
||||
if (element !== null && element.displayName === name) {
|
||||
dispatch({
|
||||
type: 'SELECT_ELEMENT_BY_ID',
|
||||
payload: id,
|
||||
@@ -186,14 +220,16 @@ function ProfilerContextController({ children }: Props) {
|
||||
selectedTabID,
|
||||
selectTab,
|
||||
|
||||
hasProfilingData,
|
||||
didRecordCommits,
|
||||
isProcessingData,
|
||||
isProfiling,
|
||||
profilingData,
|
||||
startProfiling,
|
||||
stopProfiling,
|
||||
supportsProfiling,
|
||||
|
||||
rendererID,
|
||||
rootID,
|
||||
rootHasProfilingData,
|
||||
setRootID,
|
||||
|
||||
isCommitFilterEnabled,
|
||||
setIsCommitFilterEnabled,
|
||||
@@ -214,14 +250,16 @@ function ProfilerContextController({ children }: Props) {
|
||||
selectedTabID,
|
||||
selectTab,
|
||||
|
||||
hasProfilingData,
|
||||
didRecordCommits,
|
||||
isProcessingData,
|
||||
isProfiling,
|
||||
profilingData,
|
||||
startProfiling,
|
||||
stopProfiling,
|
||||
supportsProfiling,
|
||||
|
||||
rendererID,
|
||||
rootID,
|
||||
rootHasProfilingData,
|
||||
setRootID,
|
||||
|
||||
isCommitFilterEnabled,
|
||||
setIsCommitFilterEnabled,
|
||||
|
||||
@@ -5,45 +5,52 @@ import { ProfilerContext } from './ProfilerContext';
|
||||
import { ModalDialogContext } from '../ModalDialog';
|
||||
import Button from '../Button';
|
||||
import ButtonIcon from '../ButtonIcon';
|
||||
import { BridgeContext, StoreContext } from '../context';
|
||||
import { prepareProfilingExport, prepareProfilingImport } from './utils';
|
||||
import { StoreContext } from '../context';
|
||||
import {
|
||||
prepareProfilingDataExport,
|
||||
prepareProfilingDataFrontendFromExport,
|
||||
} from './utils';
|
||||
import { downloadFile } from '../utils';
|
||||
|
||||
import styles from './ProfilingImportExportButtons.css';
|
||||
|
||||
import type { ImportedProfilingData } from './types';
|
||||
import type { ProfilingDataExport } from './types';
|
||||
|
||||
export default function ProfilingImportExportButtons() {
|
||||
const bridge = useContext(BridgeContext);
|
||||
const { isProfiling, rendererID, rootHasProfilingData, rootID } = useContext(
|
||||
ProfilerContext
|
||||
);
|
||||
const { isProfiling, profilingData, rootID } = useContext(ProfilerContext);
|
||||
const store = useContext(StoreContext);
|
||||
const { profilerStore } = store;
|
||||
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const { dispatch: modalDialogDispatch } = useContext(ModalDialogContext);
|
||||
|
||||
const downloadData = useCallback(() => {
|
||||
if (rendererID === null || rootID === null) {
|
||||
if (rootID === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
bridge.send(
|
||||
'exportProfilingSummary',
|
||||
prepareProfilingExport(
|
||||
store.profilingOperations,
|
||||
store.profilingSnapshots,
|
||||
rootID,
|
||||
rendererID
|
||||
)
|
||||
);
|
||||
}, [
|
||||
bridge,
|
||||
rendererID,
|
||||
rootID,
|
||||
store.profilingOperations,
|
||||
store.profilingSnapshots,
|
||||
]);
|
||||
if (profilingData !== null) {
|
||||
const profilingDataExport = prepareProfilingDataExport(profilingData);
|
||||
const date = new Date();
|
||||
const dateString = date
|
||||
.toLocaleDateString(undefined, {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
})
|
||||
.replace(/\//g, '-');
|
||||
const timeString = date
|
||||
.toLocaleTimeString(undefined, {
|
||||
hour12: false,
|
||||
})
|
||||
.replace(/:/g, '-');
|
||||
downloadFile(
|
||||
`profiling-data.${dateString}.${timeString}.json`,
|
||||
JSON.stringify(profilingDataExport, null, 2)
|
||||
);
|
||||
}
|
||||
}, [rootID, profilingData]);
|
||||
|
||||
const uploadData = useCallback(() => {
|
||||
if (inputRef.current !== null) {
|
||||
@@ -58,9 +65,12 @@ export default function ProfilingImportExportButtons() {
|
||||
fileReader.addEventListener('load', () => {
|
||||
try {
|
||||
const raw = ((fileReader.result: any): string);
|
||||
const data = prepareProfilingImport(raw);
|
||||
|
||||
store.importedProfilingData = ((data: any): ImportedProfilingData);
|
||||
const profilingDataExport = ((JSON.parse(
|
||||
raw
|
||||
): any): ProfilingDataExport);
|
||||
profilerStore.profilingData = prepareProfilingDataFrontendFromExport(
|
||||
profilingDataExport
|
||||
);
|
||||
} catch (error) {
|
||||
modalDialogDispatch({
|
||||
type: 'SHOW',
|
||||
@@ -76,9 +86,10 @@ export default function ProfilingImportExportButtons() {
|
||||
});
|
||||
}
|
||||
});
|
||||
// TODO (profiling) Handle fileReader errors.
|
||||
fileReader.readAsText(input.files[0]);
|
||||
}
|
||||
}, [modalDialogDispatch, store]);
|
||||
}, [modalDialogDispatch, profilerStore]);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
@@ -97,15 +108,13 @@ export default function ProfilingImportExportButtons() {
|
||||
>
|
||||
<ButtonIcon type="import" />
|
||||
</Button>
|
||||
{store.supportsFileDownloads && (
|
||||
<Button
|
||||
disabled={isProfiling || !rootHasProfilingData}
|
||||
onClick={downloadData}
|
||||
title="Save profile..."
|
||||
>
|
||||
<ButtonIcon type="export" />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
disabled={isProfiling || !profilerStore.didRecordCommits}
|
||||
onClick={downloadData}
|
||||
title="Save profile..."
|
||||
>
|
||||
<ButtonIcon type="export" />
|
||||
</Button>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// @flow
|
||||
|
||||
import { calculateSelfDuration, formatDuration } from './utils';
|
||||
import { ElementTypeForwardRef, ElementTypeMemo } from 'src/types';
|
||||
import { formatDuration } from './utils';
|
||||
import ProfilerStore from 'src/devtools/ProfilerStore';
|
||||
|
||||
import type { CommitDetailsFrontend, CommitTreeFrontend } from './types';
|
||||
import type { CommitTree } from './types';
|
||||
|
||||
export type ChartNode = {|
|
||||
id: number,
|
||||
@@ -19,15 +21,19 @@ export type ChartData = {|
|
||||
const cachedChartData: Map<string, ChartData> = new Map();
|
||||
|
||||
export function getChartData({
|
||||
commitDetails,
|
||||
commitIndex,
|
||||
commitTree,
|
||||
profilerStore,
|
||||
rootID,
|
||||
}: {|
|
||||
commitDetails: CommitDetailsFrontend,
|
||||
commitIndex: number,
|
||||
commitTree: CommitTreeFrontend,
|
||||
commitTree: CommitTree,
|
||||
profilerStore: ProfilerStore,
|
||||
rootID: number,
|
||||
|}): ChartData {
|
||||
const { actualDurations, rootID } = commitDetails;
|
||||
const commitDatum = profilerStore.getCommitData(rootID, commitIndex);
|
||||
|
||||
const { fiberActualDurations, fiberSelfDurations } = commitDatum;
|
||||
const { nodes } = commitTree;
|
||||
|
||||
const key = `${rootID}-${commitIndex}`;
|
||||
@@ -38,23 +44,35 @@ export function getChartData({
|
||||
let maxSelfDuration = 0;
|
||||
|
||||
const chartNodes: Array<ChartNode> = [];
|
||||
actualDurations.forEach((actualDuration, id) => {
|
||||
fiberActualDurations.forEach((actualDuration, id) => {
|
||||
const node = nodes.get(id);
|
||||
|
||||
if (node == null) {
|
||||
throw Error(`Could not find node with id "${id}" in commit tree`);
|
||||
}
|
||||
|
||||
const { displayName, key, parentID, type } = node;
|
||||
|
||||
// Don't show the root node in this chart.
|
||||
if (node.parentID === 0) {
|
||||
if (parentID === 0) {
|
||||
return;
|
||||
}
|
||||
const selfDuration = calculateSelfDuration(id, commitTree, commitDetails);
|
||||
const selfDuration = fiberSelfDurations.get(id) || 0;
|
||||
maxSelfDuration = Math.max(maxSelfDuration, selfDuration);
|
||||
|
||||
const name = node.displayName || 'Unknown';
|
||||
const maybeKey = node.key !== null ? ` key="${node.key}"` : '';
|
||||
const label = `${name}${maybeKey} (${formatDuration(selfDuration)}ms)`;
|
||||
const name = displayName || 'Anonymous';
|
||||
const maybeKey = key !== null ? ` key="${key}"` : '';
|
||||
|
||||
let maybeBadge = '';
|
||||
if (type === ElementTypeForwardRef) {
|
||||
maybeBadge = ' (ForwardRef)';
|
||||
} else if (type === ElementTypeMemo) {
|
||||
maybeBadge = ' (Memo)';
|
||||
}
|
||||
|
||||
const label = `${name}${maybeBadge}${maybeKey} (${formatDuration(
|
||||
selfDuration
|
||||
)}ms)`;
|
||||
chartNodes.push({
|
||||
id,
|
||||
label,
|
||||
|
||||
@@ -23,3 +23,16 @@
|
||||
.InactiveRecordToggle:active {
|
||||
color: var(--color-record-hover);
|
||||
}
|
||||
|
||||
.DisabledRecordToggle {
|
||||
color: var(--color-button-disabled);
|
||||
}
|
||||
.DisabledRecordToggle:hover {
|
||||
color: var(--color-button-disabled);
|
||||
}
|
||||
.DisabledRecordToggle:focus {
|
||||
color: var(--color-button-disabled);
|
||||
}
|
||||
.DisabledRecordToggle:active {
|
||||
color: var(--color-button-disabled);
|
||||
}
|
||||
|
||||
@@ -16,11 +16,16 @@ export default function RecordToggle({ disabled }: Props) {
|
||||
ProfilerContext
|
||||
);
|
||||
|
||||
let className = styles.InactiveRecordToggle;
|
||||
if (disabled) {
|
||||
className = styles.DisabledRecordToggle;
|
||||
} else if (isProfiling) {
|
||||
className = styles.ActiveRecordToggle;
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={
|
||||
isProfiling ? styles.ActiveRecordToggle : styles.InactiveRecordToggle
|
||||
}
|
||||
className={className}
|
||||
disabled={disabled}
|
||||
onClick={isProfiling ? stopProfiling : startProfiling}
|
||||
title={isProfiling ? 'Stop profiling' : 'Start profiling'}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.Spacer {
|
||||
flex: 1;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// @flow
|
||||
|
||||
import React, { Fragment, useCallback, useContext } from 'react';
|
||||
import { ProfilerContext } from './ProfilerContext';
|
||||
|
||||
import styles from './RootSelector.css';
|
||||
|
||||
export default function RootSelector(_: {||}) {
|
||||
const { profilingData, rootID, setRootID } = useContext(ProfilerContext);
|
||||
|
||||
const options = [];
|
||||
if (profilingData !== null) {
|
||||
profilingData.dataForRoots.forEach((dataForRoot, rootID) => {
|
||||
options.push(
|
||||
<option key={rootID} value={rootID}>
|
||||
{dataForRoot.displayName}
|
||||
</option>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const handleChange = useCallback(
|
||||
({ currentTarget }) => {
|
||||
setRootID(parseInt(currentTarget.value, 10));
|
||||
},
|
||||
[setRootID]
|
||||
);
|
||||
|
||||
if (profilingData === null || profilingData.dataForRoots.size <= 1) {
|
||||
// Don't take up visual space if there's only one root.
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className={styles.Spacer} />
|
||||
<select value={rootID} onChange={handleChange}>
|
||||
{options}
|
||||
</select>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
@@ -12,24 +12,12 @@ export type Props = {||};
|
||||
export default function SidebarCommitInfo(_: Props) {
|
||||
const {
|
||||
selectedCommitIndex,
|
||||
rendererID,
|
||||
rootID,
|
||||
selectInteraction,
|
||||
selectTab,
|
||||
} = useContext(ProfilerContext);
|
||||
|
||||
const {
|
||||
captureScreenshots,
|
||||
profilingCache,
|
||||
profilingScreenshots,
|
||||
} = useContext(StoreContext);
|
||||
|
||||
const screenshotsByCommitIndex =
|
||||
rootID !== null ? profilingScreenshots.get(rootID) : null;
|
||||
const screenshot =
|
||||
screenshotsByCommitIndex != null && selectedCommitIndex !== null
|
||||
? screenshotsByCommitIndex.get(selectedCommitIndex)
|
||||
: null;
|
||||
const { captureScreenshots, profilerStore } = useContext(StoreContext);
|
||||
|
||||
const [
|
||||
isScreenshotModalVisible,
|
||||
@@ -45,26 +33,22 @@ export default function SidebarCommitInfo(_: Props) {
|
||||
[]
|
||||
);
|
||||
|
||||
if (selectedCommitIndex === null) {
|
||||
if (rootID === null || selectedCommitIndex === null) {
|
||||
return <div className={styles.NothingSelected}>Nothing selected</div>;
|
||||
}
|
||||
|
||||
const { commitDurations, commitTimes } = profilingCache.ProfilingSummary.read(
|
||||
{
|
||||
rendererID: ((rendererID: any): number),
|
||||
rootID: ((rootID: any): number),
|
||||
}
|
||||
);
|
||||
const { interactions } = profilerStore.getDataForRoot(rootID);
|
||||
const {
|
||||
duration,
|
||||
interactionIDs,
|
||||
priorityLevel,
|
||||
screenshot,
|
||||
timestamp,
|
||||
} = profilerStore.getCommitData(rootID, selectedCommitIndex);
|
||||
|
||||
const { interactions } = profilingCache.CommitDetails.read({
|
||||
commitIndex: selectedCommitIndex,
|
||||
rendererID: ((rendererID: any): number),
|
||||
rootID: ((rootID: any): number),
|
||||
});
|
||||
|
||||
const viewInteraction = interaction => {
|
||||
const viewInteraction = interactionID => {
|
||||
selectTab('interactions');
|
||||
selectInteraction(interaction.id);
|
||||
selectInteraction(interactionID);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -72,36 +56,41 @@ export default function SidebarCommitInfo(_: Props) {
|
||||
<div className={styles.Toolbar}>Commit information</div>
|
||||
<div className={styles.Content}>
|
||||
<ul className={styles.List}>
|
||||
{priorityLevel !== null && (
|
||||
<li className={styles.ListItem}>
|
||||
<label className={styles.Label}>Priority</label>:{' '}
|
||||
<span className={styles.Value}>{priorityLevel}</span>
|
||||
</li>
|
||||
)}
|
||||
<li className={styles.ListItem}>
|
||||
<label className={styles.Label}>Committed at</label>:{' '}
|
||||
<span className={styles.Value}>
|
||||
{formatTime(commitTimes[((selectedCommitIndex: any): number)])}s
|
||||
</span>
|
||||
<span className={styles.Value}>{formatTime(timestamp)}s</span>
|
||||
</li>
|
||||
<li className={styles.ListItem}>
|
||||
<label className={styles.Label}>Render duration</label>:{' '}
|
||||
<span className={styles.Value}>
|
||||
{formatDuration(
|
||||
commitDurations[((selectedCommitIndex: any): number)]
|
||||
)}
|
||||
ms
|
||||
</span>
|
||||
<span className={styles.Value}>{formatDuration(duration)}ms</span>
|
||||
</li>
|
||||
<li className={styles.Interactions}>
|
||||
<label className={styles.Label}>Interactions</label>:
|
||||
<div className={styles.InteractionList}>
|
||||
{interactions.length === 0 ? (
|
||||
{interactionIDs.length === 0 ? (
|
||||
<div className={styles.NoInteractions}>None</div>
|
||||
) : null}
|
||||
{interactions.map((interaction, index) => (
|
||||
<button
|
||||
key={index}
|
||||
className={styles.Interaction}
|
||||
onClick={() => viewInteraction(interaction)}
|
||||
>
|
||||
{interaction.name}
|
||||
</button>
|
||||
))}
|
||||
{interactionIDs.map(interactionID => {
|
||||
const interaction = interactions.get(interactionID);
|
||||
if (interaction == null) {
|
||||
throw Error(`Invalid interaction "${interactionID}"`);
|
||||
}
|
||||
return (
|
||||
<button
|
||||
key={interactionID}
|
||||
className={styles.Interaction}
|
||||
onClick={() => viewInteraction(interactionID)}
|
||||
>
|
||||
{interaction.name}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</li>
|
||||
{captureScreenshots && (
|
||||
|
||||
@@ -13,48 +13,70 @@ export type Props = {||};
|
||||
export default function SidebarInteractions(_: Props) {
|
||||
const {
|
||||
selectedInteractionID,
|
||||
rendererID,
|
||||
rootID,
|
||||
selectCommitIndex,
|
||||
selectTab,
|
||||
} = useContext(ProfilerContext);
|
||||
|
||||
const { profilingCache } = useContext(StoreContext);
|
||||
const { profilerStore } = useContext(StoreContext);
|
||||
const { profilingCache } = profilerStore;
|
||||
|
||||
if (selectedInteractionID === null) {
|
||||
return <div className={styles.NothingSelected}>Nothing selected</div>;
|
||||
}
|
||||
|
||||
const interactions = profilingCache.Interactions.read({
|
||||
rendererID: ((rendererID: any): number),
|
||||
rootID: ((rootID: any): number),
|
||||
});
|
||||
const interaction = interactions.find(
|
||||
interaction => interaction.id === selectedInteractionID
|
||||
const { interactionCommits, interactions } = profilerStore.getDataForRoot(
|
||||
((rootID: any): number)
|
||||
);
|
||||
const interaction = interactions.get(selectedInteractionID);
|
||||
if (interaction == null) {
|
||||
throw Error(
|
||||
`Could not find interaction by selected interaction id "${selectedInteractionID}"`
|
||||
);
|
||||
}
|
||||
|
||||
const profilingSummary = profilingCache.ProfilingSummary.read({
|
||||
rendererID: ((rendererID: any): number),
|
||||
const { maxCommitDuration } = profilingCache.getInteractionsChartData({
|
||||
rootID: ((rootID: any): number),
|
||||
});
|
||||
|
||||
const { maxCommitDuration } = profilingCache.getInteractionsChartData({
|
||||
interactions,
|
||||
profilingSummary,
|
||||
});
|
||||
|
||||
const { commitDurations, commitTimes } = profilingSummary;
|
||||
|
||||
const viewCommit = (commitIndex: number) => {
|
||||
selectTab('flame-chart');
|
||||
selectCommitIndex(commitIndex);
|
||||
};
|
||||
|
||||
const listItems: Array<React$Node> = [];
|
||||
const commitIndices = interactionCommits.get(selectedInteractionID);
|
||||
if (commitIndices != null) {
|
||||
commitIndices.forEach(commitIndex => {
|
||||
const { duration, timestamp } = profilerStore.getCommitData(
|
||||
((rootID: any): number),
|
||||
commitIndex
|
||||
);
|
||||
|
||||
listItems.push(
|
||||
<li
|
||||
key={commitIndex}
|
||||
className={styles.ListItem}
|
||||
onClick={() => viewCommit(commitIndex)}
|
||||
>
|
||||
<div
|
||||
className={styles.CommitBox}
|
||||
style={{
|
||||
backgroundColor: getGradientColor(
|
||||
Math.min(1, Math.max(0, duration / maxCommitDuration)) || 0
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
timestamp: {formatTime(timestamp)}s
|
||||
<br />
|
||||
duration: {formatDuration(duration)}ms
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className={styles.Toolbar}>
|
||||
@@ -62,35 +84,7 @@ export default function SidebarInteractions(_: Props) {
|
||||
</div>
|
||||
<div className={styles.Content}>
|
||||
<div className={styles.Commits}>Commits:</div>
|
||||
<ul className={styles.List}>
|
||||
{interaction.commits.map(commitIndex => (
|
||||
<li
|
||||
key={commitIndex}
|
||||
className={styles.ListItem}
|
||||
onClick={() => viewCommit(commitIndex)}
|
||||
>
|
||||
<div
|
||||
className={styles.CommitBox}
|
||||
style={{
|
||||
backgroundColor: getGradientColor(
|
||||
Math.min(
|
||||
1,
|
||||
Math.max(
|
||||
0,
|
||||
commitDurations[commitIndex] / maxCommitDuration
|
||||
)
|
||||
) || 0
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
timestamp: {formatTime(commitTimes[commitIndex])}s
|
||||
<br />
|
||||
duration: {formatDuration(commitDurations[commitIndex])}ms
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<ul className={styles.List}>{listItems}</ul>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
@@ -12,9 +12,8 @@ import styles from './SidebarSelectedFiberInfo.css';
|
||||
export type Props = {||};
|
||||
|
||||
export default function SidebarSelectedFiberInfo(_: Props) {
|
||||
const { profilingCache } = useContext(StoreContext);
|
||||
const { profilerStore } = useContext(StoreContext);
|
||||
const {
|
||||
rendererID,
|
||||
rootID,
|
||||
selectCommitIndex,
|
||||
selectedCommitIndex,
|
||||
@@ -22,23 +21,21 @@ export default function SidebarSelectedFiberInfo(_: Props) {
|
||||
selectedFiberName,
|
||||
selectFiber,
|
||||
} = useContext(ProfilerContext);
|
||||
const { profilingCache } = profilerStore;
|
||||
|
||||
const { commitTimes } = profilingCache.ProfilingSummary.read({
|
||||
rendererID: ((rendererID: any): number),
|
||||
rootID: ((rootID: any): number),
|
||||
});
|
||||
|
||||
const { commitDurations } = profilingCache.FiberCommits.read({
|
||||
const commitIndices = profilingCache.getFiberCommits({
|
||||
fiberID: ((selectedFiberID: any): number),
|
||||
rendererID: ((rendererID: any): number),
|
||||
rootID: ((rootID: any): number),
|
||||
});
|
||||
|
||||
const listItems = [];
|
||||
for (let i = 0; i < commitDurations.length; i += 2) {
|
||||
const commitIndex = commitDurations[i];
|
||||
const duration = commitDurations[i + 1];
|
||||
const time = commitTimes[commitIndex];
|
||||
for (let i = 0; i < commitIndices.length; i += 2) {
|
||||
const commitIndex = commitIndices[i];
|
||||
|
||||
const { duration, timestamp } = profilerStore.getCommitData(
|
||||
((rootID: any): number),
|
||||
commitIndex
|
||||
);
|
||||
|
||||
listItems.push(
|
||||
<button
|
||||
@@ -50,7 +47,7 @@ export default function SidebarSelectedFiberInfo(_: Props) {
|
||||
}
|
||||
onClick={() => selectCommitIndex(commitIndex)}
|
||||
>
|
||||
{formatTime(time)}s for {formatDuration(duration)}ms
|
||||
{formatTime(timestamp)}s for {formatDuration(duration)}ms
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,5 +11,6 @@
|
||||
.Inner {
|
||||
width: 100%;
|
||||
min-height: 5px;
|
||||
background-color: var(--color-commit-did-not-render);
|
||||
background-color: var(--color-commit-did-not-render-fill);
|
||||
color: var(--color-commit-did-not-render-fill-text);
|
||||
}
|
||||
|
||||
@@ -16,29 +16,33 @@ export default function SnapshotSelector(_: Props) {
|
||||
const {
|
||||
isCommitFilterEnabled,
|
||||
minCommitDuration,
|
||||
rendererID,
|
||||
rootID,
|
||||
selectedCommitIndex,
|
||||
selectCommitIndex,
|
||||
} = useContext(ProfilerContext);
|
||||
|
||||
const { profilingCache } = useContext(StoreContext);
|
||||
const { commitDurations, commitTimes } = profilingCache.ProfilingSummary.read(
|
||||
{
|
||||
rendererID: ((rendererID: any): number),
|
||||
rootID: ((rootID: any): number),
|
||||
}
|
||||
);
|
||||
const { profilerStore } = useContext(StoreContext);
|
||||
const { commitData } = profilerStore.getDataForRoot(((rootID: any): number));
|
||||
|
||||
const commitDurations: Array<number> = [];
|
||||
const commitTimes: Array<number> = [];
|
||||
commitData.forEach(commitDatum => {
|
||||
commitDurations.push(commitDatum.duration);
|
||||
commitTimes.push(commitDatum.timestamp);
|
||||
});
|
||||
|
||||
const filteredCommitIndices = useMemo(
|
||||
() =>
|
||||
commitDurations.reduce((reduced, commitDuration, index) => {
|
||||
if (!isCommitFilterEnabled || commitDuration >= minCommitDuration) {
|
||||
commitData.reduce((reduced, commitDatum, index) => {
|
||||
if (
|
||||
!isCommitFilterEnabled ||
|
||||
commitDatum.duration >= minCommitDuration
|
||||
) {
|
||||
reduced.push(index);
|
||||
}
|
||||
return reduced;
|
||||
}, []),
|
||||
[commitDurations, isCommitFilterEnabled, minCommitDuration]
|
||||
[commitData, isCommitFilterEnabled, minCommitDuration]
|
||||
);
|
||||
|
||||
const numFilteredCommits = filteredCommitIndices.length;
|
||||
@@ -112,7 +116,7 @@ export default function SnapshotSelector(_: Props) {
|
||||
[viewNextCommit, viewPrevCommit]
|
||||
);
|
||||
|
||||
if (commitDurations.length === 0) {
|
||||
if (commitData.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,72 +1,124 @@
|
||||
// @flow
|
||||
|
||||
export type CommitTreeNodeFrontend = {|
|
||||
import type { ElementType } from 'src/types';
|
||||
|
||||
export type CommitTreeNode = {|
|
||||
id: number,
|
||||
children: Array<number>,
|
||||
displayName: string | null,
|
||||
key: number | string | null,
|
||||
parentID: number,
|
||||
treeBaseDuration: number,
|
||||
type: ElementType,
|
||||
|};
|
||||
|
||||
export type CommitTreeFrontend = {|
|
||||
nodes: Map<number, CommitTreeNodeFrontend>,
|
||||
export type CommitTree = {|
|
||||
nodes: Map<number, CommitTreeNode>,
|
||||
rootID: number,
|
||||
|};
|
||||
|
||||
export type InteractionFrontend = {|
|
||||
export type Interaction = {|
|
||||
id: number,
|
||||
name: string,
|
||||
timestamp: number,
|
||||
|};
|
||||
|
||||
export type InteractionWithCommitsFrontend = {|
|
||||
...InteractionFrontend,
|
||||
commits: Array<number>,
|
||||
|};
|
||||
|
||||
export type InteractionsFrontend = Array<InteractionWithCommitsFrontend>;
|
||||
|
||||
export type CommitDetailsFrontend = {|
|
||||
rootID: number,
|
||||
commitIndex: number,
|
||||
actualDurations: Map<number, number>,
|
||||
interactions: Array<InteractionFrontend>,
|
||||
|};
|
||||
|
||||
export type FiberCommitsFrontend = {|
|
||||
commitDurations: Array<number>,
|
||||
fiberID: number,
|
||||
rootID: number,
|
||||
|};
|
||||
|
||||
export type ProfilingSummaryFrontend = {|
|
||||
rootID: number,
|
||||
|
||||
// Commit durations
|
||||
commitDurations: Array<number>,
|
||||
|
||||
// Commit times (relative to when profiling started)
|
||||
commitTimes: Array<number>,
|
||||
|
||||
// Map of fiber id to (initial) tree base duration
|
||||
initialTreeBaseDurations: Map<number, number>,
|
||||
|
||||
interactionCount: number,
|
||||
|};
|
||||
|
||||
export type ProfilingSnapshotNode = {|
|
||||
export type SnapshotNode = {|
|
||||
id: number,
|
||||
children: Array<number>,
|
||||
displayName: string | null,
|
||||
key: number | string | null,
|
||||
type: ElementType,
|
||||
|};
|
||||
|
||||
export type ImportedProfilingData = {|
|
||||
version: number,
|
||||
profilingOperations: Map<number, Array<Uint32Array>>,
|
||||
profilingSnapshots: Map<number, Map<number, ProfilingSnapshotNode>>,
|
||||
commitDetails: CommitDetailsFrontend,
|
||||
interactions: InteractionsFrontend,
|
||||
profilingSummary: ProfilingSummaryFrontend,
|
||||
export type CommitDataFrontend = {|
|
||||
// How long was this commit?
|
||||
duration: number,
|
||||
|
||||
// Map of Fiber (ID) to actual duration for this commit;
|
||||
// Fibers that did not render will not have entries in this Map.
|
||||
fiberActualDurations: Map<number, number>,
|
||||
|
||||
// Map of Fiber (ID) to "self duration" for this commit;
|
||||
// Fibers that did not render will not have entries in this Map.
|
||||
fiberSelfDurations: Map<number, number>,
|
||||
|
||||
// Which interactions (IDs) were associated with this commit.
|
||||
interactionIDs: Array<number>,
|
||||
|
||||
// Priority level of the commit (if React provided this info)
|
||||
priorityLevel: string | null,
|
||||
|
||||
// Screenshot data for this commit (if available).
|
||||
screenshot: string | null,
|
||||
|
||||
// When did this commit occur (relative to the start of profiling)
|
||||
timestamp: number,
|
||||
|};
|
||||
|
||||
export type ProfilingDataForRootFrontend = {|
|
||||
// Timing, duration, and other metadata about each commit.
|
||||
commitData: Array<CommitDataFrontend>,
|
||||
|
||||
// Display name of the nearest descendant component (ideally a function or class component).
|
||||
// This value is used by the root selector UI.
|
||||
displayName: string,
|
||||
|
||||
// Map of fiber id to (initial) tree base duration when Profiling session was started.
|
||||
// This info can be used along with commitOperations to reconstruct the tree for any commit.
|
||||
initialTreeBaseDurations: Map<number, number>,
|
||||
|
||||
// All interactions recorded (for this root) during the current session.
|
||||
interactionCommits: Map<number, Array<number>>,
|
||||
|
||||
// All interactions recorded (for this root) during the current session.
|
||||
interactions: Map<number, Interaction>,
|
||||
|
||||
// List of tree mutation that occur during profiling.
|
||||
// These mutations can be used along with initial snapshots to reconstruct the tree for any commit.
|
||||
operations: Array<Uint32Array>,
|
||||
|
||||
// Identifies the root this profiler data corresponds to.
|
||||
rootID: number,
|
||||
|
||||
// Map of fiber id to node when the Profiling session was started.
|
||||
// This info can be used along with commitOperations to reconstruct the tree for any commit.
|
||||
snapshots: Map<number, SnapshotNode>,
|
||||
|};
|
||||
|
||||
// Combination of profiling data collected by the renderer interface (backend) and Store (frontend).
|
||||
export type ProfilingDataFrontend = {|
|
||||
// Profiling data per root.
|
||||
dataForRoots: Map<number, ProfilingDataForRootFrontend>,
|
||||
|};
|
||||
|
||||
export type CommitDataExport = {|
|
||||
duration: number,
|
||||
// Tuple of fiber ID and actual duration
|
||||
fiberActualDurations: Array<[number, number]>,
|
||||
// Tuple of fiber ID and computed "self" duration
|
||||
fiberSelfDurations: Array<[number, number]>,
|
||||
interactionIDs: Array<number>,
|
||||
priorityLevel: string | null,
|
||||
screenshot: string | null,
|
||||
timestamp: number,
|
||||
|};
|
||||
|
||||
export type ProfilingDataForRootExport = {|
|
||||
commitData: Array<CommitDataExport>,
|
||||
displayName: string,
|
||||
// Tuple of Fiber ID and base duration
|
||||
initialTreeBaseDurations: Array<[number, number]>,
|
||||
// Tuple of Interaction ID and commit indices
|
||||
interactionCommits: Array<[number, Array<number>]>,
|
||||
interactions: Array<[number, Interaction]>,
|
||||
operations: Array<Array<number>>,
|
||||
rootID: number,
|
||||
snapshots: Array<[number, SnapshotNode]>,
|
||||
|};
|
||||
|
||||
// Serializable vefrsion of ProfilingDataFrontend data.
|
||||
export type ProfilingDataExport = {|
|
||||
version: 4,
|
||||
dataForRoots: Array<ProfilingDataForRootExport>,
|
||||
|};
|
||||
|
||||
@@ -2,10 +2,13 @@
|
||||
|
||||
import { PROFILER_EXPORT_VERSION } from 'src/constants';
|
||||
|
||||
import type { ProfilingDataBackend } from 'src/backend/types';
|
||||
import type {
|
||||
CommitDetailsFrontend,
|
||||
CommitTreeFrontend,
|
||||
ProfilingSnapshotNode,
|
||||
ProfilingDataExport,
|
||||
ProfilingDataForRootExport,
|
||||
ProfilingDataForRootFrontend,
|
||||
ProfilingDataFrontend,
|
||||
SnapshotNode,
|
||||
} from './types';
|
||||
|
||||
const commitGradient = [
|
||||
@@ -21,83 +24,176 @@ const commitGradient = [
|
||||
'var(--color-commit-gradient-9)',
|
||||
];
|
||||
|
||||
export const calculateSelfDuration = (
|
||||
id: number,
|
||||
commitTree: CommitTreeFrontend,
|
||||
commitDetails: CommitDetailsFrontend
|
||||
): number => {
|
||||
const { actualDurations } = commitDetails;
|
||||
const { nodes } = commitTree;
|
||||
// Combines info from the Store (frontend) and renderer interfaces (backend) into the format required by the Profiler UI.
|
||||
// This format can then be quickly exported (and re-imported).
|
||||
export function prepareProfilingDataFrontendFromBackendAndStore(
|
||||
dataBackends: Array<ProfilingDataBackend>,
|
||||
operationsByRootID: Map<number, Array<Uint32Array>>,
|
||||
screenshotsByRootID: Map<number, Map<number, string>>,
|
||||
snapshotsByRootID: Map<number, Map<number, SnapshotNode>>
|
||||
): ProfilingDataFrontend {
|
||||
const dataForRoots: Map<number, ProfilingDataForRootFrontend> = new Map();
|
||||
|
||||
if (!actualDurations.has(id)) {
|
||||
return 0;
|
||||
}
|
||||
dataBackends.forEach(dataBackend => {
|
||||
dataBackend.dataForRoots.forEach(
|
||||
({
|
||||
commitData,
|
||||
displayName,
|
||||
initialTreeBaseDurations,
|
||||
interactionCommits,
|
||||
interactions,
|
||||
rootID,
|
||||
}) => {
|
||||
const screenshots = screenshotsByRootID.get(rootID) || null;
|
||||
|
||||
const node = nodes.get(id);
|
||||
if (node == null) {
|
||||
throw Error(`Could not find node with id "${id}" in commit tree`);
|
||||
}
|
||||
const operations = operationsByRootID.get(rootID);
|
||||
if (operations == null) {
|
||||
throw Error(`Could not find profiling operations for root ${rootID}`);
|
||||
}
|
||||
|
||||
let selfDuration = actualDurations.get(id) || 0;
|
||||
const snapshots = snapshotsByRootID.get(rootID);
|
||||
if (snapshots == null) {
|
||||
throw Error(`Could not find profiling snapshots for root ${rootID}`);
|
||||
}
|
||||
|
||||
node.children.forEach(childID => {
|
||||
if (actualDurations.has(childID)) {
|
||||
selfDuration -= actualDurations.get(childID) || 0;
|
||||
}
|
||||
dataForRoots.set(rootID, {
|
||||
commitData: commitData.map((commitDataBackend, commitIndex) => ({
|
||||
duration: commitDataBackend.duration,
|
||||
fiberActualDurations: new Map(
|
||||
commitDataBackend.fiberActualDurations
|
||||
),
|
||||
fiberSelfDurations: new Map(commitDataBackend.fiberSelfDurations),
|
||||
interactionIDs: commitDataBackend.interactionIDs,
|
||||
priorityLevel: commitDataBackend.priorityLevel,
|
||||
screenshot:
|
||||
(screenshots !== null && screenshots.get(commitIndex)) || null,
|
||||
timestamp: commitDataBackend.timestamp,
|
||||
})),
|
||||
displayName,
|
||||
initialTreeBaseDurations: new Map(initialTreeBaseDurations),
|
||||
interactionCommits: new Map(interactionCommits),
|
||||
interactions: new Map(interactions),
|
||||
operations,
|
||||
rootID,
|
||||
snapshots,
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
return selfDuration;
|
||||
};
|
||||
return { dataForRoots };
|
||||
}
|
||||
|
||||
export const prepareProfilingExport = (
|
||||
profilingOperations: Map<number, Array<Uint32Array>>,
|
||||
profilingSnapshots: Map<number, Map<number, ProfilingSnapshotNode>>,
|
||||
rendererID: number,
|
||||
rootID: number
|
||||
) => {
|
||||
const profilingOperationsForRoot = [];
|
||||
const operations = profilingOperations.get(rootID);
|
||||
if (operations != null) {
|
||||
operations.forEach(operations => {
|
||||
// Convert typed Array before JSON serialization, or it will be converted to an Object.
|
||||
profilingOperationsForRoot.push(Array.from<number>(operations));
|
||||
});
|
||||
// Converts a Profiling data export into the format required by the Store.
|
||||
export function prepareProfilingDataFrontendFromExport(
|
||||
profilingDataExport: ProfilingDataExport
|
||||
): ProfilingDataFrontend {
|
||||
const { version } = profilingDataExport;
|
||||
|
||||
if (version !== PROFILER_EXPORT_VERSION) {
|
||||
throw Error(`Unsupported profiler export version "${version}"`);
|
||||
}
|
||||
|
||||
// Convert Map to Object or JSON.stringify will clobber the contents.
|
||||
const profilingSnapshotsForRoot = {};
|
||||
const profilingSnapshotsMap = profilingSnapshots.get(rootID);
|
||||
if (profilingSnapshotsMap != null) {
|
||||
for (let [id, snapshot] of profilingSnapshotsMap.entries()) {
|
||||
profilingSnapshotsForRoot[id] = snapshot;
|
||||
const dataForRoots: Map<number, ProfilingDataForRootFrontend> = new Map();
|
||||
profilingDataExport.dataForRoots.forEach(
|
||||
({
|
||||
commitData,
|
||||
displayName,
|
||||
initialTreeBaseDurations,
|
||||
interactionCommits,
|
||||
interactions,
|
||||
operations,
|
||||
rootID,
|
||||
snapshots,
|
||||
}) => {
|
||||
dataForRoots.set(rootID, {
|
||||
commitData: commitData.map(
|
||||
({
|
||||
duration,
|
||||
fiberActualDurations,
|
||||
fiberSelfDurations,
|
||||
interactionIDs,
|
||||
priorityLevel,
|
||||
screenshot,
|
||||
timestamp,
|
||||
}) => ({
|
||||
duration,
|
||||
fiberActualDurations: new Map(fiberActualDurations),
|
||||
fiberSelfDurations: new Map(fiberSelfDurations),
|
||||
interactionIDs,
|
||||
priorityLevel,
|
||||
screenshot,
|
||||
timestamp,
|
||||
})
|
||||
),
|
||||
displayName,
|
||||
initialTreeBaseDurations: new Map(initialTreeBaseDurations),
|
||||
interactionCommits: new Map(interactionCommits),
|
||||
interactions: new Map(interactions),
|
||||
operations: operations.map(array => Uint32Array.from(array)), // Convert Array back to Uint32Array
|
||||
rootID,
|
||||
snapshots: new Map(snapshots),
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return { dataForRoots };
|
||||
}
|
||||
|
||||
// Converts a Store Profiling data into a format that can be safely (JSON) serialized for export.
|
||||
export function prepareProfilingDataExport(
|
||||
profilingDataFrontend: ProfilingDataFrontend
|
||||
): ProfilingDataExport {
|
||||
const dataForRoots: Array<ProfilingDataForRootExport> = [];
|
||||
profilingDataFrontend.dataForRoots.forEach(
|
||||
({
|
||||
commitData,
|
||||
displayName,
|
||||
initialTreeBaseDurations,
|
||||
interactionCommits,
|
||||
interactions,
|
||||
operations,
|
||||
rootID,
|
||||
snapshots,
|
||||
}) => {
|
||||
dataForRoots.push({
|
||||
commitData: commitData.map(
|
||||
({
|
||||
duration,
|
||||
fiberActualDurations,
|
||||
fiberSelfDurations,
|
||||
interactionIDs,
|
||||
priorityLevel,
|
||||
screenshot,
|
||||
timestamp,
|
||||
}) => ({
|
||||
duration,
|
||||
fiberActualDurations: Array.from(fiberActualDurations.entries()),
|
||||
fiberSelfDurations: Array.from(fiberSelfDurations.entries()),
|
||||
interactionIDs,
|
||||
priorityLevel,
|
||||
screenshot,
|
||||
timestamp,
|
||||
})
|
||||
),
|
||||
displayName,
|
||||
initialTreeBaseDurations: Array.from(
|
||||
initialTreeBaseDurations.entries()
|
||||
),
|
||||
interactionCommits: Array.from(interactionCommits.entries()),
|
||||
interactions: Array.from(interactions.entries()),
|
||||
operations: operations.map(array => Array.from(array)), // Convert Uint32Array to Array for serialization
|
||||
rootID,
|
||||
snapshots: Array.from(snapshots.entries()),
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
profilingOperations: profilingOperationsForRoot,
|
||||
profilingSnapshots: profilingSnapshotsForRoot,
|
||||
rendererID,
|
||||
rootID,
|
||||
version: PROFILER_EXPORT_VERSION,
|
||||
dataForRoots,
|
||||
};
|
||||
};
|
||||
|
||||
export const prepareProfilingImport = (raw: string) => {
|
||||
const parsed = JSON.parse(raw);
|
||||
|
||||
if (parsed.version !== PROFILER_EXPORT_VERSION) {
|
||||
throw Error(`Unsupported profiler export version "${parsed.version}".`);
|
||||
}
|
||||
|
||||
const entries = [];
|
||||
Object.values(parsed.profilingSnapshots).forEach(snapshot => {
|
||||
entries.push([(snapshot: any).id, snapshot]);
|
||||
});
|
||||
|
||||
const rootID = parsed.profilingSummary.rootID;
|
||||
parsed.profilingOperations = new Map([[rootID, parsed.profilingOperations]]);
|
||||
parsed.profilingSnapshots = new Map([[rootID, new Map(entries)]]);
|
||||
return parsed;
|
||||
};
|
||||
}
|
||||
|
||||
export const getGradientColor = (value: number) => {
|
||||
const maxIndex = commitGradient.length - 1;
|
||||
|
||||
@@ -214,7 +214,26 @@ function updateThemeVariables(
|
||||
updateStyleHelper(theme, 'color-button-disabled', documentElements);
|
||||
updateStyleHelper(theme, 'color-button-focus', documentElements);
|
||||
updateStyleHelper(theme, 'color-button-hover', documentElements);
|
||||
updateStyleHelper(theme, 'color-commit-did-not-render', documentElements);
|
||||
updateStyleHelper(
|
||||
theme,
|
||||
'color-commit-did-not-render-fill',
|
||||
documentElements
|
||||
);
|
||||
updateStyleHelper(
|
||||
theme,
|
||||
'color-commit-did-not-render-fill-text',
|
||||
documentElements
|
||||
);
|
||||
updateStyleHelper(
|
||||
theme,
|
||||
'color-commit-did-not-render-pattern',
|
||||
documentElements
|
||||
);
|
||||
updateStyleHelper(
|
||||
theme,
|
||||
'color-commit-did-not-render-pattern-text',
|
||||
documentElements
|
||||
);
|
||||
updateStyleHelper(theme, 'color-commit-gradient-0', documentElements);
|
||||
updateStyleHelper(theme, 'color-commit-gradient-1', documentElements);
|
||||
updateStyleHelper(theme, 'color-commit-gradient-2', documentElements);
|
||||
@@ -228,6 +247,16 @@ function updateThemeVariables(
|
||||
updateStyleHelper(theme, 'color-commit-gradient-text', documentElements);
|
||||
updateStyleHelper(theme, 'color-component-name', documentElements);
|
||||
updateStyleHelper(theme, 'color-component-name-inverted', documentElements);
|
||||
updateStyleHelper(
|
||||
theme,
|
||||
'color-component-badge-background',
|
||||
documentElements
|
||||
);
|
||||
updateStyleHelper(
|
||||
theme,
|
||||
'color-component-badge-background-inverted',
|
||||
documentElements
|
||||
);
|
||||
updateStyleHelper(theme, 'color-dim', documentElements);
|
||||
updateStyleHelper(theme, 'color-dimmer', documentElements);
|
||||
updateStyleHelper(theme, 'color-dimmest', documentElements);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user