Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 3 additions & 9 deletions packages/next/src/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1208,7 +1208,7 @@ export default async function build(
if (experimentalBuildMode === 'generate-env') {
if (bundler === Bundler.Turbopack) {
Log.warn('generate-env is not needed with turbopack')
process.exit(0)
return
}
Log.info('Inlining static env ...')
await nextBuildSpan
Expand All @@ -1221,9 +1221,7 @@ export default async function build(
})

Log.info('Complete')
flushAllTraces()
teardownTraceSubscriber()
process.exit(0)
return
}

// when using compile mode static env isn't inlined so we
Expand Down Expand Up @@ -1793,11 +1791,7 @@ export default async function build(
shutdownPromise: p,
warnings,
...rest
} = await turbopackBuild(
process.env.NEXT_TURBOPACK_USE_WORKER === undefined ||
process.env.NEXT_TURBOPACK_USE_WORKER !== '0',
telemetry
)
} = await turbopackBuild(telemetry)
shutdownPromise = p
deferredTurbopackWarnings = warnings
traceMemoryUsage('Finished build', nextBuildSpan)
Expand Down
115 changes: 16 additions & 99 deletions packages/next/src/build/turbopack-build/impl.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
// Import cpu-profile first to start profiling early if enabled
import { saveCpuProfile } from '../../server/lib/cpu-profile'
import path from 'path'
import { validateTurboNextConfig } from '../../lib/turbopack-warning'
import { seedTurbopackCacheIfNeeded } from '../../lib/turbopack-cache-seed'
import { NextBuildContext } from '../build-context'
import { createDefineEnv, getBindingsSync } from '../swc'
import { installBindings } from '../swc/install-bindings'
import {
handleRouteType,
rawEntrypointsToEntrypoints,
Expand All @@ -14,16 +11,8 @@ import { TurbopackManifestLoader } from '../../shared/lib/turbopack/manifest-loa
import { promises as fs } from 'fs'
import { PHASE_PRODUCTION_BUILD } from '../../shared/lib/constants'
import loadConfig from '../../server/config'
import { hasCustomExportOutput } from '../../export/utils'
import { Telemetry } from '../../telemetry/storage'
import type { Telemetry } from '../../telemetry/storage'
import { eventBuildFeatureUsageFromTurbopack } from '../../telemetry/events/build'
import {
setGlobal,
trace,
initializeTraceState,
getTraceEvents,
} from '../../trace'
import type { TraceState } from '../../trace'
import { isCI } from '../../server/ci-info'
import { backgroundLogCompilationEvents } from '../../shared/lib/turbopack/compilation-events'
import { getSupportedBrowsers } from '../get-supported-browsers'
Expand Down Expand Up @@ -157,15 +146,23 @@ export async function turbopackBuild(telemetry: Telemetry): Promise<{
}
: undefined
)
const buildEventsSpan = trace('turbopack-build-events')
// Stop immediately: this span is only used as a parent for
// manualTraceChild calls which carry their own timestamps.
buildEventsSpan.stop()
const shutdownController = new AbortController()
const compilationEvents = backgroundLogCompilationEvents(project, {
parentSpan: buildEventsSpan,
// Compilation events carry their own timestamps, so they hang directly off
// the build rather than a synthetic grouping span.
parentSpan: NextBuildContext.nextBuildSpan,
signal: shutdownController.signal,
})
const runShutdown = async () => {
// Shutdown may trigger final compilation events (e.g. persistence,
// compaction trace spans). This is the last chance to capture them.
// After shutdown resolves we abort the signal to close the iterator
// and drain any remaining buffered events.

await project.shutdown()
shutdownController.abort()
await compilationEvents
}

try {
// Write an empty file in a known location to signal this was built with Turbopack
Expand Down Expand Up @@ -295,95 +292,15 @@ export async function turbopackBuild(telemetry: Telemetry): Promise<{
await project.writeAnalyzeData(appDirOnly)
}

// Shutdown may trigger final compilation events (e.g. persistence,
// compaction trace spans). This is the last chance to capture them.
// After shutdown resolves we abort the signal to close the iterator
// and drain any remaining buffered events.
const shutdownPromise = project.shutdown().then(() => {
shutdownController.abort()
return compilationEvents.catch(() => {})
})

const time = process.hrtime(startTime)
return {
duration: time[0] + time[1] / 1e9,
buildTraceContext: undefined,
shutdownPromise,
shutdownPromise: runShutdown(),
warnings,
}
} catch (err) {
await project.shutdown()
shutdownController.abort()
await compilationEvents.catch(() => {})
await runShutdown()
throw err
}
}

let shutdownPromise: Promise<void> | undefined
export async function workerMain(workerData: {
buildContext: typeof NextBuildContext
traceState: TraceState & { shouldSaveTraceEvents: boolean }
}): Promise<
Omit<Awaited<ReturnType<typeof turbopackBuild>>, 'shutdownPromise'>
> {
// setup new build context from the serialized data passed from the parent
Object.assign(NextBuildContext, workerData.buildContext)
initializeTraceState(workerData.traceState)

/// load the config because it's not serializable
const config = await loadConfig(
PHASE_PRODUCTION_BUILD,
NextBuildContext.dir!,
{
debugPrerender: NextBuildContext.debugPrerender,
reactProductionProfiling: NextBuildContext.reactProductionProfiling,
bundler: Bundler.Turbopack,
}
)
NextBuildContext.config = config
// Matches handling in build/index.ts
// https://github.com/vercel/next.js/blob/84f347fc86f4efc4ec9f13615c215e4b9fb6f8f0/packages/next/src/build/index.ts#L815-L818
// Ensures the `config.distDir` option is matched.
if (hasCustomExportOutput(NextBuildContext.config)) {
NextBuildContext.config.distDir = '.next'
}

// Clone the telemetry for worker
const telemetry = new Telemetry({
distDir: NextBuildContext.config.distDir,
})
setGlobal('telemetry', telemetry)
// Install bindings early so we can access synchronously later
await installBindings(config.experimental?.useWasmBinary)

try {
const {
shutdownPromise: resultShutdownPromise,
buildTraceContext,
duration,
warnings,
} = await turbopackBuild(telemetry)
shutdownPromise = resultShutdownPromise
return {
buildTraceContext,
duration,
warnings,
}
} finally {
// Always flush telemetry before worker exits (waits for async operations like setTimeout in debug mode)
await telemetry.flush()
// Save CPU profile before worker exits
await saveCpuProfile()
}
}

export async function waitForShutdown(): Promise<{
debugTraceEvents?: ReturnType<typeof getTraceEvents>
}> {
if (shutdownPromise) {
await shutdownPromise
}
// Collect trace events after shutdown completes so that all compilation
// events (e.g. persistence trace spans) have been processed.
return { debugTraceEvents: getTraceEvents() }
}
93 changes: 5 additions & 88 deletions packages/next/src/build/turbopack-build/index.ts
Original file line number Diff line number Diff line change
@@ -1,95 +1,12 @@
import path from 'path'

import { Worker } from '../../lib/worker'
import { NextBuildContext } from '../build-context'
import { exportTraceState, recordTraceEvents } from '../../trace'
import type { Telemetry } from '../../telemetry/storage'

async function turbopackBuildWithWorker(): ReturnType<
typeof import('./impl').turbopackBuild
> {
const nextBuildSpan = NextBuildContext.nextBuildSpan!
try {
const worker = new Worker(path.join(__dirname, 'impl.js'), {
exposedMethods: ['workerMain', 'waitForShutdown'],
enableWorkerThreads: true,
debuggerPortOffset: -1,
isolatedMemory: false,
numWorkers: 1,
maxRetries: 0,
forkOptions: {
env: {
NEXT_PRIVATE_BUILD_WORKER: '1',
...(process.env.NEXT_CPU_PROF
? {
NEXT_CPU_PROF: '1',
NEXT_CPU_PROF_DIR: process.env.NEXT_CPU_PROF_DIR,
__NEXT_PRIVATE_CPU_PROFILE: 'build-turbopack',
}
: undefined),
},
},
}) as Worker & typeof import('./impl')
const {
nextBuildSpan: _nextBuildSpan,
// Config is not serializable and is loaded in the worker.
config: _config,
...prunedBuildContext
} = NextBuildContext
const { buildTraceContext, duration, warnings } = await worker.workerMain({
buildContext: prunedBuildContext,
traceState: {
...exportTraceState(),
defaultParentSpanId: nextBuildSpan.getId(),
shouldSaveTraceEvents: true,
},
})

return {
// destroy worker when Turbopack has shutdown so it's not sticking around using memory
// We need to wait for shutdown to make sure filesystem cache is flushed
shutdownPromise: worker.waitForShutdown().then(({ debugTraceEvents }) => {
if (debugTraceEvents) {
recordTraceEvents(debugTraceEvents)
}
worker.end()
}),
buildTraceContext,
duration,
warnings,
}
} catch (err: any) {
// When the error is a serialized `Error` object we need to recreate the `Error` instance
// in order to keep the consistent error reporting behavior.
if (err.type === 'Error') {
const error = new Error(err.message)
if (err.name) {
error.name = err.name
}
if (err.cause) {
error.cause = err.cause
}
error.message = err.message
error.stack = err.stack
throw error
}
throw err
}
}
import { turbopackBuild as turbopackBuildImpl } from './impl'

export function turbopackBuild(
withWorker: boolean,
telemetry: Telemetry
): ReturnType<typeof import('./impl').turbopackBuild> {
): ReturnType<typeof turbopackBuildImpl> {
const nextBuildSpan = NextBuildContext.nextBuildSpan!
return nextBuildSpan.traceChild('run-turbopack').traceAsyncFn(async () => {
if (withWorker) {
// Worker creates its own Telemetry instance; no need to forward.
return await turbopackBuildWithWorker()
} else {
const build = (require('./impl') as typeof import('./impl'))
.turbopackBuild
return await build(telemetry)
}
})
return nextBuildSpan
.traceChild('run-turbopack')
.traceAsyncFn(() => turbopackBuildImpl(telemetry))
}
15 changes: 8 additions & 7 deletions packages/next/src/shared/lib/turbopack/compilation-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ export function backgroundLogCompilationEvents(
): Promise<void> {
const iterator = project.compilationEventsSubscribe(eventTypes)

// If there is no signal assume there will be no clean shutdown,
// to ensure trace spans aren't lost just flush after each one.
const flushEachTraceEvent = signal === undefined

// Close the iterator as soon as the signal fires so the for-await loop
// exits without waiting for the next compilation event.
signal?.addEventListener('abort', () => iterator.return?.(undefined as any), {
Expand All @@ -49,11 +53,9 @@ export function backgroundLogCompilationEvents(
Object.fromEntries(data.attributes ?? [])
)
traceMemoryUsage(data.name, parentSpan)
// We flush after each event to make sure it makes it to disk. These events are rare and
// tend to happen at the very end of a build so to make sure they are logged we need to
// flush.
// NOTE: in a `next build` environment where we are reporting events to the parent thread, this is a no-op.
flushAllTraces()
if (flushEachTraceEvent) {
flushAllTraces()
}
} catch {}
continue // don't log these events, they just go to the trace file
}
Expand Down Expand Up @@ -83,6 +85,5 @@ export function backgroundLogCompilationEvents(
}
})()
// Prevent unhandled rejection if the subscription errors after the project shuts down.
promise.catch(() => {})
return promise
return promise.catch(() => {})
}
1 change: 0 additions & 1 deletion packages/next/src/trace/report/to-json-build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ const allowlistedEvents = new Set([
'adapter-handle-build-complete',
'output-standalone',
'telemetry-flush',
'turbopack-build-events',
'turbopack-persistence',
'turbopack-compaction',
])
Expand Down
4 changes: 3 additions & 1 deletion test/e2e/app-dir/trace-build-file/trace-build-file.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ describe('trace-build-file', () => {
}

if (process.env.IS_TURBOPACK_TEST) {
// Compaction only runs when it is due, so it may or may not appear.
foundEvents.delete('turbopack-compaction')

expect([...foundEvents].sort()).toMatchInlineSnapshot(`
[
"next-build",
Expand All @@ -86,7 +89,6 @@ describe('trace-build-file', () => {
"static-check",
"static-generation",
"telemetry-flush",
"turbopack-build-events",
"turbopack-persistence",
]
`)
Expand Down
8 changes: 3 additions & 5 deletions test/e2e/cpu-profiling/cpu-profiling-build.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,9 @@ describe('CPU Profiling - next build', () => {
expect(cpuProfiles.some((f) => f.startsWith('build-main-'))).toBe(true)

if (isTurbopack) {
// Turbopack mode generates: build-main, build-turbopack
expect(cpuProfiles.length).toBe(2)
expect(cpuProfiles.some((f) => f.startsWith('build-turbopack-'))).toBe(
true
)
// Turbopack builds in the main build process, so `build-main` is the only
// profile.
expect(cpuProfiles.length).toBe(1)
} else {
// Webpack mode generates: build-main, build-webpack-client, build-webpack-server, build-webpack-edge-server
expect(cpuProfiles.length).toBe(4)
Expand Down
Loading
Loading