diff --git a/src/__tests__/__snapshots__/profilingCharts-test.js.snap b/src/__tests__/__snapshots__/profilingCharts-test.js.snap
new file mode 100644
index 0000000000..36e4c8cf90
--- /dev/null
+++ b/src/__tests__/__snapshots__/profilingCharts-test.js.snap
@@ -0,0 +1,333 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`profiling charts flamegraph chart should contain valid data: 0: CommitTree 1`] = `
+Object {
+ "nodes": Map {
+ 1 => Object {
+ "children": Array [
+ 2,
+ ],
+ "displayName": null,
+ "id": 1,
+ "key": null,
+ "parentID": 0,
+ "treeBaseDuration": 14,
+ },
+ 2 => Object {
+ "children": Array [
+ 3,
+ 4,
+ ],
+ "displayName": "Parent",
+ "id": 2,
+ "key": null,
+ "parentID": 1,
+ "treeBaseDuration": 14,
+ },
+ 3 => Object {
+ "children": Array [],
+ "displayName": "Memo(Child)",
+ "id": 3,
+ "key": "first",
+ "parentID": 2,
+ "treeBaseDuration": 2,
+ },
+ 4 => Object {
+ "children": Array [],
+ "displayName": "Memo(Child)",
+ "id": 4,
+ "key": "second",
+ "parentID": 2,
+ "treeBaseDuration": 2,
+ },
+ },
+ "rootID": 1,
+}
+`;
+
+exports[`profiling charts flamegraph chart should contain valid data: 0: FlamegraphChartData 1`] = `
+Object {
+ "baseDuration": 14,
+ "depth": 2,
+ "idToDepthMap": Map {
+ 2 => 1,
+ 4 => 2,
+ 3 => 2,
+ },
+ "maxSelfDuration": 10,
+ "rows": Array [
+ Array [
+ Object {
+ "actualDuration": 14,
+ "didRender": true,
+ "id": 2,
+ "label": "Parent (10.0ms of 14.0ms)",
+ "name": "Parent",
+ "offset": 0,
+ "selfDuration": 10,
+ "treeBaseDuration": 14,
+ },
+ ],
+ Array [
+ Object {
+ "actualDuration": 2,
+ "didRender": true,
+ "id": 4,
+ "label": "Memo(Child) key=\\"second\\" (2.0ms of 2.0ms)",
+ "name": "Memo(Child)",
+ "offset": 12,
+ "selfDuration": 2,
+ "treeBaseDuration": 2,
+ },
+ Object {
+ "actualDuration": 2,
+ "didRender": true,
+ "id": 3,
+ "label": "Memo(Child) key=\\"first\\" (2.0ms of 2.0ms)",
+ "name": "Memo(Child)",
+ "offset": 10,
+ "selfDuration": 2,
+ "treeBaseDuration": 2,
+ },
+ ],
+ ],
+}
+`;
+
+exports[`profiling charts flamegraph chart should contain valid data: 1: CommitTree 1`] = `
+Object {
+ "nodes": Map {
+ 1 => Object {
+ "children": Array [
+ 2,
+ ],
+ "displayName": null,
+ "id": 1,
+ "key": null,
+ "parentID": 0,
+ "treeBaseDuration": 14,
+ },
+ 2 => Object {
+ "children": Array [
+ 3,
+ 4,
+ ],
+ "displayName": "Parent",
+ "id": 2,
+ "key": null,
+ "parentID": 1,
+ "treeBaseDuration": 14,
+ },
+ 3 => Object {
+ "children": Array [],
+ "displayName": "Memo(Child)",
+ "id": 3,
+ "key": "first",
+ "parentID": 2,
+ "treeBaseDuration": 2,
+ },
+ 4 => Object {
+ "children": Array [],
+ "displayName": "Memo(Child)",
+ "id": 4,
+ "key": "second",
+ "parentID": 2,
+ "treeBaseDuration": 2,
+ },
+ },
+ "rootID": 1,
+}
+`;
+
+exports[`profiling charts flamegraph chart should contain valid data: 1: FlamegraphChartData 1`] = `
+Object {
+ "baseDuration": 14,
+ "depth": 2,
+ "idToDepthMap": Map {
+ 2 => 1,
+ 4 => 2,
+ 3 => 2,
+ },
+ "maxSelfDuration": 10,
+ "rows": Array [
+ Array [
+ Object {
+ "actualDuration": 10,
+ "didRender": true,
+ "id": 2,
+ "label": "Parent (10.0ms of 10.0ms)",
+ "name": "Parent",
+ "offset": 0,
+ "selfDuration": 10,
+ "treeBaseDuration": 14,
+ },
+ ],
+ Array [
+ Object {
+ "actualDuration": 0,
+ "didRender": false,
+ "id": 4,
+ "label": "Memo(Child) key=\\"second\\"",
+ "name": "Memo(Child)",
+ "offset": 12,
+ "selfDuration": 0,
+ "treeBaseDuration": 2,
+ },
+ Object {
+ "actualDuration": 0,
+ "didRender": false,
+ "id": 3,
+ "label": "Memo(Child) key=\\"first\\"",
+ "name": "Memo(Child)",
+ "offset": 10,
+ "selfDuration": 0,
+ "treeBaseDuration": 2,
+ },
+ ],
+ ],
+}
+`;
+
+exports[`profiling charts interactions should contain valid data: Interactions 1`] = `
+Object {
+ "lastInteractionTime": 24,
+ "maxCommitDuration": 14,
+}
+`;
+
+exports[`profiling charts interactions should contain valid data: Interactions 2`] = `
+Object {
+ "lastInteractionTime": 24,
+ "maxCommitDuration": 14,
+}
+`;
+
+exports[`profiling charts ranked chart should contain valid data: 0: CommitTree 1`] = `
+Object {
+ "nodes": Map {
+ 1 => Object {
+ "children": Array [
+ 2,
+ ],
+ "displayName": null,
+ "id": 1,
+ "key": null,
+ "parentID": 0,
+ "treeBaseDuration": 14,
+ },
+ 2 => Object {
+ "children": Array [
+ 3,
+ 4,
+ ],
+ "displayName": "Parent",
+ "id": 2,
+ "key": null,
+ "parentID": 1,
+ "treeBaseDuration": 14,
+ },
+ 3 => Object {
+ "children": Array [],
+ "displayName": "Memo(Child)",
+ "id": 3,
+ "key": "first",
+ "parentID": 2,
+ "treeBaseDuration": 2,
+ },
+ 4 => Object {
+ "children": Array [],
+ "displayName": "Memo(Child)",
+ "id": 4,
+ "key": "second",
+ "parentID": 2,
+ "treeBaseDuration": 2,
+ },
+ },
+ "rootID": 1,
+}
+`;
+
+exports[`profiling charts ranked chart should contain valid data: 0: RankedChartData 1`] = `
+Object {
+ "maxValue": 10,
+ "nodes": Array [
+ Object {
+ "id": 2,
+ "label": "Parent (10.0ms)",
+ "name": "Parent",
+ "value": 10,
+ },
+ Object {
+ "id": 3,
+ "label": "Memo(Child) key=\\"first\\" (2.0ms)",
+ "name": "Memo(Child)",
+ "value": 2,
+ },
+ Object {
+ "id": 4,
+ "label": "Memo(Child) key=\\"second\\" (2.0ms)",
+ "name": "Memo(Child)",
+ "value": 2,
+ },
+ ],
+}
+`;
+
+exports[`profiling charts ranked chart should contain valid data: 1: CommitTree 1`] = `
+Object {
+ "nodes": Map {
+ 1 => Object {
+ "children": Array [
+ 2,
+ ],
+ "displayName": null,
+ "id": 1,
+ "key": null,
+ "parentID": 0,
+ "treeBaseDuration": 14,
+ },
+ 2 => Object {
+ "children": Array [
+ 3,
+ 4,
+ ],
+ "displayName": "Parent",
+ "id": 2,
+ "key": null,
+ "parentID": 1,
+ "treeBaseDuration": 14,
+ },
+ 3 => Object {
+ "children": Array [],
+ "displayName": "Memo(Child)",
+ "id": 3,
+ "key": "first",
+ "parentID": 2,
+ "treeBaseDuration": 2,
+ },
+ 4 => Object {
+ "children": Array [],
+ "displayName": "Memo(Child)",
+ "id": 4,
+ "key": "second",
+ "parentID": 2,
+ "treeBaseDuration": 2,
+ },
+ },
+ "rootID": 1,
+}
+`;
+
+exports[`profiling charts ranked chart should contain valid data: 1: RankedChartData 1`] = `
+Object {
+ "maxValue": 10,
+ "nodes": Array [
+ Object {
+ "id": 2,
+ "label": "Parent (10.0ms)",
+ "name": "Parent",
+ "value": 10,
+ },
+ ],
+}
+`;
diff --git a/src/__tests__/profiling-test.js b/src/__tests__/profiling-test.js
index 6c49272847..39dd595e49 100644
--- a/src/__tests__/profiling-test.js
+++ b/src/__tests__/profiling-test.js
@@ -1,13 +1,11 @@
// @flow
-import type React from 'react';
-import type ReactDOM from 'react-dom';
import typeof ReactTestRenderer from 'react-test-renderer';
import type Store from 'src/devtools/store';
describe('profiling', () => {
- let React: React;
- let ReactDOM: ReactDOM;
+ let React;
+ let ReactDOM;
let Scheduler;
let SchedulerTracing;
let TestRenderer: ReactTestRenderer;
diff --git a/src/__tests__/profilingCharts-test.js b/src/__tests__/profilingCharts-test.js
new file mode 100644
index 0000000000..3e2613b5af
--- /dev/null
+++ b/src/__tests__/profilingCharts-test.js
@@ -0,0 +1,292 @@
+// @flow
+
+import typeof TestRendererType from 'react-test-renderer';
+import type Store from 'src/devtools/store';
+
+describe('profiling charts', () => {
+ let React;
+ let ReactDOM;
+ let Scheduler;
+ let SchedulerTracing;
+ let TestRenderer: TestRendererType;
+ let store: Store;
+ let utils;
+
+ beforeEach(() => {
+ utils = require('./utils');
+ utils.beforeEachProfiling();
+
+ store = global.store;
+ store.collapseNodesByDefault = false;
+
+ React = require('react');
+ ReactDOM = require('react-dom');
+ Scheduler = require('scheduler');
+ SchedulerTracing = require('scheduler/tracing');
+ TestRenderer = utils.requireTestRenderer();
+ });
+
+ describe('flamegraph chart', () => {
+ it('should contain valid data', async done => {
+ const Parent = ({ count }) => {
+ Scheduler.advanceTime(10);
+ return (
+
+
+
+
+ );
+ };
+
+ // Memoize children to verify that chart doesn't include in the update.
+ const Child = React.memo(function Child() {
+ Scheduler.advanceTime(2);
+ return null;
+ });
+
+ const container = document.createElement('div');
+
+ utils.act(() => store.startProfiling());
+ utils.act(() =>
+ SchedulerTracing.unstable_trace('mount', Scheduler.unstable_now(), () =>
+ ReactDOM.render(, container)
+ )
+ );
+ utils.act(() =>
+ SchedulerTracing.unstable_trace(
+ 'update',
+ Scheduler.unstable_now(),
+ () => ReactDOM.render(, container)
+ )
+ );
+ utils.act(() => store.stopProfiling());
+
+ let suspenseResolved = false;
+
+ function Suspender({ commitIndex, rendererID, rootID }) {
+ const profilingSummary = store.profilingCache.ProfilingSummary.read({
+ rendererID,
+ 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,
+ });
+ expect(commitTree).toMatchSnapshot(`${commitIndex}: CommitTree`);
+ expect(chartData).toMatchSnapshot(
+ `${commitIndex}: FlamegraphChartData`
+ );
+ return null;
+ }
+
+ const rendererID = utils.getRendererID();
+ const rootID = store.roots[0];
+
+ for (let commitIndex = 0; commitIndex < 2; commitIndex++) {
+ suspenseResolved = false;
+
+ await utils.actSuspense(
+ () =>
+ TestRenderer.create(
+
+
+
+ ),
+ 3
+ );
+
+ expect(suspenseResolved).toBe(true);
+ }
+
+ expect(suspenseResolved).toBe(true);
+
+ done();
+ });
+ });
+
+ describe('ranked chart', () => {
+ it('should contain valid data', async done => {
+ const Parent = ({ count }) => {
+ Scheduler.advanceTime(10);
+ return (
+
+
+
+
+ );
+ };
+
+ // Memoize children to verify that chart doesn't include in the update.
+ const Child = React.memo(function Child() {
+ Scheduler.advanceTime(2);
+ return null;
+ });
+
+ const container = document.createElement('div');
+
+ utils.act(() => store.startProfiling());
+ utils.act(() =>
+ SchedulerTracing.unstable_trace('mount', Scheduler.unstable_now(), () =>
+ ReactDOM.render(, container)
+ )
+ );
+ utils.act(() =>
+ SchedulerTracing.unstable_trace(
+ 'update',
+ Scheduler.unstable_now(),
+ () => ReactDOM.render(, container)
+ )
+ );
+ utils.act(() => store.stopProfiling());
+
+ let suspenseResolved = false;
+
+ function Suspender({ commitIndex, rendererID, rootID }) {
+ const profilingSummary = store.profilingCache.ProfilingSummary.read({
+ rendererID,
+ 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,
+ });
+ expect(commitTree).toMatchSnapshot(`${commitIndex}: CommitTree`);
+ expect(chartData).toMatchSnapshot(`${commitIndex}: RankedChartData`);
+ return null;
+ }
+
+ const rendererID = utils.getRendererID();
+ const rootID = store.roots[0];
+
+ for (let commitIndex = 0; commitIndex < 2; commitIndex++) {
+ suspenseResolved = false;
+
+ await utils.actSuspense(
+ () =>
+ TestRenderer.create(
+
+
+
+ ),
+ 3
+ );
+
+ expect(suspenseResolved).toBe(true);
+ }
+
+ done();
+ });
+ });
+
+ describe('interactions', () => {
+ it('should contain valid data', async done => {
+ const Parent = ({ count }) => {
+ Scheduler.advanceTime(10);
+ return (
+
+
+
+
+ );
+ };
+
+ // Memoize children to verify that chart doesn't include in the update.
+ const Child = React.memo(function Child() {
+ Scheduler.advanceTime(2);
+ return null;
+ });
+
+ const container = document.createElement('div');
+
+ utils.act(() => store.startProfiling());
+ utils.act(() =>
+ SchedulerTracing.unstable_trace('mount', Scheduler.unstable_now(), () =>
+ ReactDOM.render(, container)
+ )
+ );
+ utils.act(() =>
+ SchedulerTracing.unstable_trace(
+ 'update',
+ Scheduler.unstable_now(),
+ () => ReactDOM.render(, container)
+ )
+ );
+ utils.act(() => store.stopProfiling());
+
+ let suspenseResolved = 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,
+ });
+ expect(chartData).toMatchSnapshot('Interactions');
+ return null;
+ }
+
+ const rendererID = utils.getRendererID();
+ const rootID = store.roots[0];
+
+ for (let commitIndex = 0; commitIndex < 2; commitIndex++) {
+ suspenseResolved = false;
+
+ await utils.actSuspense(
+ () =>
+ TestRenderer.create(
+
+
+
+ ),
+ 3
+ );
+
+ expect(suspenseResolved).toBe(true);
+ }
+
+ done();
+ });
+ });
+});
diff --git a/src/__tests__/utils.js b/src/__tests__/utils.js
index 1e3acc55ac..89c7ffd6b1 100644
--- a/src/__tests__/utils.js
+++ b/src/__tests__/utils.js
@@ -12,7 +12,10 @@ export function act(callback: Function): void {
jest.runAllTimers();
}
-export async function actSuspense(callback: Function): Promise {
+export async function actSuspense(
+ callback: Function,
+ numTimesToFlush: number = 1
+): Promise {
const TestUtils = require('react-dom/test-utils');
const Scheduler = require('scheduler');
@@ -24,8 +27,16 @@ export async function actSuspense(callback: Function): Promise {
jest.runAllTimers();
});
- // Re-render after resolved promises
- Scheduler.flushAll();
+ // Run cascading microtasks and flush scheduled React work.
+ // Components that suspend multiple times will need to do this once per suspend operation.
+ // HACK Ideally the mock scheduler would provide an API to ask if there was outstanding work.
+ while (--numTimesToFlush >= 0) {
+ // $FlowFixMe Flow doens't know about "await act()" yet
+ await TestUtils.act(async () => {
+ jest.runAllTimers();
+ Scheduler.flushAll();
+ });
+ }
}
export function beforeEachProfiling(): void {