Skip to content

Commit

Permalink
Merge app renderer process (#54143)
Browse files Browse the repository at this point in the history
This PR merges the app renderer worker into the router process. This
improves the memory overhead mostly.

There're future work to do to get rid of the IPC server for router and
app renderer, as they're now merged in one process.

Fixes NEXT-1492
  • Loading branch information
shuding authored Aug 22, 2023
1 parent f0ff3c4 commit 5584e57
Show file tree
Hide file tree
Showing 14 changed files with 84 additions and 95 deletions.
8 changes: 6 additions & 2 deletions packages/next-env/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,12 @@ type Log = {

function replaceProcessEnv(sourceEnv: Env) {
Object.keys(process.env).forEach((key) => {
if (sourceEnv[key] === undefined || sourceEnv[key] === '') {
delete process.env[key]
// Allow mutating internal Next.js env variables after the server has initiated.
// This is necessary for dynamic things like the IPC server port.
if (!key.startsWith('__NEXT_PRIVATE')) {
if (sourceEnv[key] === undefined || sourceEnv[key] === '') {
delete process.env[key]
}
}
})

Expand Down
10 changes: 6 additions & 4 deletions packages/next/src/build/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1935,14 +1935,12 @@ export async function copyTracedFiles(
serverOutputPath,
`${
moduleType
? `import http from 'http'
import path from 'path'
? `import path from 'path'
import { fileURLToPath } from 'url'
const __dirname = fileURLToPath(new URL('.', import.meta.url))
import { startServer } from 'next/dist/server/lib/start-server.js'
`
: `
const http = require('http')
const path = require('path')
const { startServer } = require('next/dist/server/lib/start-server')`
}
Expand All @@ -1961,13 +1959,17 @@ if (!process.env.NEXT_MANUAL_SIG_HANDLE) {
const currentPort = parseInt(process.env.PORT, 10) || 3000
const hostname = process.env.HOSTNAME || 'localhost'
let keepAliveTimeout = parseInt(process.env.KEEP_ALIVE_TIMEOUT, 10);
let keepAliveTimeout = parseInt(process.env.KEEP_ALIVE_TIMEOUT, 10)
const nextConfig = ${JSON.stringify({
...serverConfig,
distDir: `./${path.relative(dir, distDir)}`,
})}
process.env.__NEXT_PRIVATE_STANDALONE_CONFIG = JSON.stringify(nextConfig)
process.env.__NEXT_PRIVATE_PREBUNDLED_REACT = nextConfig.experimental && nextConfig.experimental.serverActions
? 'experimental'
: 'next'
if (
Number.isNaN(keepAliveTimeout) ||
Expand Down
7 changes: 5 additions & 2 deletions packages/next/src/cli/next-dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ function watchConfigFiles(
type StartServerWorker = Worker &
Pick<typeof import('../server/lib/start-server'), 'startServer'>

async function createRouterWorker(): Promise<{
async function createRouterWorker(fullConfig: NextConfigComplete): Promise<{
worker: StartServerWorker
cleanup: () => Promise<void>
}> {
Expand All @@ -143,6 +143,9 @@ async function createRouterWorker(): Promise<{
: {}),
WATCHPACK_WATCHER_LIMIT: '20',
EXPERIMENTAL_TURBOPACK: process.env.EXPERIMENTAL_TURBOPACK,
__NEXT_PRIVATE_PREBUNDLED_REACT: !!fullConfig.experimental.serverActions
? 'experimental'
: 'next',
},
},
exposedMethods: ['startServer'],
Expand Down Expand Up @@ -394,7 +397,7 @@ const nextDev: CliCommand = async (argv) => {
} else {
const runDevServer = async (reboot: boolean) => {
try {
const workerInit = await createRouterWorker()
const workerInit = await createRouterWorker(config)
if (!!args['--experimental-https']) {
Log.warn(
'Self-signed certificates are currently an experimental feature, use at your own risk.'
Expand Down
2 changes: 0 additions & 2 deletions packages/next/src/server/base-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1040,7 +1040,6 @@ export default abstract class Server<ServerOptions extends Options = Options> {
const useInvokePath =
!useMatchedPathHeader &&
process.env.NEXT_RUNTIME !== 'edge' &&
process.env.__NEXT_PRIVATE_RENDER_WORKER &&
invokePath

if (useInvokePath) {
Expand Down Expand Up @@ -1131,7 +1130,6 @@ export default abstract class Server<ServerOptions extends Options = Options> {

if (
process.env.NEXT_RUNTIME !== 'edge' &&
process.env.__NEXT_PRIVATE_RENDER_WORKER &&
req.headers['x-middleware-invoke']
) {
const nextDataResult = await this.normalizeNextData(req, res, parsedUrl)
Expand Down
6 changes: 4 additions & 2 deletions packages/next/src/server/lib/render-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,10 @@ export async function initialize(opts: {
return result
}

const type = process.env.__NEXT_PRIVATE_RENDER_WORKER!
process.title = 'next-render-worker-' + type
const type = process.env.__NEXT_PRIVATE_RENDER_WORKER
if (type) {
process.title = 'next-render-worker-' + type
}

let requestHandler: RequestHandler
let upgradeHandler: any
Expand Down
50 changes: 29 additions & 21 deletions packages/next/src/server/lib/router-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,14 @@ import { signalFromNodeResponse } from '../web/spec-extension/adapters/next-requ

const debug = setupDebug('next:router-server:main')

export type RenderWorker = InstanceType<
typeof import('next/dist/compiled/jest-worker').Worker
> & {
initialize: typeof import('./render-server').initialize
deleteCache: typeof import('./render-server').deleteCache
deleteAppClientCache: typeof import('./render-server').deleteAppClientCache
clearModuleContext: typeof import('./render-server').clearModuleContext
propagateServerField: typeof import('./render-server').propagateServerField
}
export type RenderWorker = Pick<
typeof import('./render-server'),
| 'initialize'
| 'deleteCache'
| 'clearModuleContext'
| 'deleteAppClientCache'
| 'propagateServerField'
>

export interface RenderWorkers {
app?: Awaited<ReturnType<typeof createWorker>>
Expand Down Expand Up @@ -185,16 +184,19 @@ export async function initialize(opts: {
},
} as any)

const { initialEnv } = require('@next/env') as typeof import('@next/env')
if (!!config.experimental.appDir) {
process.env.__NEXT_PRIVATE_ROUTER_IPC_PORT = ipcPort + ''
process.env.__NEXT_PRIVATE_ROUTER_IPC_KEY = ipcValidationKey
process.env.__NEXT_PRIVATE_PREBUNDLED_REACT = config.experimental
.serverActions
? 'experimental'
: 'next'

renderWorkers.app = await createWorker(
ipcPort,
ipcValidationKey,
opts.isNodeDebugging,
'app',
config,
initialEnv
)
renderWorkers.app =
require('./render-server') as typeof import('./render-server')
}

const { initialEnv } = require('@next/env') as typeof import('@next/env')
renderWorkers.pages = await createWorker(
ipcPort,
ipcValidationKey,
Expand Down Expand Up @@ -272,10 +274,16 @@ export async function initialize(opts: {
}
}

const logError = async (
type: 'uncaughtException' | 'unhandledRejection',
err: Error | undefined
) => {
await devInstance?.logErrorWithOriginalStack(err, type)
}

const cleanup = () => {
debug('router-server process cleanup')
for (const curWorker of [
...((renderWorkers.app as any)?._workerPool?._workers || []),
...((renderWorkers.pages as any)?._workerPool?._workers || []),
] as {
_child?: import('child_process').ChildProcess
Expand All @@ -290,8 +298,8 @@ export async function initialize(opts: {
process.on('exit', cleanup)
process.on('SIGINT', cleanup)
process.on('SIGTERM', cleanup)
process.on('uncaughtException', cleanup)
process.on('unhandledRejection', cleanup)
process.on('uncaughtException', logError.bind(null, 'uncaughtException'))
process.on('unhandledRejection', logError.bind(null, 'unhandledRejection'))

const resolveRoutes = getResolveRoutes(
fsChecker,
Expand Down
4 changes: 2 additions & 2 deletions packages/next/src/server/lib/router-utils/resolve-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import type { TLSSocket } from 'tls'
import type { FsOutput } from './filesystem'
import type { IncomingMessage } from 'http'
import type { NextConfigComplete } from '../../config-shared'
import type { RenderWorker, initialize } from '../router-server'

import url from 'url'
import { Redirect } from '../../../../types'
import { RenderWorker } from '../router-server'
import setupDebug from 'next/dist/compiled/debug'
import { getCloneableBody } from '../../body-streams'
import { filterReqHeaders, ipcForbiddenHeaders } from '../server-ipc/utils'
Expand Down Expand Up @@ -46,7 +46,7 @@ export function getResolveRoutes(
ReturnType<typeof import('./filesystem').setupFsCheck>
>,
config: NextConfigComplete,
opts: Parameters<typeof import('../router-server').initialize>[0],
opts: Parameters<typeof initialize>[0],
renderWorkers: {
app?: RenderWorker
pages?: RenderWorker
Expand Down
14 changes: 5 additions & 9 deletions packages/next/src/server/lib/server-ipc/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import type NextServer from '../../next-server'
import type { NextConfigComplete } from '../../config-shared'
import type { RenderWorker } from '../router-server'

import { getNodeOptionsWithoutInspect } from '../utils'
import { errorToJSON } from '../../render'
import crypto from 'crypto'
import isError from '../../../lib/is-error'
import { genRenderExecArgv } from '../worker-utils'
import { deserializeErr } from './request-utils'
import { RenderWorker } from '../router-server'
import type { Env } from '@next/env'

// we can't use process.send as jest-worker relies on
Expand Down Expand Up @@ -115,13 +115,8 @@ export const createWorker = async (
__NEXT_PRIVATE_STANDALONE_CONFIG:
process.env.__NEXT_PRIVATE_STANDALONE_CONFIG,
NODE_ENV: process.env.NODE_ENV,
...(type === 'app'
? {
__NEXT_PRIVATE_PREBUNDLED_REACT: useServerActions
? 'experimental'
: 'next',
}
: {}),
__NEXT_PRIVATE_PREBUNDLED_REACT:
type === 'app' ? (useServerActions ? 'experimental' : 'next') : '',
...(process.env.NEXT_CPU_PROF
? { __NEXT_PRIVATE_CPU_PROFILE: `CPU.${type}-renderer` }
: {}),
Expand All @@ -135,7 +130,8 @@ export const createWorker = async (
'clearModuleContext',
'propagateServerField',
],
}) as any as RenderWorker
}) as any as RenderWorker &
InstanceType<typeof import('next/dist/compiled/jest-worker').Worker>

worker.getStderr().pipe(process.stderr)
worker.getStdout().pipe(process.stdout)
Expand Down
9 changes: 7 additions & 2 deletions packages/next/src/server/lib/start-server.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import '../node-polyfill-fetch'
import '../require-hook'

import type { IncomingMessage, ServerResponse } from 'http'

Expand Down Expand Up @@ -220,11 +221,15 @@ export async function startServer({
server.close()
process.exit(0)
}
const exception = (err: Error) => {
// This is the render worker, we keep the process alive
console.error(err)
}
process.on('exit', cleanup)
process.on('SIGINT', cleanup)
process.on('SIGTERM', cleanup)
process.on('uncaughtException', cleanup)
process.on('unhandledRejection', cleanup)
process.on('uncaughtException', exception)
process.on('unhandledRejection', exception)

const initResult = await getRequestHandlers({
dir,
Expand Down
11 changes: 1 addition & 10 deletions packages/next/src/server/next-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -491,15 +491,6 @@ export default class NextNodeServer extends BaseServer {
)
}

private streamResponseChunk(res: ServerResponse, chunk: any) {
res.write(chunk)
// When streaming is enabled, we need to explicitly
// flush the response to avoid it being buffered.
if ('flush' in res) {
;(res as any).flush()
}
}

protected async imageOptimizer(
req: NodeNextRequest,
res: NodeNextResponse,
Expand Down Expand Up @@ -1033,7 +1024,7 @@ export default class NextNodeServer extends BaseServer {
return {
finished: true,
}
} catch (_) {}
} catch {}

throw err
}
Expand Down
2 changes: 0 additions & 2 deletions test/development/basic/node-builtins.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,6 @@ createNextDescribe(
expect(parsedData.https).toBe(true)
expect(parsedData.os).toBe('\n')
expect(parsedData.path).toBe('/hello/world/test.txt')
expect(parsedData.process).toInclude('next-render-worker-app')
expect(parsedData.querystring).toBe('a=b')
expect(parsedData.stringDecoder).toBe(true)
expect(parsedData.sys).toBe(true)
Expand All @@ -164,7 +163,6 @@ createNextDescribe(
expect(parsedData.https).toBe(true)
expect(parsedData.os).toBe('\n')
expect(parsedData.path).toBe('/hello/world/test.txt')
expect(parsedData.process).toInclude('next-render-worker-app')
expect(parsedData.querystring).toBe('a=b')
expect(parsedData.stringDecoder).toBe(true)
expect(parsedData.sys).toBe(true)
Expand Down
18 changes: 6 additions & 12 deletions test/integration/disable-js/test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,31 @@
import { join } from 'path'
import cheerio from 'cheerio'
import {
nextServer,
nextBuild,
startApp,
stopApp,
renderViaHTTP,
findPort,
launchApp,
killApp,
nextStart,
} from 'next-test-utils'

const appDir = join(__dirname, '../')
let appPort
let server
let app

const context = {}

describe('disabled runtime JS', () => {
describe('production mode', () => {
beforeAll(async () => {
appPort = await findPort()

await nextBuild(appDir)
app = nextServer({
dir: join(__dirname, '../'),
dev: false,
quiet: true,
})
app = await nextStart(appDir, appPort)

server = await startApp(app)
context.appPort = appPort = server.address().port
context.appPort = appPort
})
afterAll(() => stopApp(server))
afterAll(() => killApp(app))

it('should render the page', async () => {
const html = await renderViaHTTP(appPort, '/')
Expand Down
20 changes: 7 additions & 13 deletions test/integration/future/test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,24 @@ import webdriver from 'next-webdriver'
import { join } from 'path'
import {
nextBuild,
nextServer,
startApp,
stopApp,
nextStart,
killApp,
findPort,
renderViaHTTP,
} from 'next-test-utils'

let appDir = join(__dirname, '../')
let server
let appPort
let app

describe('excludeDefaultMomentLocales', () => {
beforeAll(async () => {
appPort = await findPort()
await nextBuild(appDir)
const app = nextServer({
dir: appDir,
dev: false,
quiet: true,
})
server = await startApp(app)
appPort = server.address().port
// wait for it to start up:
app = await nextStart(appDir, appPort)
await renderViaHTTP(appPort, '/')
})
afterAll(() => stopApp(server))
afterAll(() => killApp(app))

it('should load momentjs', async () => {
const browser = await webdriver(appPort, '/')
Expand Down
Loading

0 comments on commit 5584e57

Please sign in to comment.