/**
* @jest-environment node
*/
'use strict';
import {patchSetImmediate} from '../../../../scripts/jest/patchSetImmediate';
import crypto from 'crypto';
import fs from 'fs/promises';
import path from 'path';
let React;
let ReactServer;
let cache;
let ReactServerDOMServer;
let ReactServerDOMClient;
let Stream;
let observer;
let getDebugInfo;
const streamOptions = {
objectMode: true,
};
function filterStackFrame(filename, functionName) {
return (
!filename.startsWith('node:') &&
!filename.includes('node_modules') &&
// Filter out our own internal source code since it'll typically be in node_modules
(!filename.includes('/packages/') || filename.includes('/__tests__/')) &&
!filename.includes('/build/') &&
!functionName.includes('internal_')
);
}
describe('ReactFlightAsyncDebugInfo', () => {
beforeEach(() => {
jest.resetModules();
jest.useRealTimers();
patchSetImmediate();
global.console = require('console');
jest.mock('react', () => require('react/react.react-server'));
jest.mock('react-server-dom-webpack/server', () =>
jest.requireActual('react-server-dom-webpack/server.node'),
);
ReactServer = require('react');
ReactServerDOMServer = require('react-server-dom-webpack/server');
cache = ReactServer.cache;
jest.resetModules();
jest.useRealTimers();
patchSetImmediate();
__unmockReact();
jest.unmock('react-server-dom-webpack/server');
jest.mock('react-server-dom-webpack/client', () =>
jest.requireActual('react-server-dom-webpack/client.node'),
);
React = require('react');
ReactServerDOMClient = require('react-server-dom-webpack/client');
Stream = require('stream');
getDebugInfo = require('internal-test-utils').getDebugInfo.bind(null, {
ignoreProps: true,
useFixedTime: true,
});
});
afterEach(() => {
observer?.disconnect();
observer = undefined;
});
function finishLoadingStream(readable) {
return new Promise(resolve => {
if (readable.readableEnded) {
resolve();
} else {
readable.on('end', () => resolve());
}
});
}
function delay(timeout) {
return new Promise(resolve => {
setTimeout(resolve, timeout);
});
}
function fetchThirdParty(Component) {
const stream = ReactServerDOMServer.renderToPipeableStream(
<Component />,
{},
{
environmentName: 'third-party',
},
);
const readable = new Stream.PassThrough(streamOptions);
const result = ReactServerDOMClient.createFromNodeStream(readable, {
moduleMap: {},
moduleLoading: {},
});
stream.pipe(readable);
return result;
}
it('can track async information when awaited', async () => {
async function getData(text) {
await delay(1);
const promise = delay(2);
promise.displayName = 'hello';
await Promise.all([promise]);
return text.toUpperCase();
}
async function Component() {
const result = await getData('hi');
const moreData = getData('seb');
return <InnerComponent text={result} promise={moreData} />;
}
async function InnerComponent({text, promise}) {
// This async function depends on the I/O in parent components but it should not
// include that I/O as part of its own meta data.
return text + ', ' + (await promise);
}
const stream = ReactServerDOMServer.renderToPipeableStream(<Component />);
const readable = new Stream.PassThrough(streamOptions);
const result = ReactServerDOMClient.createFromNodeStream(readable, {
moduleMap: {},
moduleLoading: {},
});
stream.pipe(readable);
expect(await result).toBe('HI, SEB');
await finishLoadingStream(readable);
if (__DEV__ && gate(flags => flags.enableComponentPerformanceTrack)) {
expect(getDebugInfo(result)).toMatchInlineSnapshot(`
[
{
"time": 0,
},
{
"env": "Server",
"key": null,
"name": "Component",
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
130,
109,
109,
50,
],
],
},
{
"time": 0,
},
{
"awaited": {
"end": 0,
"env": "Server",
"name": "delay",
"owner": {
"env": "Server",
"key": null,
"name": "Component",
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
130,
109,
109,
50,
],
],
},
"stack": [
[
"delay",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
87,
12,
86,
3,
],
[
"getData",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
111,
13,
110,
5,
],
[
"Component",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
119,
26,
118,
5,
],
],
"start": 0,
"value": {
"value": undefined,
},
},
"env": "Server",
"owner": {
"env": "Server",
"key": null,
"name": "Component",
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
130,
109,
109,
50,
],
],
},
"stack": [
[
"getData",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
111,
13,
110,
5,
],
[
"Component",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
119,
26,
118,
5,
],
],
},
{
"time": 0,
},
{
"time": 0,
},
{
"awaited": {
"end": 0,
"env": "Server",
"name": "hello",
"owner": {
"env": "Server",
"key": null,
"name": "Component",
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
130,
109,
109,
50,
],
],
},
"stack": [
[
"delay",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
87,
12,
86,
3,
],
[
"getData",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
112,
21,
110,
5,
],
[
"Component",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
119,
20,
118,
5,
],
],
"start": 0,
"value": {
"value": undefined,
},
},
"env": "Server",
"owner": {
"env": "Server",
"key": null,
"name": "Component",
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
130,
109,
109,
50,
],
],
},
"stack": [
[
"getData",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
114,
21,
110,
5,
],
[
"Component",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
119,
20,
118,
5,
],
],
},
{
"time": 0,
},
{
"time": 0,
},
{
"env": "Server",
"key": null,
"name": "InnerComponent",
"props": {},
"stack": [
[
"Component",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
121,
60,
118,
5,
],
],
},
{
"time": 0,
},
{
"awaited": {
"end": 0,
"env": "Server",
"name": "hello",
"owner": {
"env": "Server",
"key": null,
"name": "Component",
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
130,
109,
109,
50,
],
],
},
"stack": [
[
"delay",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
87,
12,
86,
3,
],
[
"getData",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
112,
21,
110,
5,
],
],
"start": 0,
"value": {
"value": undefined,
},
},
"env": "Server",
"owner": {
"env": "Server",
"key": null,
"name": "InnerComponent",
"props": {},
"stack": [
[
"Component",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
121,
60,
118,
5,
],
],
},
"stack": [
[
"InnerComponent",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
127,
35,
124,
5,
],
],
},
{
"time": 0,
},
{
"time": 0,
},
{
"awaited": {
"byteSize": 0,
"end": 0,
"name": "rsc stream",
"owner": null,
"start": 0,
"value": {
"value": "stream",
},
},
},
]
`);
}
});
it('adds a description to I/O', async () => {
async function getData(url) {
// just wait a macrotask for this call to get picked up
await new Promise(resolve => {
setTimeout(() => {
const fakeResponse = {url};
resolve(fakeResponse);
}, 1);
});
}
async function Component() {
await getData('http://github.com/facebook/react/pulls');
await getData('http://github.com/facebook/react/pulls/');
await getData(
'https://this-is-a-very-long-domain-name-what-happens.app/test',
);
await getData(
'https://github.com/facebook/react/commit/75897c2dcd1dd3a6ca46284dd37e13d22b4b16b4',
);
await getData('/this-is-a-very-long-directory-name-what-happens/');
await getData('/this-is-not');
return 'Hi, Sebbie';
}
const stream = ReactServerDOMServer.renderToPipeableStream(<Component />);
const readable = new Stream.PassThrough(streamOptions);
const result = ReactServerDOMClient.createFromNodeStream(
readable,
{
moduleMap: {},
moduleLoading: {},
},
{replayConsoleLogs: true},
);
stream.pipe(readable);
expect(await result).toBe('Hi, Sebbie');
if (__DEV__ && gate(flags => flags.enableComponentPerformanceTrack)) {
let gotEntries;
const hasEntries = new Promise(resolve => {
gotEntries = resolve;
});
const entries = [];
observer = new PerformanceObserver(list => {
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
for (const entry of list.getEntries()) {
const {name} = entry;
entries.push({name});
}
gotEntries();
}).observe({type: 'measure'});
await hasEntries;
await finishLoadingStream(readable);
expect(entries).toMatchInlineSnapshot(`
[
{
"name": "\u200bComponent",
},
{
"name": "await getData (…/pulls)",
},
{
"name": "await getData (…/pulls)",
},
{
"name": "await getData (…/test)",
},
{
"name": "await getData (…/75897c2dcd…13d22b4b16b4)",
},
{
"name": "await getData (/this-is-a-…what-happens)",
},
{
"name": "await getData (/this-is-not)",
},
]
`);
} else {
await finishLoadingStream(readable);
}
});
it('can track async information when use()d', async () => {
async function getData(text) {
await delay(1);
return text.toUpperCase();
}
function Component() {
const result = ReactServer.use(getData('hi'));
const moreData = getData('seb');
return <InnerComponent text={result} promise={moreData} />;
}
function InnerComponent({text, promise}) {
// This async function depends on the I/O in parent components but it should not
// include that I/O as part of its own meta data.
return text + ', ' + ReactServer.use(promise);
}
const stream = ReactServerDOMServer.renderToPipeableStream(
<Component />,
{},
{
filterStackFrame,
},
);
const readable = new Stream.PassThrough(streamOptions);
const result = ReactServerDOMClient.createFromNodeStream(readable, {
moduleMap: {},
moduleLoading: {},
});
stream.pipe(readable);
expect(await result).toBe('HI, SEB');
await finishLoadingStream(readable);
if (__DEV__ && gate(flags => flags.enableComponentPerformanceTrack)) {
expect(getDebugInfo(result)).toMatchInlineSnapshot(`
[
{
"time": 0,
},
{
"env": "Server",
"key": null,
"name": "Component",
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
578,
40,
559,
49,
],
[
"new Promise",
"",
0,
0,
0,
0,
],
],
},
{
"time": 0,
},
{
"awaited": {
"end": 0,
"env": "Server",
"name": "delay",
"owner": {
"env": "Server",
"key": null,
"name": "Component",
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
578,
40,
559,
49,
],
[
"new Promise",
"",
0,
0,
0,
0,
],
],
},
"stack": [
[
"delay",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
87,
12,
86,
3,
],
[
"getData",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
561,
13,
560,
5,
],
[
"Component",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
566,
36,
565,
5,
],
],
"start": 0,
"value": {
"value": undefined,
},
},
"env": "Server",
"owner": {
"env": "Server",
"key": null,
"name": "Component",
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
578,
40,
559,
49,
],
[
"new Promise",
"",
0,
0,
0,
0,
],
],
},
"stack": [
[
"getData",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
561,
13,
560,
5,
],
[
"Component",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
566,
36,
565,
5,
],
],
},
{
"time": 0,
},
{
"time": 0,
},
{
"env": "Server",
"key": null,
"name": "InnerComponent",
"props": {},
"stack": [
[
"Component",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
568,
60,
565,
5,
],
],
},
{
"awaited": {
"end": 0,
"env": "Server",
"name": "delay",
"owner": {
"env": "Server",
"key": null,
"name": "Component",
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
578,
40,
559,
49,
],
[
"new Promise",
"",
0,
0,
0,
0,
],
],
},
"stack": [
[
"delay",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
87,
12,
86,
3,
],
[
"getData",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
561,
13,
560,
5,
],
[
"Component",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
567,
22,
565,
5,
],
],
"start": 0,
"value": {
"value": undefined,
},
},
"env": "Server",
"owner": {
"env": "Server",
"key": null,
"name": "InnerComponent",
"props": {},
"stack": [
[
"Component",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
568,
60,
565,
5,
],
],
},
"stack": [
[
"InnerComponent",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
574,
40,
571,
5,
],
],
},
{
"time": 0,
},
{
"time": 0,
},
{
"awaited": {
"byteSize": 0,
"end": 0,
"name": "rsc stream",
"owner": null,
"start": 0,
"value": {
"value": "stream",
},
},
},
]
`);
}
});
it('cannot currently track the start of I/O when no native promise is used', async () => {
function Component() {
const callbacks = [];
setTimeout(function timer() {
callbacks.forEach(callback => callback('hi'));
}, 5);
return {
then(callback) {
callbacks.push(callback);
},
};
}
const stream = ReactServerDOMServer.renderToPipeableStream(<Component />);
const readable = new Stream.PassThrough(streamOptions);
const result = ReactServerDOMClient.createFromNodeStream(readable, {
moduleMap: {},
moduleLoading: {},
});
stream.pipe(readable);
expect(await result).toBe('hi');
await finishLoadingStream(readable);
if (__DEV__ && gate(flags => flags.enableComponentPerformanceTrack)) {
expect(getDebugInfo(result)).toMatchInlineSnapshot(`
[
{
"time": 0,
},
{
"env": "Server",
"key": null,
"name": "Component",
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
879,
109,
866,
80,
],
],
},
{
"awaited": {
"end": 0,
"env": "Server",
"name": "",
"owner": {
"env": "Server",
"key": null,
"name": "Component",
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
879,
109,
866,
80,
],
],
},
"start": 0,
},
"env": "Server",
"owner": {
"env": "Server",
"key": null,
"name": "Component",
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
879,
109,
866,
80,
],
],
},
},
{
"time": 0,
},
{
"time": 0,
},
{
"awaited": {
"byteSize": 0,
"end": 0,
"name": "rsc stream",
"owner": null,
"start": 0,
"value": {
"value": "stream",
},
},
},
]
`);
}
});
it('can ingores the start of I/O when immediately resolved non-native promise is awaited', async () => {
async function Component() {
return await {
then(callback) {
callback('hi');
},
};
}
const stream = ReactServerDOMServer.renderToPipeableStream(<Component />);
const readable = new Stream.PassThrough(streamOptions);
const result = ReactServerDOMClient.createFromNodeStream(readable, {
moduleMap: {},
moduleLoading: {},
});
stream.pipe(readable);
expect(await result).toBe('hi');
await finishLoadingStream(readable);
if (__DEV__ && gate(flags => flags.enableComponentPerformanceTrack)) {
expect(getDebugInfo(result)).toMatchInlineSnapshot(`
[
{
"time": 0,
},
{
"env": "Server",
"key": null,
"name": "Component",
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
987,
109,
978,
94,
],
],
},
{
"time": 0,
},
]
`);
}
});
it('forwards debugInfo from awaited Promises', async () => {
async function Component() {
let resolve;
const promise = new Promise(r => (resolve = r));
promise._debugInfo = [
{time: performance.now()},
{
name: 'Virtual Component',
},
{time: performance.now()},
];
const promise2 = promise.then(value => value);
promise2._debugInfo = [
{time: performance.now()},
{
name: 'Virtual Component2',
},
{time: performance.now()},
];
resolve('hi');
const result = await promise2;
return result.toUpperCase();
}
const stream = ReactServerDOMServer.renderToPipeableStream(<Component />);
const readable = new Stream.PassThrough(streamOptions);
const result = ReactServerDOMClient.createFromNodeStream(readable, {
moduleMap: {},
moduleLoading: {},
});
stream.pipe(readable);
expect(await result).toBe('HI');
await finishLoadingStream(readable);
if (__DEV__ && gate(flags => flags.enableComponentPerformanceTrack)) {
expect(getDebugInfo(result)).toMatchInlineSnapshot(`
[
{
"time": 0,
},
{
"env": "Server",
"key": null,
"name": "Component",
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1054,
109,
1030,
50,
],
],
},
{
"time": 0,
},
{
"name": "Virtual Component",
},
{
"time": 0,
},
{
"time": 0,
},
{
"name": "Virtual Component2",
},
{
"time": 0,
},
{
"time": 0,
},
]
`);
}
});
it('forwards async debug info one environment to the next', async () => {
async function getData() {
await delay(1);
await delay(2);
return 'hi';
}
async function ThirdPartyComponent() {
const data = await getData();
return data;
}
async function Component() {
const data = await fetchThirdParty(ThirdPartyComponent);
return data.toUpperCase();
}
const stream = ReactServerDOMServer.renderToPipeableStream(<Component />);
const readable = new Stream.PassThrough(streamOptions);
const result = ReactServerDOMClient.createFromNodeStream(readable, {
moduleMap: {},
moduleLoading: {},
});
stream.pipe(readable);
expect(await result).toBe('HI');
await finishLoadingStream(readable);
if (__DEV__ && gate(flags => flags.enableComponentPerformanceTrack)) {
expect(getDebugInfo(result)).toMatchInlineSnapshot(`
[
{
"time": 0,
},
{
"env": "Server",
"key": null,
"name": "Component",
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1132,
109,
1115,
63,
],
],
},
{
"time": 0,
},
{
"env": "third-party",
"key": null,
"name": "ThirdPartyComponent",
"props": {},
"stack": [
[
"fetchThirdParty",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
94,
40,
92,
3,
],
[
"Component",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1128,
24,
1127,
5,
],
],
},
{
"time": 0,
},
{
"awaited": {
"end": 0,
"env": "third-party",
"name": "delay",
"owner": {
"env": "third-party",
"key": null,
"name": "ThirdPartyComponent",
"props": {},
"stack": [
[
"fetchThirdParty",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
94,
40,
92,
3,
],
[
"Component",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1128,
24,
1127,
5,
],
],
},
"stack": [
[
"delay",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
87,
12,
86,
3,
],
[
"getData",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1117,
13,
1116,
5,
],
[
"ThirdPartyComponent",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1123,
24,
1122,
5,
],
],
"start": 0,
"value": {
"value": undefined,
},
},
"env": "third-party",
"owner": {
"env": "third-party",
"key": null,
"name": "ThirdPartyComponent",
"props": {},
"stack": [
[
"fetchThirdParty",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
94,
40,
92,
3,
],
[
"Component",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1128,
24,
1127,
5,
],
],
},
"stack": [
[
"getData",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1117,
13,
1116,
5,
],
[
"ThirdPartyComponent",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1123,
24,
1122,
5,
],
],
},
{
"time": 0,
},
{
"time": 0,
},
{
"awaited": {
"end": 0,
"env": "third-party",
"name": "delay",
"owner": {
"env": "third-party",
"key": null,
"name": "ThirdPartyComponent",
"props": {},
"stack": [
[
"fetchThirdParty",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
94,
40,
92,
3,
],
[
"Component",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1128,
24,
1127,
5,
],
],
},
"stack": [
[
"delay",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
87,
12,
86,
3,
],
[
"getData",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1118,
13,
1116,
5,
],
[
"ThirdPartyComponent",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1123,
18,
1122,
5,
],
],
"start": 0,
"value": {
"value": undefined,
},
},
"env": "third-party",
"owner": {
"env": "third-party",
"key": null,
"name": "ThirdPartyComponent",
"props": {},
"stack": [
[
"fetchThirdParty",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
94,
40,
92,
3,
],
[
"Component",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1128,
24,
1127,
5,
],
],
},
"stack": [
[
"getData",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1118,
13,
1116,
5,
],
[
"ThirdPartyComponent",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1123,
18,
1122,
5,
],
],
},
{
"time": 0,
},
{
"time": 0,
},
{
"awaited": {
"byteSize": 0,
"end": 0,
"env": "Server",
"name": "rsc stream",
"start": 0,
"value": {
"value": "stream",
},
},
"env": "Server",
},
{
"time": 0,
},
{
"awaited": {
"byteSize": 0,
"end": 0,
"name": "rsc stream",
"owner": null,
"start": 0,
"value": {
"value": "stream",
},
},
},
]
`);
}
});
it('can track cached entries awaited in later components', async () => {
const getData = cache(async function getData(text) {
await delay(1);
return text.toUpperCase();
});
async function Child() {
const greeting = await getData('hi');
return greeting + ', Seb';
}
async function Component() {
await getData('hi');
return <Child />;
}
const stream = ReactServerDOMServer.renderToPipeableStream(
<Component />,
{},
{
filterStackFrame,
},
);
const readable = new Stream.PassThrough(streamOptions);
const result = ReactServerDOMClient.createFromNodeStream(readable, {
moduleMap: {},
moduleLoading: {},
});
stream.pipe(readable);
expect(await result).toBe('HI, Seb');
await finishLoadingStream(readable);
if (__DEV__ && gate(flags => flags.enableComponentPerformanceTrack)) {
expect(getDebugInfo(result)).toMatchInlineSnapshot(`
[
{
"time": 0,
},
{
"env": "Server",
"key": null,
"name": "Component",
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1467,
40,
1450,
62,
],
[
"new Promise",
"",
0,
0,
0,
0,
],
],
},
{
"time": 0,
},
{
"awaited": {
"end": 0,
"env": "Server",
"name": "delay",
"owner": {
"env": "Server",
"key": null,
"name": "Component",
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1467,
40,
1450,
62,
],
[
"new Promise",
"",
0,
0,
0,
0,
],
],
},
"stack": [
[
"delay",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
87,
12,
86,
3,
],
[
"getData",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1452,
13,
1451,
25,
],
[
"Component",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1462,
13,
1461,
5,
],
],
"start": 0,
"value": {
"value": undefined,
},
},
"env": "Server",
"owner": {
"env": "Server",
"key": null,
"name": "Component",
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1467,
40,
1450,
62,
],
[
"new Promise",
"",
0,
0,
0,
0,
],
],
},
"stack": [
[
"getData",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1452,
13,
1451,
25,
],
[
"Component",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1462,
13,
1461,
5,
],
],
},
{
"time": 0,
},
{
"time": 0,
},
{
"env": "Server",
"key": null,
"name": "Child",
"props": {},
"stack": [
[
"Component",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1463,
60,
1461,
5,
],
],
},
{
"time": 0,
},
{
"awaited": {
"end": 0,
"env": "Server",
"name": "delay",
"owner": {
"env": "Server",
"key": null,
"name": "Component",
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1467,
40,
1450,
62,
],
[
"new Promise",
"",
0,
0,
0,
0,
],
],
},
"stack": [
[
"delay",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
87,
12,
86,
3,
],
[
"getData",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1452,
13,
1451,
25,
],
[
"Component",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1462,
13,
1461,
5,
],
],
"start": 0,
"value": {
"value": undefined,
},
},
"env": "Server",
"owner": {
"env": "Server",
"key": null,
"name": "Child",
"props": {},
"stack": [
[
"Component",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1463,
60,
1461,
5,
],
],
},
"stack": [
[
"Child",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1457,
28,
1456,
5,
],
],
},
{
"time": 0,
},
{
"time": 0,
},
{
"awaited": {
"byteSize": 0,
"end": 0,
"name": "rsc stream",
"owner": null,
"start": 0,
"value": {
"value": "stream",
},
},
},
]
`);
}
});
it('can track cached entries used in child position', async () => {
const getData = cache(async function getData(text) {
await delay(1);
return text.toUpperCase();
});
function Child() {
return getData('hi');
}
function Component() {
ReactServer.use(getData('hi'));
return <Child />;
}
const stream = ReactServerDOMServer.renderToPipeableStream(
<Component />,
{},
{
filterStackFrame,
},
);
const readable = new Stream.PassThrough(streamOptions);
const result = ReactServerDOMClient.createFromNodeStream(readable, {
moduleMap: {},
moduleLoading: {},
});
stream.pipe(readable);
expect(await result).toBe('HI');
await finishLoadingStream(readable);
if (__DEV__ && gate(flags => flags.enableComponentPerformanceTrack)) {
expect(getDebugInfo(result)).toMatchInlineSnapshot(`
[
{
"time": 0,
},
{
"env": "Server",
"key": null,
"name": "Component",
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1774,
40,
1758,
57,
],
[
"new Promise",
"",
0,
0,
0,
0,
],
],
},
{
"time": 0,
},
{
"awaited": {
"end": 0,
"env": "Server",
"name": "delay",
"owner": {
"env": "Server",
"key": null,
"name": "Component",
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1774,
40,
1758,
57,
],
[
"new Promise",
"",
0,
0,
0,
0,
],
],
},
"stack": [
[
"delay",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
87,
12,
86,
3,
],
[
"getData",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1760,
13,
1759,
25,
],
[
"Component",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1769,
23,
1768,
5,
],
],
"start": 0,
"value": {
"value": undefined,
},
},
"env": "Server",
"owner": {
"env": "Server",
"key": null,
"name": "Component",
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1774,
40,
1758,
57,
],
[
"new Promise",
"",
0,
0,
0,
0,
],
],
},
"stack": [
[
"getData",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1760,
13,
1759,
25,
],
[
"Component",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1769,
23,
1768,
5,
],
],
},
{
"time": 0,
},
{
"time": 0,
},
{
"env": "Server",
"key": null,
"name": "Child",
"props": {},
"stack": [
[
"Component",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1770,
60,
1768,
5,
],
],
},
{
"awaited": {
"end": 0,
"env": "Server",
"name": "delay",
"owner": {
"env": "Server",
"key": null,
"name": "Component",
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1774,
40,
1758,
57,
],
[
"new Promise",
"",
0,
0,
0,
0,
],
],
},
"stack": [
[
"delay",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
87,
12,
86,
3,
],
[
"getData",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1760,
13,
1759,
25,
],
[
"Component",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1769,
23,
1768,
5,
],
],
"start": 0,
"value": {
"value": undefined,
},
},
"env": "Server",
"stack": [
[
"Component",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
1770,
60,
1768,
5,
],
],
},
{
"time": 0,
},
{
"time": 0,
},
{
"awaited": {
"byteSize": 0,
"end": 0,
"name": "rsc stream",
"owner": null,
"start": 0,
"value": {
"value": "stream",
},
},
},
]
`);
}
});
it('can track implicit returned promises that are blocked by previous data', async () => {
async function delayTwice() {
await delay('', 20);
await delay('', 10);
}
async function delayTrice() {
const p = delayTwice();
await delay('', 40);
return p;
}
async function Bar({children}) {
await delayTrice();
return 'hi';
}
const stream = ReactServerDOMServer.renderToPipeableStream(
<Bar />,
{},
{
filterStackFrame,
},
);
const readable = new Stream.PassThrough(streamOptions);
const result = ReactServerDOMClient.createFromNodeStream(readable, {
moduleMap: {},
moduleLoading: {},
});
stream.pipe(readable);
expect(await result).toBe('hi');
await finishLoadingStream(readable);
if (__DEV__ && gate(flags => flags.enableComponentPerformanceTrack)) {
expect(getDebugInfo(result)).toMatchInlineSnapshot(`
[
{
"time": 0,
},
{
"env": "Server",
"key": null,
"name": "Bar",
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
2064,
40,
2046,
80,
],
[
"new Promise",
"",
0,
0,
0,
0,
],
],
},
{
"time": 0,
},
{
"awaited": {
"end": 0,
"env": "Server",
"name": "delay",
"owner": {
"env": "Server",
"key": null,
"name": "Bar",
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
2064,
40,
2046,
80,
],
[
"new Promise",
"",
0,
0,
0,
0,
],
],
},
"stack": [
[
"delay",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
87,
12,
86,
3,
],
[
"delayTrice",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
2054,
13,
2052,
5,
],
[
"Bar",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
2059,
13,
2058,
5,
],
],
"start": 0,
"value": {
"value": undefined,
},
},
"env": "Server",
"owner": {
"env": "Server",
"key": null,
"name": "Bar",
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
2064,
40,
2046,
80,
],
[
"new Promise",
"",
0,
0,
0,
0,
],
],
},
"stack": [
[
"delayTrice",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
2054,
13,
2052,
5,
],
[
"Bar",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
2059,
13,
2058,
5,
],
],
},
{
"time": 0,
},
{
"awaited": {
"end": 0,
"env": "Server",
"name": "delay",
"owner": {
"env": "Server",
"key": null,
"name": "Bar",
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
2064,
40,
2046,
80,
],
[
"new Promise",
"",
0,
0,
0,
0,
],
],
},
"stack": [
[
"delay",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
87,
12,
86,
3,
],
[
"delayTwice",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
2048,
13,
2047,
5,
],
[
"delayTrice",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
2053,
15,
2052,
5,
],
[
"Bar",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
2059,
13,
2058,
5,
],
],
"start": 0,
"value": {
"value": undefined,
},
},
"env": "Server",
"owner": {
"env": "Server",
"key": null,
"name": "Bar",
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
2064,
40,
2046,
80,
],
[
"new Promise",
"",
0,
0,
0,
0,
],
],
},
"stack": [
[
"delayTwice",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
2048,
13,
2047,
5,
],
[
"delayTrice",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
2053,
15,
2052,
5,
],
[
"Bar",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
2059,
13,
2058,
5,
],
],
},
{
"time": 0,
},
{
"awaited": {
"end": 0,
"env": "Server",
"name": "delay",
"owner": {
"env": "Server",
"key": null,
"name": "Bar",
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
2064,
40,
2046,
80,
],
[
"new Promise",
"",
0,
0,
0,
0,
],
],
},
"stack": [
[
"delay",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
87,
12,
86,
3,
],
[
"delayTwice",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
2049,
13,
2047,
5,
],
],
"start": 0,
"value": {
"value": undefined,
},
},
"env": "Server",
"owner": {
"env": "Server",
"key": null,
"name": "Bar",
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
2064,
40,
2046,
80,
],
[
"new Promise",
"",
0,
0,
0,
0,
],
],
},
"stack": [
[
"delayTwice",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
2049,
13,
2047,
5,
],
],
},
{
"time": 0,
},
{
"time": 0,
},
{
"awaited": {
"byteSize": 0,
"end": 0,
"name": "rsc stream",
"owner": null,
"start": 0,
"value": {
"value": "stream",
},
},
},
]
`);
}
});
it('can track IO that is chained via then(async ...)', async () => {
function getData(text) {
return delay(1).then(async () => {
return text.toUpperCase();
});
}
async function Component({text, promise}) {
return await getData('hi, sebbie');
}
const stream = ReactServerDOMServer.renderToPipeableStream(<Component />);
const readable = new Stream.PassThrough(streamOptions);
const result = ReactServerDOMClient.createFromNodeStream(readable, {
moduleMap: {},
moduleLoading: {},
});
stream.pipe(readable);
expect(await result).toBe('HI, SEBBIE');
await finishLoadingStream(readable);
if (__DEV__ && gate(flags => flags.enableComponentPerformanceTrack)) {
expect(getDebugInfo(result)).toMatchInlineSnapshot(`
[
{
"time": 0,
},
{
"env": "Server",
"key": null,
"name": "Component",
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
2467,
109,
2456,
58,
],
],
},
{
"time": 0,
},
{
"awaited": {
"end": 0,
"env": "Server",
"name": "delay",
"owner": {
"env": "Server",
"key": null,
"name": "Component",
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
2467,
109,
2456,
58,
],
],
},
"stack": [
[
"delay",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
87,
12,
86,
3,
],
[
"getData",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
2458,
14,
2457,
5,
],
[
"Component",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
2464,
20,
2463,
5,
],
],
"start": 0,
"value": {
"value": undefined,
},
},
"env": "Server",
"owner": {
"env": "Server",
"key": null,
"name": "Component",
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
2467,
109,
2456,
58,
],
],
},
"stack": [
[
"getData",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
2458,
23,
2457,
5,
],
[
"Component",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
2464,
20,
2463,
5,
],
],
},
{
"time": 0,
},
{
"time": 0,
},
{
"awaited": {
"byteSize": 0,
"end": 0,
"name": "rsc stream",
"owner": null,
"start": 0,
"value": {
"value": "stream",
},
},
},
]
`);
}
});
it('can track IO that is not awaited in user space', async () => {
function internal_API(text) {
return delay(1).then(async () => {
return text.toUpperCase();
});
}
async function Component({text, promise}) {
return await internal_API('hi, seb');
}
const stream = ReactServerDOMServer.renderToPipeableStream(
<Component />,
{},
{
filterStackFrame,
},
);
const readable = new Stream.PassThrough(streamOptions);
const result = ReactServerDOMClient.createFromNodeStream(readable, {
moduleMap: {},
moduleLoading: {},
});
stream.pipe(readable);
expect(await result).toBe('HI, SEB');
await finishLoadingStream(readable);
if (__DEV__ && gate(flags => flags.enableComponentPerformanceTrack)) {
expect(getDebugInfo(result)).toMatchInlineSnapshot(`
[
{
"time": 0,
},
{
"env": "Server",
"key": null,
"name": "Component",
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
2628,
40,
2616,
56,
],
[
"new Promise",
"",
0,
0,
0,
0,
],
],
},
{
"time": 0,
},
{
"awaited": {
"end": 0,
"env": "Server",
"name": "delay",
"owner": {
"env": "Server",
"key": null,
"name": "Component",
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
2628,
40,
2616,
56,
],
[
"new Promise",
"",
0,
0,
0,
0,
],
],
},
"stack": [
[
"delay",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
87,
12,
86,
3,
],
[
"Component",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
2624,
20,
2623,
5,
],
],
"start": 0,
"value": {
"value": "HI, SEB",
},
},
"env": "Server",
"owner": {
"env": "Server",
"key": null,
"name": "Component",
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
2628,
40,
2616,
56,
],
[
"new Promise",
"",
0,
0,
0,
0,
],
],
},
"stack": [
[
"Component",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
2624,
20,
2623,
5,
],
],
},
{
"time": 0,
},
{
"time": 0,
},
{
"awaited": {
"byteSize": 0,
"end": 0,
"name": "rsc stream",
"owner": null,
"start": 0,
"value": {
"value": "stream",
},
},
},
]
`);
}
});
it('can track IO in third-party code', async () => {
async function thirdParty(endpoint) {
return new Promise(resolve => {
setTimeout(() => {
resolve('third-party ' + endpoint);
}, 10);
}).then(async value => {
await new Promise(resolve => {
setTimeout(resolve, 10);
});
return value;
});
}
async function Component() {
const value = await thirdParty('hi');
return value;
}
const stream = ReactServerDOMServer.renderToPipeableStream(
<Component />,
{},
{
filterStackFrame(filename, functionName) {
if (functionName === 'thirdParty') {
return false;
}
return filterStackFrame(filename, functionName);
},
},
);
const readable = new Stream.PassThrough(streamOptions);
const result = ReactServerDOMClient.createFromNodeStream(readable, {
moduleMap: {},
moduleLoading: {},
});
stream.pipe(readable);
expect(await result).toBe('third-party hi');
await finishLoadingStream(readable);
if (__DEV__ && gate(flags => flags.enableComponentPerformanceTrack)) {
expect(getDebugInfo(result)).toMatchInlineSnapshot(`
[
{
"time": 0,
},
{
"env": "Server",
"key": null,
"name": "Component",
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
2811,
40,
2790,
42,
],
[
"new Promise",
"",
0,
0,
0,
0,
],
],
},
{
"time": 0,
},
{
"awaited": {
"end": 0,
"env": "Server",
"name": "",
"owner": {
"env": "Server",
"key": null,
"name": "Component",
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
2811,
40,
2790,
42,
],
[
"new Promise",
"",
0,
0,
0,
0,
],
],
},
"stack": [
[
"",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
2797,
15,
2796,
15,
],
[
"Component",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
2806,
19,
2805,
5,
],
],
"start": 0,
"value": {
"value": undefined,
},
},
"env": "Server",
"owner": {
"env": "Server",
"key": null,
"name": "Component",
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
2811,
40,
2790,
42,
],
[
"new Promise",
"",
0,
0,
0,
0,
],
],
},
"stack": [
[
"",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
2797,
15,
2796,
15,
],
[
"Component",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
2806,
19,
2805,
5,
],
],
},
{
"time": 0,
},
{
"awaited": {
"end": 0,
"env": "Server",
"name": "thirdParty",
"owner": {
"env": "Server",
"key": null,
"name": "Component",
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
2811,
40,
2790,
42,
],
[
"new Promise",
"",
0,
0,
0,
0,
],
],
},
"stack": [
[
"Component",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
2806,
25,
2805,
5,
],
],
"start": 0,
"value": {
"value": "third-party hi",
},
},
"env": "Server",
"owner": {
"env": "Server",
"key": null,
"name": "Component",
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
2811,
40,
2790,
42,
],
[
"new Promise",
"",
0,
0,
0,
0,
],
],
},
"stack": [
[
"Component",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
2806,
25,
2805,
5,
],
],
},
{
"time": 0,
},
{
"time": 0,
},
{
"awaited": {
"byteSize": 0,
"end": 0,
"name": "rsc stream",
"owner": null,
"start": 0,
"value": {
"value": "stream",
},
},
},
]
`);
}
});
it('can track async file reads', async () => {
const filename = path.join(__dirname, 'test-file.txt');
async function Component() {
const buffer = await fs
.readFile(filename) // This loads a Buffer.
// TODO: For some reason, without this we're extracting the wrong promise.
.then(v => v);
const text = buffer.toString('utf8');
return text.slice(0, 26);
}
const stream = ReactServerDOMServer.renderToPipeableStream(
ReactServer.createElement(Component),
{},
);
const readable = new Stream.PassThrough(streamOptions);
const result = ReactServerDOMClient.createFromNodeStream(readable, {
moduleMap: {},
moduleLoading: {},
});
stream.pipe(readable);
expect(await result).toBe('Lorem ipsum dolor sit amet');
await finishLoadingStream(readable);
if (__DEV__ && gate(flags => flags.enableComponentPerformanceTrack)) {
expect(getDebugInfo(result)).toMatchInlineSnapshot(`
[
{
"time": 0,
},
{
"env": "Server",
"key": null,
"name": "Component",
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
3081,
19,
3069,
36,
],
],
},
{
"time": 0,
},
{
"awaited": {
"end": 0,
"env": "Server",
"name": "Object.readFile",
"owner": {
"env": "Server",
"key": null,
"name": "Component",
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
3081,
19,
3069,
36,
],
],
},
"stack": [
[
"Component",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
3073,
7,
3071,
5,
],
],
"start": 0,
"value": {
"status": "halted",
},
},
"env": "Server",
"owner": {
"env": "Server",
"key": null,
"name": "Component",
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
3081,
19,
3069,
36,
],
],
},
"stack": [
[
"Component",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
3075,
7,
3071,
5,
],
],
},
{
"time": 0,
},
{
"time": 0,
},
{
"awaited": {
"byteSize": 0,
"end": 0,
"name": "rsc stream",
"owner": null,
"start": 0,
"value": {
"value": "stream",
},
},
},
]
`);
}
});
it('can track a promise created fully outside first party code', async function internal_test() {
async function internal_API(text, timeout) {
let resolve;
const promise = new Promise(r => {
resolve = r;
});
promise.displayName = 'greeting';
setTimeout(() => resolve(text), timeout);
return promise;
}
async function Component({promise}) {
const result = await promise;
return result;
}
const stream = ReactServerDOMServer.renderToPipeableStream(
<Component promise={internal_API('hello', 1)} />,
{},
{
filterStackFrame,
},
);
const readable = new Stream.PassThrough(streamOptions);
const result = ReactServerDOMClient.createFromNodeStream(readable, {
moduleMap: {},
moduleLoading: {},
});
stream.pipe(readable);
expect(await result).toBe('hello');
await finishLoadingStream(readable);
if (__DEV__ && gate(flags => flags.enableComponentPerformanceTrack)) {
expect(getDebugInfo(result)).toMatchInlineSnapshot(`
[
{
"time": 0,
},
{
"env": "Server",
"key": null,
"name": "Component",
"props": {},
"stack": [
[
"new Promise",
"",
0,
0,
0,
0,
],
],
},
{
"time": 0,
},
{
"awaited": {
"end": 0,
"env": "Server",
"name": "greeting",
"start": 0,
"value": {
"status": "halted",
},
},
"env": "Server",
"owner": {
"env": "Server",
"key": null,
"name": "Component",
"props": {},
"stack": [
[
"new Promise",
"",
0,
0,
0,
0,
],
],
},
"stack": [
[
"Component",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
3220,
20,
3219,
5,
],
],
},
{
"time": 0,
},
{
"time": 0,
},
]
`);
}
});
// Regression test: Database clients like Gel/EdgeDB with connection pools can
// create very long chains of async nodes linked via `previous` pointers.
// visitAsyncNode must handle chains of thousands of nodes without stack
// overflow.
//
// The pattern that creates deep chains (Event/messageWaiter):
// 1. Create a Promise resolved by an I/O callback
// 2. Start I/O, await the Promise
// 3. When I/O resolves, code continues in the callback context
// 4. The next iteration's async nodes link via `previous` to the current
// context
it('handles deep linear async chains from connection pool patterns', async () => {
// Replicate Gel's Event class pattern, a Promise resolved externally by I/O
class Event {
constructor() {
this._resolve = null;
this._promise = new Promise(resolve => {
this._resolve = resolve;
});
}
wait() {
return this._promise;
}
set() {
this._resolve(true);
}
}
// Replicate Gel's _waitForMessage pattern:
// Each iteration creates an Event, schedules I/O to resolve it, and awaits.
// The next iteration runs in the context of the previous I/O callback,
// creating linear chains of previous pointers.
async function buildLinearChain(depth) {
for (let i = 0; i < depth; i++) {
const event = new Event();
// crypto.randomBytes uses the thread pool and is recognized as I/O
// (type='RANDOMBYTESREQUEST'). It's much faster than setTimeout.
crypto.randomBytes(1, () => event.set());
await event.wait();
// After this await resolves, we're in the crypto callback's context
// The next Event will be created in this context, linking prev pointers
}
}
async function Component() {
// Use 2000 iterations to ensure regression would be caught. Using
// crypto.randomBytes keeps the test fast.
await buildLinearChain(2000);
return 'done';
}
const stream = ReactServerDOMServer.renderToPipeableStream(
<Component />,
{},
{
filterStackFrame: (filename, functionName) => {
// Custom filter that treats Event and buildLinearChain as library
// code. This simulates how real DB libraries like Gel/EdgeDB would be
// filtered.
if (
functionName === 'new Event' ||
functionName === 'buildLinearChain' ||
functionName === '_loop' // Generated name for the for-loop
) {
return false;
}
return filterStackFrame(filename, functionName);
},
},
);
const readable = new Stream.PassThrough(streamOptions);
const result = ReactServerDOMClient.createFromNodeStream(readable, {
moduleMap: {},
moduleLoading: {},
});
stream.pipe(readable);
// This should not throw "Maximum call stack size exceeded"
expect(await result).toBe('done');
await finishLoadingStream(readable);
if (__DEV__ && gate(flags => flags.enableComponentPerformanceTrack)) {
// With library code filtered out, we should only see the Component's
// debug info, not thousands of entries from the internal
// Event/buildLinearChain operations.
expect(getDebugInfo(result)).toMatchInlineSnapshot(`
[
{
"time": 0,
},
{
"env": "Server",
"key": null,
"name": "Component",
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
3371,
40,
3328,
72,
],
[
"new Promise",
"",
0,
0,
0,
0,
],
],
},
{
"time": 0,
},
{
"awaited": {
"end": 0,
"env": "Server",
"name": "buildLinearChain",
"owner": {
"env": "Server",
"key": null,
"name": "Component",
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
3371,
40,
3328,
72,
],
[
"new Promise",
"",
0,
0,
0,
0,
],
],
},
"stack": [
[
"Component",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
3366,
13,
3363,
5,
],
],
"start": 0,
"value": {
"value": undefined,
},
},
"env": "Server",
"owner": {
"env": "Server",
"key": null,
"name": "Component",
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
3371,
40,
3328,
72,
],
[
"new Promise",
"",
0,
0,
0,
0,
],
],
},
"stack": [
[
"Component",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
3366,
13,
3363,
5,
],
],
},
{
"time": 0,
},
{
"time": 0,
},
{
"awaited": {
"byteSize": 0,
"end": 0,
"name": "rsc stream",
"owner": null,
"start": 0,
"value": {
"value": "stream",
},
},
},
]
`);
}
});
it('should not exponentially accumulate debug info on outlined debug chunks', async () => {
// Regression test: Each Level wraps its received `context` prop in a new
// object before passing it down. This creates props deduplication
// references to the parent's outlined chunk alongside the owner reference,
// giving 2 references per level to the direct parent's chunk. Without
// skipping transferReferencedDebugInfo during debug info resolution, this
// test would fail with an infinite loop detection error.
async function Level({depth, context}) {
await delay(0);
if (depth === 0) {
return <div>Hello, World!</div>;
}
const newContext = {prev: context, id: depth};
return ReactServer.createElement(Level, {
depth: depth - 1,
context: newContext,
});
}
const stream = ReactServerDOMServer.renderToPipeableStream(
ReactServer.createElement(Level, {depth: 20, context: {root: true}}),
);
const readable = new Stream.PassThrough(streamOptions);
const result = ReactServerDOMClient.createFromNodeStream(readable, {
moduleMap: {},
moduleLoading: {},
});
stream.pipe(readable);
const resolved = await result;
expect(resolved.type).toBe('div');
await finishLoadingStream(readable);
});
it('omits large debug strings to avoid blocking the main thread when parsing', async () => {
async function Component() {
// This promise's value is expected to show up in the debug info below.
const small = await new Promise(resolve => {
setTimeout(() => resolve('hello'), 1);
});
// This promise's value exceeds the threshold for debug string length and
// is expected to show up as a placeholder in the debug info below.
// Reconstructing a multi-megabyte string on the client would block the
// main thread for too long.
const large = await new Promise(resolve => {
setTimeout(() => resolve('x'.repeat(1000001)), 1);
});
return small + ' ' + large.length;
}
const stream = ReactServerDOMServer.renderToPipeableStream(
ReactServer.createElement(Component),
{},
{filterStackFrame},
);
const readable = new Stream.PassThrough(streamOptions);
const result = ReactServerDOMClient.createFromNodeStream(readable, {
moduleMap: {},
moduleLoading: {},
});
stream.pipe(readable);
expect(await result).toBe('hello 1000001');
await finishLoadingStream(readable);
if (__DEV__ && gate(flags => flags.enableComponentPerformanceTrack)) {
expect(getDebugInfo(result)).toMatchInlineSnapshot(`
[
{
"time": 0,
},
{
"env": "Server",
"key": null,
"name": "Component",
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
3596,
19,
3577,
82,
],
[
"new Promise",
"",
0,
0,
0,
0,
],
],
},
{
"time": 0,
},
{
"awaited": {
"end": 0,
"env": "Server",
"name": "Component",
"owner": {
"env": "Server",
"key": null,
"name": "Component",
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
3596,
19,
3577,
82,
],
[
"new Promise",
"",
0,
0,
0,
0,
],
],
},
"stack": [
[
"Component",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
3580,
25,
3578,
5,
],
],
"start": 0,
"value": {
"value": "hello",
},
},
"env": "Server",
"owner": {
"env": "Server",
"key": null,
"name": "Component",
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
3596,
19,
3577,
82,
],
[
"new Promise",
"",
0,
0,
0,
0,
],
],
},
"stack": [
[
"Component",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
3580,
25,
3578,
5,
],
],
},
{
"time": 0,
},
{
"time": 0,
},
{
"awaited": {
"end": 0,
"env": "Server",
"name": "Component",
"owner": {
"env": "Server",
"key": null,
"name": "Component",
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
3596,
19,
3577,
82,
],
[
"new Promise",
"",
0,
0,
0,
0,
],
],
},
"stack": [
[
"Component",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
3588,
25,
3578,
5,
],
],
"start": 0,
"value": {
"value": "This string of length 1000001 has been omitted by React to avoid sending too much data from the server.",
},
},
"env": "Server",
"owner": {
"env": "Server",
"key": null,
"name": "Component",
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
3596,
19,
3577,
82,
],
[
"new Promise",
"",
0,
0,
0,
0,
],
],
},
"stack": [
[
"Component",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
3588,
25,
3578,
5,
],
],
},
{
"time": 0,
},
{
"time": 0,
},
{
"awaited": {
"byteSize": 0,
"end": 0,
"name": "rsc stream",
"owner": null,
"start": 0,
"value": {
"value": "stream",
},
},
},
]
`);
}
});
it('does not lose I/O debug info when intermediate promises are garbage collected', async () => {
// Get a handle on the garbage collector without running the test with --expose-gc.
const v8 = require('v8');
const vm = require('vm');
v8.setFlagsFromString('--expose-gc');
const gc = vm.runInNewContext('gc');
v8.setFlagsFromString('--no-expose-gc');
// The global setImmediate is patched to setTimeout in this test which
// registers as new I/O in async_hooks. Use the real setImmediate for
// yielding to the event loop while waiting for GC so that it doesn't add
// I/O entries to the debug info of the component below.
const {setImmediate: realSetImmediate} = require('timers');
function tick() {
return new Promise(resolve => realSetImmediate(resolve));
}
let ioPromiseRef = null;
let collectedIntermediatePromises = false;
async function getData(text) {
const promise = delay(1);
ioPromiseRef = new WeakRef(promise);
await promise;
return text.toUpperCase();
}
async function waitForGarbageCollection(weakRef) {
// deref() keeps the target alive until the end of the current task, so
// check it on a later tick than the gc() call.
let collected = false;
for (let i = 0; !collected && i < 100; i++) {
gc();
await tick();
collected = weakRef.deref() === undefined;
await tick();
}
// The destroy() hooks of collected promises fire asynchronously. Yield
// a few more times to ensure they have all run.
for (let i = 0; i < 5; i++) {
gc();
await tick();
}
return collected;
}
async function Component() {
const result = await getData('hi');
// At this point the intermediate promises (the delay() promise and
// getData's own async function promise) are unreachable. The async
// debug info graph holds them only weakly through WeakRefs. Force them
// to be garbage collected, which fires their async_hooks destroy()
// hooks, before this component resolves, which is when React walks the
// async graph to emit the component's debug info.
collectedIntermediatePromises =
await waitForGarbageCollection(ioPromiseRef);
return result;
}
const stream = ReactServerDOMServer.renderToPipeableStream(<Component />);
const readable = new Stream.PassThrough(streamOptions);
const result = ReactServerDOMClient.createFromNodeStream(readable, {
moduleMap: {},
moduleLoading: {},
});
stream.pipe(readable);
expect(await result).toBe('HI');
// If this fails, the GC helper above no longer actually collects the
// promises and this test is not testing anything.
expect(collectedIntermediatePromises).toBe(true);
await finishLoadingStream(readable);
if (__DEV__ && gate(flags => flags.enableComponentPerformanceTrack)) {
const debugInfo = getDebugInfo(result);
// The I/O entry for delay() must survive the garbage collection of the
// intermediate promises. The graph nodes are intentionally held
// strongly (only the promises themselves are held weakly through
// WeakRefs) so that the originating I/O is still reachable when the
// debug info is emitted after the promises are gone.
expect(debugInfo).toContainEqual(
expect.objectContaining({
awaited: expect.objectContaining({name: 'delay'}),
}),
);
expect(debugInfo).toMatchInlineSnapshot(`
[
{
"time": 0,
},
{
"env": "Server",
"key": null,
"name": "Component",
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
3893,
109,
3834,
87,
],
],
},
{
"time": 0,
},
{
"awaited": {
"end": 0,
"env": "Server",
"name": "delay",
"owner": {
"env": "Server",
"key": null,
"name": "Component",
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
3893,
109,
3834,
87,
],
],
},
"stack": [
[
"delay",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
87,
12,
86,
3,
],
[
"getData",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
3855,
21,
3854,
5,
],
[
"Component",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
3881,
26,
3880,
5,
],
],
"start": 0,
"value": {
"value": undefined,
},
},
"env": "Server",
"owner": {
"env": "Server",
"key": null,
"name": "Component",
"props": {},
"stack": [
[
"Object.<anonymous>",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
3893,
109,
3834,
87,
],
],
},
"stack": [
[
"getData",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
3857,
7,
3854,
5,
],
[
"Component",
"/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js",
3881,
26,
3880,
5,
],
],
},
{
"time": 0,
},
{
"time": 0,
},
{
"awaited": {
"byteSize": 0,
"end": 0,
"name": "rsc stream",
"owner": null,
"start": 0,
"value": {
"value": "stream",
},
},
},
]
`);
}
});
it('should not exponentially accumulate debug info on deduplicated model chunks', async () => {
// Regression test for debug info that grows exponentially with the length
// of the chain, along the path that resolves a reference synchronously.
// Each page derives its records from the records of the page before it.
// Those records deduplicate to the row of that page, which gives this row
// one reference per record. That page has already resolved by then, and it
// still holds its debug info because it resolves to a plain object. Only an
// array, an async iterable, an element, or a lazy node hands the debug info
// to the value. So each reference copies the entries of the previous page,
// and the count doubles at every page.
const pageCount = 10;
async function loadPage(pageNumber, previousRecords) {
await delay(0);
const records = previousRecords.map(record => ({previous: record}));
return {
records,
nextPage:
pageNumber === pageCount ? null : loadPage(pageNumber + 1, records),
};
}
const stream = ReactServerDOMServer.renderToPipeableStream(
loadPage(1, [{id: 'a'}, {id: 'b'}]),
);
const readable = new Stream.PassThrough(streamOptions);
const result = ReactServerDOMClient.createFromNodeStream(readable, {
moduleMap: {},
moduleLoading: {},
});
stream.pipe(readable);
let page = await result;
let lastPage = null;
let pagesRead = 1;
while (page.nextPage !== null) {
lastPage = page.nextPage;
page = await page.nextPage;
pagesRead++;
}
expect(pagesRead).toBe(pageCount);
await finishLoadingStream(readable);
if (__DEV__) {
// Flight represents a Promise in the model with the chunk of its row, so
// this reads the debug info of the row itself. Every page contributes a
// fixed number of entries, so the last page holds a multiple of the page
// count. Without deduplication in transferReferencedDebugInfo the count
// doubles at every page.
expect(lastPage._debugInfo.length).toBeLessThan(100);
}
});
});