/** * @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( , {}, { 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 ; } 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(); 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 && flags.enableAsyncDebugInfo, ) ) { expect(getDebugInfo(result)).toMatchInlineSnapshot(` [ { "time": 0, }, { "env": "Server", "key": null, "name": "Component", "props": {}, "stack": [ [ "Object.", "/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.", "/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.", "/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.", "/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.", "/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.", "/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(); 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 && flags.enableAsyncDebugInfo, ) ) { 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 ; } 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( , {}, { 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 && flags.enableAsyncDebugInfo, ) ) { expect(getDebugInfo(result)).toMatchInlineSnapshot(` [ { "time": 0, }, { "env": "Server", "key": null, "name": "Component", "props": {}, "stack": [ [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 590, 40, 571, 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.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 590, 40, 571, 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", 573, 13, 572, 5, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 578, 36, 577, 5, ], ], "start": 0, "value": { "value": undefined, }, }, "env": "Server", "owner": { "env": "Server", "key": null, "name": "Component", "props": {}, "stack": [ [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 590, 40, 571, 49, ], [ "new Promise", "", 0, 0, 0, 0, ], ], }, "stack": [ [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 573, 13, 572, 5, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 578, 36, 577, 5, ], ], }, { "time": 0, }, { "time": 0, }, { "env": "Server", "key": null, "name": "InnerComponent", "props": {}, "stack": [ [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 580, 60, 577, 5, ], ], }, { "awaited": { "end": 0, "env": "Server", "name": "delay", "owner": { "env": "Server", "key": null, "name": "Component", "props": {}, "stack": [ [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 590, 40, 571, 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", 573, 13, 572, 5, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 579, 22, 577, 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", 580, 60, 577, 5, ], ], }, "stack": [ [ "InnerComponent", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 586, 40, 583, 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(); 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 && flags.enableAsyncDebugInfo, ) ) { expect(getDebugInfo(result)).toMatchInlineSnapshot(` [ { "time": 0, }, { "env": "Server", "key": null, "name": "Component", "props": {}, "stack": [ [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 897, 109, 884, 80, ], ], }, { "awaited": { "end": 0, "env": "Server", "name": "", "owner": { "env": "Server", "key": null, "name": "Component", "props": {}, "stack": [ [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 897, 109, 884, 80, ], ], }, "start": 0, }, "env": "Server", "owner": { "env": "Server", "key": null, "name": "Component", "props": {}, "stack": [ [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 897, 109, 884, 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(); 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 && flags.enableAsyncDebugInfo, ) ) { expect(getDebugInfo(result)).toMatchInlineSnapshot(` [ { "time": 0, }, { "env": "Server", "key": null, "name": "Component", "props": {}, "stack": [ [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 1011, 109, 1002, 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(); 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 && flags.enableAsyncDebugInfo, ) ) { expect(getDebugInfo(result)).toMatchInlineSnapshot(` [ { "time": 0, }, { "env": "Server", "key": null, "name": "Component", "props": {}, "stack": [ [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 1084, 109, 1060, 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(); 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 && flags.enableAsyncDebugInfo, ) ) { expect(getDebugInfo(result)).toMatchInlineSnapshot(` [ { "time": 0, }, { "env": "Server", "key": null, "name": "Component", "props": {}, "stack": [ [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 1168, 109, 1151, 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", 1164, 24, 1163, 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", 1164, 24, 1163, 5, ], ], }, "stack": [ [ "delay", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 87, 12, 86, 3, ], [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 1153, 13, 1152, 5, ], [ "ThirdPartyComponent", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 1159, 24, 1158, 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", 1164, 24, 1163, 5, ], ], }, "stack": [ [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 1153, 13, 1152, 5, ], [ "ThirdPartyComponent", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 1159, 24, 1158, 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", 1164, 24, 1163, 5, ], ], }, "stack": [ [ "delay", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 87, 12, 86, 3, ], [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 1154, 13, 1152, 5, ], [ "ThirdPartyComponent", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 1159, 18, 1158, 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", 1164, 24, 1163, 5, ], ], }, "stack": [ [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 1154, 13, 1152, 5, ], [ "ThirdPartyComponent", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 1159, 18, 1158, 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 ; } const stream = ReactServerDOMServer.renderToPipeableStream( , {}, { 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 && flags.enableAsyncDebugInfo, ) ) { expect(getDebugInfo(result)).toMatchInlineSnapshot(` [ { "time": 0, }, { "env": "Server", "key": null, "name": "Component", "props": {}, "stack": [ [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 1509, 40, 1492, 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.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 1509, 40, 1492, 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", 1494, 13, 1493, 25, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 1504, 13, 1503, 5, ], ], "start": 0, "value": { "value": undefined, }, }, "env": "Server", "owner": { "env": "Server", "key": null, "name": "Component", "props": {}, "stack": [ [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 1509, 40, 1492, 62, ], [ "new Promise", "", 0, 0, 0, 0, ], ], }, "stack": [ [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 1494, 13, 1493, 25, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 1504, 13, 1503, 5, ], ], }, { "time": 0, }, { "time": 0, }, { "env": "Server", "key": null, "name": "Child", "props": {}, "stack": [ [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 1505, 60, 1503, 5, ], ], }, { "time": 0, }, { "awaited": { "end": 0, "env": "Server", "name": "delay", "owner": { "env": "Server", "key": null, "name": "Component", "props": {}, "stack": [ [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 1509, 40, 1492, 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", 1494, 13, 1493, 25, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 1504, 13, 1503, 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", 1505, 60, 1503, 5, ], ], }, "stack": [ [ "Child", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 1499, 28, 1498, 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 ; } const stream = ReactServerDOMServer.renderToPipeableStream( , {}, { 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 && flags.enableAsyncDebugInfo, ) ) { expect(getDebugInfo(result)).toMatchInlineSnapshot(` [ { "time": 0, }, { "env": "Server", "key": null, "name": "Component", "props": {}, "stack": [ [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 1822, 40, 1806, 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.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 1822, 40, 1806, 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", 1808, 13, 1807, 25, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 1817, 23, 1816, 5, ], ], "start": 0, "value": { "value": undefined, }, }, "env": "Server", "owner": { "env": "Server", "key": null, "name": "Component", "props": {}, "stack": [ [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 1822, 40, 1806, 57, ], [ "new Promise", "", 0, 0, 0, 0, ], ], }, "stack": [ [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 1808, 13, 1807, 25, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 1817, 23, 1816, 5, ], ], }, { "time": 0, }, { "time": 0, }, { "env": "Server", "key": null, "name": "Child", "props": {}, "stack": [ [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 1818, 60, 1816, 5, ], ], }, { "awaited": { "end": 0, "env": "Server", "name": "delay", "owner": { "env": "Server", "key": null, "name": "Component", "props": {}, "stack": [ [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 1822, 40, 1806, 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", 1808, 13, 1807, 25, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 1817, 23, 1816, 5, ], ], "start": 0, "value": { "value": undefined, }, }, "env": "Server", "stack": [ [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 1818, 60, 1816, 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( , {}, { 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 && flags.enableAsyncDebugInfo, ) ) { expect(getDebugInfo(result)).toMatchInlineSnapshot(` [ { "time": 0, }, { "env": "Server", "key": null, "name": "Bar", "props": {}, "stack": [ [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 2118, 40, 2100, 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.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 2118, 40, 2100, 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", 2108, 13, 2106, 5, ], [ "Bar", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 2113, 13, 2112, 5, ], ], "start": 0, "value": { "value": undefined, }, }, "env": "Server", "owner": { "env": "Server", "key": null, "name": "Bar", "props": {}, "stack": [ [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 2118, 40, 2100, 80, ], [ "new Promise", "", 0, 0, 0, 0, ], ], }, "stack": [ [ "delayTrice", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 2108, 13, 2106, 5, ], [ "Bar", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 2113, 13, 2112, 5, ], ], }, { "time": 0, }, { "awaited": { "end": 0, "env": "Server", "name": "delay", "owner": { "env": "Server", "key": null, "name": "Bar", "props": {}, "stack": [ [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 2118, 40, 2100, 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", 2102, 13, 2101, 5, ], [ "delayTrice", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 2107, 15, 2106, 5, ], [ "Bar", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 2113, 13, 2112, 5, ], ], "start": 0, "value": { "value": undefined, }, }, "env": "Server", "owner": { "env": "Server", "key": null, "name": "Bar", "props": {}, "stack": [ [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 2118, 40, 2100, 80, ], [ "new Promise", "", 0, 0, 0, 0, ], ], }, "stack": [ [ "delayTwice", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 2102, 13, 2101, 5, ], [ "delayTrice", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 2107, 15, 2106, 5, ], [ "Bar", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 2113, 13, 2112, 5, ], ], }, { "time": 0, }, { "awaited": { "end": 0, "env": "Server", "name": "delay", "owner": { "env": "Server", "key": null, "name": "Bar", "props": {}, "stack": [ [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 2118, 40, 2100, 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", 2103, 13, 2101, 5, ], ], "start": 0, "value": { "value": undefined, }, }, "env": "Server", "owner": { "env": "Server", "key": null, "name": "Bar", "props": {}, "stack": [ [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 2118, 40, 2100, 80, ], [ "new Promise", "", 0, 0, 0, 0, ], ], }, "stack": [ [ "delayTwice", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 2103, 13, 2101, 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(); 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 && flags.enableAsyncDebugInfo, ) ) { expect(getDebugInfo(result)).toMatchInlineSnapshot(` [ { "time": 0, }, { "env": "Server", "key": null, "name": "Component", "props": {}, "stack": [ [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 2527, 109, 2516, 58, ], ], }, { "time": 0, }, { "awaited": { "end": 0, "env": "Server", "name": "delay", "owner": { "env": "Server", "key": null, "name": "Component", "props": {}, "stack": [ [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 2527, 109, 2516, 58, ], ], }, "stack": [ [ "delay", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 87, 12, 86, 3, ], [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 2518, 14, 2517, 5, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 2524, 20, 2523, 5, ], ], "start": 0, "value": { "value": undefined, }, }, "env": "Server", "owner": { "env": "Server", "key": null, "name": "Component", "props": {}, "stack": [ [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 2527, 109, 2516, 58, ], ], }, "stack": [ [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 2518, 23, 2517, 5, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 2524, 20, 2523, 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( , {}, { 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 && flags.enableAsyncDebugInfo, ) ) { expect(getDebugInfo(result)).toMatchInlineSnapshot(` [ { "time": 0, }, { "env": "Server", "key": null, "name": "Component", "props": {}, "stack": [ [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 2694, 40, 2682, 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.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 2694, 40, 2682, 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", 2690, 20, 2689, 5, ], ], "start": 0, "value": { "value": "HI, SEB", }, }, "env": "Server", "owner": { "env": "Server", "key": null, "name": "Component", "props": {}, "stack": [ [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 2694, 40, 2682, 56, ], [ "new Promise", "", 0, 0, 0, 0, ], ], }, "stack": [ [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 2690, 20, 2689, 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( , {}, { 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 && flags.enableAsyncDebugInfo, ) ) { expect(getDebugInfo(result)).toMatchInlineSnapshot(` [ { "time": 0, }, { "env": "Server", "key": null, "name": "Component", "props": {}, "stack": [ [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 2883, 40, 2862, 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.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 2883, 40, 2862, 42, ], [ "new Promise", "", 0, 0, 0, 0, ], ], }, "stack": [ [ "", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 2869, 15, 2868, 15, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 2878, 19, 2877, 5, ], ], "start": 0, "value": { "value": undefined, }, }, "env": "Server", "owner": { "env": "Server", "key": null, "name": "Component", "props": {}, "stack": [ [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 2883, 40, 2862, 42, ], [ "new Promise", "", 0, 0, 0, 0, ], ], }, "stack": [ [ "", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 2869, 15, 2868, 15, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 2878, 19, 2877, 5, ], ], }, { "time": 0, }, { "awaited": { "end": 0, "env": "Server", "name": "thirdParty", "owner": { "env": "Server", "key": null, "name": "Component", "props": {}, "stack": [ [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 2883, 40, 2862, 42, ], [ "new Promise", "", 0, 0, 0, 0, ], ], }, "stack": [ [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 2878, 25, 2877, 5, ], ], "start": 0, "value": { "value": "third-party hi", }, }, "env": "Server", "owner": { "env": "Server", "key": null, "name": "Component", "props": {}, "stack": [ [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 2883, 40, 2862, 42, ], [ "new Promise", "", 0, 0, 0, 0, ], ], }, "stack": [ [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 2878, 25, 2877, 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 && flags.enableAsyncDebugInfo, ) ) { expect(getDebugInfo(result)).toMatchInlineSnapshot(` [ { "time": 0, }, { "env": "Server", "key": null, "name": "Component", "props": {}, "stack": [ [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 3159, 19, 3147, 36, ], ], }, { "time": 0, }, { "awaited": { "end": 0, "env": "Server", "name": "Object.readFile", "owner": { "env": "Server", "key": null, "name": "Component", "props": {}, "stack": [ [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 3159, 19, 3147, 36, ], ], }, "stack": [ [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 3151, 7, 3149, 5, ], ], "start": 0, "value": { "status": "halted", }, }, "env": "Server", "owner": { "env": "Server", "key": null, "name": "Component", "props": {}, "stack": [ [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 3159, 19, 3147, 36, ], ], }, "stack": [ [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 3153, 7, 3149, 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( , {}, { 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 && flags.enableAsyncDebugInfo, ) ) { 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", 3304, 20, 3303, 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( , {}, { 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 && flags.enableAsyncDebugInfo, ) ) { // 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.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 3461, 40, 3418, 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.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 3461, 40, 3418, 72, ], [ "new Promise", "", 0, 0, 0, 0, ], ], }, "stack": [ [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 3456, 13, 3453, 5, ], ], "start": 0, "value": { "value": undefined, }, }, "env": "Server", "owner": { "env": "Server", "key": null, "name": "Component", "props": {}, "stack": [ [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 3461, 40, 3418, 72, ], [ "new Promise", "", 0, 0, 0, 0, ], ], }, "stack": [ [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 3456, 13, 3453, 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
Hello, World!
; } 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 && flags.enableAsyncDebugInfo, ) ) { expect(getDebugInfo(result)).toMatchInlineSnapshot(` [ { "time": 0, }, { "env": "Server", "key": null, "name": "Component", "props": {}, "stack": [ [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 3692, 19, 3673, 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.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 3692, 19, 3673, 82, ], [ "new Promise", "", 0, 0, 0, 0, ], ], }, "stack": [ [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 3676, 25, 3674, 5, ], ], "start": 0, "value": { "value": "hello", }, }, "env": "Server", "owner": { "env": "Server", "key": null, "name": "Component", "props": {}, "stack": [ [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 3692, 19, 3673, 82, ], [ "new Promise", "", 0, 0, 0, 0, ], ], }, "stack": [ [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 3676, 25, 3674, 5, ], ], }, { "time": 0, }, { "time": 0, }, { "awaited": { "end": 0, "env": "Server", "name": "Component", "owner": { "env": "Server", "key": null, "name": "Component", "props": {}, "stack": [ [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 3692, 19, 3673, 82, ], [ "new Promise", "", 0, 0, 0, 0, ], ], }, "stack": [ [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 3684, 25, 3674, 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.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 3692, 19, 3673, 82, ], [ "new Promise", "", 0, 0, 0, 0, ], ], }, "stack": [ [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 3684, 25, 3674, 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(); 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 && flags.enableAsyncDebugInfo, ) ) { 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.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 3995, 109, 3936, 87, ], ], }, { "time": 0, }, { "awaited": { "end": 0, "env": "Server", "name": "delay", "owner": { "env": "Server", "key": null, "name": "Component", "props": {}, "stack": [ [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 3995, 109, 3936, 87, ], ], }, "stack": [ [ "delay", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 87, 12, 86, 3, ], [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 3957, 21, 3956, 5, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 3983, 26, 3982, 5, ], ], "start": 0, "value": { "value": undefined, }, }, "env": "Server", "owner": { "env": "Server", "key": null, "name": "Component", "props": {}, "stack": [ [ "Object.", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 3995, 109, 3936, 87, ], ], }, "stack": [ [ "getData", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 3959, 7, 3956, 5, ], [ "Component", "/packages/react-server/src/__tests__/ReactFlightAsyncDebugInfo-test.js", 3983, 26, 3982, 5, ], ], }, { "time": 0, }, { "time": 0, }, { "awaited": { "byteSize": 0, "end": 0, "name": "rsc stream", "owner": null, "start": 0, "value": { "value": "stream", }, }, }, ] `); } }); });