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
10 changes: 10 additions & 0 deletions crates/next-napi-bindings/src/next_api/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2152,6 +2152,16 @@ pub fn project_compilation_events_subscribe(
break;
}
}
// Signal the JS side that the subscription has ended (e.g. after
// project shutdown drops all senders). This allows the async
// iterator to exit promptly instead of hanging forever.
let _ = tsfn.call(
Err(napi::Error::new(
Status::Cancelled,
"compilation events subscription closed",
)),
ThreadsafeFunctionCallMode::Blocking,
);
});

Ok(())
Expand Down
36 changes: 20 additions & 16 deletions packages/next/src/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1009,16 +1009,6 @@ export default async function build(
// Reading the config can modify environment variables that influence the bundler selection.
bundler = finalizeBundlerFromConfig(bundler)
nextBuildSpan.setAttribute('bundler', getBundlerForTelemetry(bundler))
// Install the native bindings early so we can have synchronous access later.
await installBindings(config.experimental?.useWasmBinary)

// Set up code frame renderer for error formatting
const { installCodeFrameSupport } =
require('../server/lib/install-code-frame') as typeof import('../server/lib/install-code-frame')
installCodeFrameSupport()

process.env.NEXT_DEPLOYMENT_ID = config.deploymentId || ''
NextBuildContext.config = config

let configOutDir = 'out'
if (hasCustomExportOutput(config)) {
Expand All @@ -1030,6 +1020,26 @@ export default async function build(
setGlobal('phase', PHASE_PRODUCTION_BUILD)
setGlobal('distDir', distDir)

// Check for build cache before initializing telemetry, because the
// Telemetry constructor creates the cache directory in CI environments.
const cacheDir = getCacheDir(distDir)

// Initialize telemetry before installBindings so that SWC load failure
// events are captured if native bindings fail to load.
const telemetry = new Telemetry({ distDir })
setGlobal('telemetry', telemetry)

// Install the native bindings early so we can have synchronous access later.
await installBindings(config.experimental?.useWasmBinary)

// Set up code frame renderer for error formatting
const { installCodeFrameSupport } =
require('../server/lib/install-code-frame') as typeof import('../server/lib/install-code-frame')
installCodeFrameSupport()

process.env.NEXT_DEPLOYMENT_ID = config.deploymentId || ''
NextBuildContext.config = config

const buildId = await getBuildId(
isGenerateMode,
distDir,
Expand Down Expand Up @@ -1117,12 +1127,6 @@ export default async function build(
)
}

const cacheDir = getCacheDir(distDir)

const telemetry = new Telemetry({ distDir })

setGlobal('telemetry', telemetry)

const publicDir = path.join(dir, 'public')
const { pagesDir, appDir } = findPagesDir(dir)

Expand Down
45 changes: 27 additions & 18 deletions packages/next/src/build/turbopack-build/impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,15 +146,17 @@ export async function turbopackBuild(): Promise<{
}
: undefined
)
try {
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()
backgroundLogCompilationEvents(project, {
parentSpan: buildEventsSpan,
})
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,
signal: shutdownController.signal,
})

try {
// Write an empty file in a known location to signal this was built with Turbopack
await fs.writeFile(path.join(distDir, 'turbopack'), '')

Expand Down Expand Up @@ -264,7 +266,14 @@ export async function turbopackBuild(): Promise<{
await project.writeAnalyzeData(appDirOnly)
}

const shutdownPromise = project.shutdown()
// 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 {
Expand All @@ -274,6 +283,8 @@ export async function turbopackBuild(): Promise<{
}
} catch (err) {
await project.shutdown()
shutdownController.abort()
await compilationEvents.catch(() => {})
throw err
}
}
Expand All @@ -283,9 +294,7 @@ export async function workerMain(workerData: {
buildContext: typeof NextBuildContext
traceState: TraceState & { shouldSaveTraceEvents: boolean }
}): Promise<
Omit<Awaited<ReturnType<typeof turbopackBuild>>, 'shutdownPromise'> & {
debugTraceEvents?: ReturnType<typeof getTraceEvents>
}
Omit<Awaited<ReturnType<typeof turbopackBuild>>, 'shutdownPromise'>
> {
// setup new build context from the serialized data passed from the parent
Object.assign(NextBuildContext, workerData.buildContext)
Expand Down Expand Up @@ -324,14 +333,9 @@ export async function workerMain(workerData: {
duration,
} = await turbopackBuild()
shutdownPromise = resultShutdownPromise
// Wait for shutdown to complete so that all compilation events
// (e.g. persistence trace spans) have been processed before we
// collect the saved trace events.
await shutdownPromise
return {
buildTraceContext,
duration,
debugTraceEvents: getTraceEvents(),
}
} finally {
// Always flush telemetry before worker exits (waits for async operations like setTimeout in debug mode)
Expand All @@ -341,8 +345,13 @@ export async function workerMain(workerData: {
}
}

export async function waitForShutdown(): Promise<void> {
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() }
}
26 changes: 12 additions & 14 deletions packages/next/src/build/turbopack-build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,24 +35,22 @@ async function turbopackBuildWithWorker(): ReturnType<
config: _config,
...prunedBuildContext
} = NextBuildContext
const { buildTraceContext, duration, debugTraceEvents } =
await worker.workerMain({
buildContext: prunedBuildContext,
traceState: {
...exportTraceState(),
defaultParentSpanId: nextBuildSpan.getId(),
shouldSaveTraceEvents: true,
},
})

if (debugTraceEvents) {
recordTraceEvents(debugTraceEvents)
}
const { buildTraceContext, duration } = 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(() => {
shutdownPromise: worker.waitForShutdown().then(({ debugTraceEvents }) => {
if (debugTraceEvents) {
recordTraceEvents(debugTraceEvents)
}
worker.end()
}),
buildTraceContext,
Expand Down
20 changes: 13 additions & 7 deletions packages/next/src/shared/lib/turbopack/compilation-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ export function msToNs(ms: number): bigint {
* When `parentSpan` is provided, `TraceEvent` compilation events are recorded
* as trace spans in the `.next/trace` file.
*
* The `signal` argument is partially implemented. The abort may not happen until the next
* compilation event arrives.
* Returns a promise that resolves when the subscription ends. Abort the
* `signal` to close the underlying async iterator and settle the promise
* promptly. The iterator also closes automatically when the Rust side
* drops the subscription (e.g. after project shutdown).
*/
export function backgroundLogCompilationEvents(
project: Project,
Expand All @@ -26,12 +28,16 @@ export function backgroundLogCompilationEvents(
parentSpan,
}: { eventTypes?: string[]; signal?: AbortSignal; parentSpan?: Span } = {}
): Promise<void> {
const promise = (async function () {
for await (const event of project.compilationEventsSubscribe(eventTypes)) {
if (signal?.aborted) {
return
}
const iterator = project.compilationEventsSubscribe(eventTypes)

// 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), {
once: true,
})

const promise = (async function () {
for await (const event of iterator) {
// Record TraceEvent compilation events as trace spans in .next/trace.
if (parentSpan && event.typeName === 'TraceEvent' && event.eventJson) {
try {
Expand Down
3 changes: 3 additions & 0 deletions packages/next/src/trace/report/to-json-build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ const allowlistedEvents = new Set([
'adapter-handle-build-complete',
'output-standalone',
'telemetry-flush',
'turbopack-build-events',
'turbopack-persistence',
'turbopack-compaction',
])

export default createJsonReporter({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,46 +3,7 @@ import { join } from 'path'
import { existsSync, readFileSync } from 'fs'
import { createServer } from 'http'
import { spawn } from 'child_process'
import type { TraceEvent } from 'next/dist/trace'

interface TraceStructure {
events: TraceEvent[]
eventsByName: Map<string, TraceEvent[]>
eventsById: Map<string, TraceEvent>
}

function parseTraceFile(tracePath: string): TraceStructure {
const traceContent = readFileSync(tracePath, 'utf8')
const traceLines = traceContent
.trim()
.split('\n')
.filter((line) => line.trim())

const allEvents: TraceEvent[] = []

for (const line of traceLines) {
const events = JSON.parse(line) as TraceEvent[]
allEvents.push(...events)
}

const eventsByName = new Map<string, TraceEvent[]>()
const eventsById = new Map<string, TraceEvent>()

// Index all events
for (const event of allEvents) {
if (!eventsByName.has(event.name)) {
eventsByName.set(event.name, [])
}
eventsByName.get(event.name)!.push(event)
eventsById.set(event.id.toString(), event)
}

return {
events: allEvents,
eventsByName,
eventsById,
}
}
import { parseTraceFile } from '../../../lib/parse-trace-file'

describe('enabled features in trace', () => {
const { next, isNextDev } = nextTestSetup({
Expand Down
43 changes: 2 additions & 41 deletions test/development/app-dir/hmr-trace-timing/hmr-trace-timing.test.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,7 @@
import { nextTestSetup } from 'e2e-utils'
import { join } from 'path'
import { existsSync, readFileSync } from 'fs'
import type { TraceEvent } from 'next/dist/trace'

interface TraceStructure {
events: TraceEvent[]
eventsByName: Map<string, TraceEvent[]>
eventsById: Map<string, TraceEvent>
}

function parseTraceFile(tracePath: string): TraceStructure {
const traceContent = readFileSync(tracePath, 'utf8')
const traceLines = traceContent
.trim()
.split('\n')
.filter((line) => line.trim())

const allEvents: TraceEvent[] = []

for (const line of traceLines) {
const events = JSON.parse(line) as TraceEvent[]
allEvents.push(...events)
}

const eventsByName = new Map<string, TraceEvent[]>()
const eventsById = new Map<string, TraceEvent>()

// Index all events
for (const event of allEvents) {
if (!eventsByName.has(event.name)) {
eventsByName.set(event.name, [])
}
eventsByName.get(event.name)!.push(event)
eventsById.set(event.id.toString(), event)
}

return {
events: allEvents,
eventsByName,
eventsById,
}
}
import { existsSync } from 'fs'
import { parseTraceFile } from '../../../lib/parse-trace-file'

describe('render-path tracing', () => {
const { next, isNextDev } = nextTestSetup({
Expand Down
6 changes: 5 additions & 1 deletion test/e2e/app-dir/trace-build-file/next.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {}
const nextConfig = {
experimental: {
turbopackFileSystemCacheForBuild: true,
},
}

module.exports = nextConfig
Loading
Loading