rsnext/test/e2e/instrumentation-hook/instrumentation-hook.test.ts
Tim Neutkens c5a8e0989e
Consolidate Server and Routing process into one process (#53523)
In the current version of Next.js there are 4 processes when running in
production:

- Server
- Routing
- Rendering Pages Router
- Rendering App Router

This setup was introduced in order to allow App Router and Pages Router
to use different versions of React (i.e. Server Actions currently
requires react@experimental to function). I wrote down more on why these
processes exist in this comment:
https://github.com/vercel/next.js/issues/49929#issuecomment-1637185156

This PR combines the Server and Routing process into one handler, as the
"Server" process was only proxying to the Routing process. In my testing
this caused about ~2x the amount of memory per request as the response
body was duplicated between the processes. This was especially visible
in the case of that memory leak in Node.js 18.16 as it grew memory usage
on both sides quickly.

In the process of going through these changes I found a couple of other
bugs like the propagation of values to the worker processes not being
awaited
([link](https://github.com/vercel/next.js/pull/53523/files#diff-0ef09f360141930bb03263b378d37d71ad9432ac851679aeabc577923536df84R54))
and the dot syntax for propagation was not functioning.

It also seemed there were a few cases where watchpack was used that
would cause many more files to be watched than expected, for now I've
removed those cases, specifically the "delete dir while running" and
instrument.js hmr (instrument.js is experimental). Those tests have been
skipped for now until we can revisit them to verfiy it

I've also cleaned up the types a bit while I was looking into these
changes.

### Improvement

⚠️ Important preface to this, measuring memory usage / peak usage is not
super reliable especially when JS gets garbage collected. These numbers
are just to show the rough percentage of less memory usage.

#### Baseline

Old:

```
next-server: 44.8MB
next-router-worker: 57.5MB
next-render-worker-app: 39,6MB
next-render-worker-pages: 39,1MB
```

New:

```
next-server: Removed
next-router-worker: 64.4MB
next-render-worker-app: 43.1MB (Note: no changes here, this shows what I meant by rough numbers)
next-render-worker-pages: 42.4MB (Note: no changes here, this shows what I meant by rough numbers)
```

Overall win: ~40MB (process is removed)

#### Peak usage

Old:

```
next-server: 118.6MB
next-router-worker: 223.7MB
next-render-worker-app: 32.8MB (I ran the test on a pages application in this case)
next-render-worker-pages: 101.1MB
```

New:

```
next-server: Removed
next-router-worker: 179.1MB
next-render-worker-app: 33.4MB
next-render-worker-pages: 117.5MB
```

Overall win: ~100MB (but it scales with requests so it was about ~50% of
next-router-worker constantly)

Related: #53523

---------

Co-authored-by: JJ Kasper <jj@jjsweb.site>
2023-08-08 16:06:32 +02:00

136 lines
4.2 KiB
TypeScript

import { createNextDescribe } from 'e2e-utils'
import { check } from 'next-test-utils'
import path from 'path'
const describeCase = (
caseName: string,
callback: Parameters<typeof createNextDescribe>[2]
) => {
createNextDescribe(
caseName,
{
files: path.join(__dirname, caseName),
nextConfig: {
experimental: {
instrumentationHook: true,
},
},
skipDeployment: true,
},
callback
)
}
describe('Instrumentation Hook', () => {
// TODO: investigate the failure with esm import
// createNextDescribe(
// 'with-esm-import',
// {
// files: path.join(__dirname, 'with-esm-import'),
// nextConfig: {
// experimental: {
// instrumentationHook: true,
// },
// },
// dependencies: {
// // This test is mostly for compatibility with this package
// '@vercel/otel': 'latest',
// },
// skipDeployment: true,
// },
// ({ next }) => {
// eslint-disable-next-line jest/no-commented-out-tests
// it('with-esm-import should run the instrumentation hook', async () => {
// await next.render('/')
// await check(
// () => next.cliOutput,
// /register in instrumentation\.js is running/
// )
// })
// }
// )
describeCase('with-middleware', ({ next }) => {
it('with-middleware should run the instrumentation hook', async () => {
await next.render('/')
await check(() => next.cliOutput, /instrumentation hook on the edge/)
})
})
describeCase('with-edge-api', ({ next }) => {
it('with-edge-api should run the instrumentation hook', async () => {
await next.render('/api')
await check(() => next.cliOutput, /instrumentation hook on the edge/)
})
})
describeCase('with-edge-page', ({ next }) => {
it('with-edge-page should run the instrumentation hook', async () => {
await next.render('/')
await check(() => next.cliOutput, /instrumentation hook on the edge/)
})
})
describeCase('with-node-api', ({ next }) => {
it('with-node-api should run the instrumentation hook', async () => {
await next.render('/api')
await check(() => next.cliOutput, /instrumentation hook on nodejs/)
})
})
describeCase('with-node-page', ({ next }) => {
it('with-node-page should run the instrumentation hook', async () => {
await next.render('/')
await check(() => next.cliOutput, /instrumentation hook on nodejs/)
})
})
describeCase('with-async-node-page', ({ next }) => {
it('with-async-node-page should run the instrumentation hook', async () => {
const page = await next.render('/')
expect(page).toContain('Node - finished: true')
})
})
describeCase('with-async-edge-page', ({ next }) => {
it('with-async-edge-page should run the instrumentation hook', async () => {
const page = await next.render('/')
expect(page).toContain('Edge - finished: true')
})
})
describeCase('general', ({ next, isNextDev }) => {
it('should not overlap with a instrumentation page', async () => {
const page = await next.render('/instrumentation')
expect(page).toContain('Hello')
})
if (isNextDev) {
// TODO: Implement handling for changing the instrument file.
it.skip('should reload the server when the instrumentation hook changes', async () => {
await next.render('/')
await next.patchFile(
'./instrumentation.js',
`export function register() {console.log('toast')}`
)
await check(() => next.cliOutput, /toast/)
await next.renameFile(
'./instrumentation.js',
'./instrumentation.js.bak'
)
await check(
() => next.cliOutput,
/The instrumentation file has been removed/
)
await next.patchFile(
'./instrumentation.js.bak',
`export function register() {console.log('bread')}`
)
await next.renameFile(
'./instrumentation.js.bak',
'./instrumentation.js'
)
await check(() => next.cliOutput, /The instrumentation file was added/)
await check(() => next.cliOutput, /bread/)
})
}
})
})