Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add build diagnostics for saving debug information during a build #66187

Merged
merged 9 commits into from
Jun 23, 2024
37 changes: 37 additions & 0 deletions packages/next/src/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,10 @@ import { buildDataRoute } from '../server/lib/router-utils/build-data-route'
import { collectBuildTraces } from './collect-build-traces'
import type { BuildTraceContext } from './webpack/plugins/next-trace-entrypoints-plugin'
import { formatManifest } from './manifests/formatter/format-manifest'
import {
recordFrameworkVersion,
updateBuildDiagnostics,
} from '../diagnostics/build-diagnostics'
import { getStartServerInfo, logStartInfo } from '../server/lib/app-info-log'
import type { NextEnabledDirectories } from '../server/base-server'
import { hasCustomExportOutput } from '../export/utils'
Expand Down Expand Up @@ -813,6 +817,11 @@ export default async function build(
expFeatureInfo,
})

await recordFrameworkVersion(process.env.__NEXT_VERSION as string)
await updateBuildDiagnostics({
buildStage: 'start',
})

const ignoreESLint = Boolean(config.eslint.ignoreDuringBuilds)
const shouldLint = !ignoreESLint && runLint

Expand Down Expand Up @@ -1580,6 +1589,13 @@ export default async function build(
Log.info('Creating an optimized production build ...')
traceMemoryUsage('Starting build', nextBuildSpan)

await updateBuildDiagnostics({
buildStage: 'compile',
buildOptions: {
useBuildWorker: String(useBuildWorker),
},
})

if (!isGenerateMode) {
if (turboNextBuild) {
const { duration: compilerDuration, ...rest } = await turbopackBuild()
Expand All @@ -1600,6 +1616,10 @@ export default async function build(
) {
let durationInSeconds = 0

await updateBuildDiagnostics({
buildStage: 'compile-server',
})

const serverBuildPromise = webpackBuild(useBuildWorker, [
'server',
]).then((res) => {
Expand Down Expand Up @@ -1637,6 +1657,9 @@ export default async function build(
})
if (!runServerAndEdgeInParallel) {
await serverBuildPromise
await updateBuildDiagnostics({
buildStage: 'webpack-compile-edge-server',
})
}

const edgeBuildPromise = webpackBuild(useBuildWorker, [
Expand All @@ -1650,9 +1673,16 @@ export default async function build(
})
if (runServerAndEdgeInParallel) {
await serverBuildPromise
await updateBuildDiagnostics({
buildStage: 'webpack-compile-edge-server',
})
}
await edgeBuildPromise

await updateBuildDiagnostics({
buildStage: 'webpack-compile-client',
})

await webpackBuild(useBuildWorker, ['client']).then((res) => {
durationInSeconds += res.duration
traceMemoryUsage('Finished client compilation', nextBuildSpan)
Expand Down Expand Up @@ -1687,6 +1717,9 @@ export default async function build(

// For app directory, we run type checking after build.
if (appDir && !isCompileMode && !isGenerateMode) {
await updateBuildDiagnostics({
buildStage: 'type-checking',
})
await startTypeChecking(typeCheckingOptions)
traceMemoryUsage('Finished type checking', nextBuildSpan)
}
Expand Down Expand Up @@ -2438,6 +2471,10 @@ export default async function build(
)
const hasStaticApp404 = hasApp404 && isApp404Static

await updateBuildDiagnostics({
buildStage: 'static-generation',
})

// we need to trigger automatic exporting when we have
// - static 404/500
// - getStaticProps paths
Expand Down
67 changes: 67 additions & 0 deletions packages/next/src/diagnostics/build-diagnostics.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { mkdtemp, readFile } from 'fs/promises'
import { tmpdir } from 'os'
import { join } from 'path'
import { setGlobal } from '../trace/shared'
import {
recordFrameworkVersion,
updateBuildDiagnostics,
} from './build-diagnostics'

async function readBuildDiagnostics(dir: string) {
return JSON.parse(
await readFile(join(dir, 'diagnostics', 'build-diagnostics.json'), 'utf8')
)
}

describe('build-diagnostics', () => {
it('records framework version to framework.json correctly', async () => {
const tmpDir = await mkdtemp(join(tmpdir(), 'build-diagnostics'))
setGlobal('distDir', tmpDir)

// Record the initial diagnostics and make sure it's correct.
await recordFrameworkVersion('14.2.3')
let diagnostics = JSON.parse(
await readFile(join(tmpDir, 'diagnostics', 'framework.json'), 'utf8')
)
expect(diagnostics.version).toEqual('14.2.3')
})

it('records build diagnostics to a file correctly', async () => {
const tmpDir = await mkdtemp(join(tmpdir(), 'build-diagnostics'))
setGlobal('distDir', tmpDir)

// Record the initial diagnostics and make sure it's correct.
await updateBuildDiagnostics({
buildStage: 'compile',
})
let diagnostics = await readBuildDiagnostics(tmpDir)
expect(diagnostics.buildStage).toEqual('compile')

// Add a new build option. Make sure that existing fields are preserved.
await updateBuildDiagnostics({
buildStage: 'compile-server',
buildOptions: {
useBuildWorker: String(false),
},
})
diagnostics = await readBuildDiagnostics(tmpDir)
expect(diagnostics.buildStage).toEqual('compile-server')
expect(diagnostics.buildOptions).toEqual({
useBuildWorker: 'false',
})

// Make sure that it keeps existing build options when adding a new one.
await updateBuildDiagnostics({
buildStage: 'compile-client',
buildOptions: {
experimentalBuildMode: 'compile',
},
})
diagnostics = await readBuildDiagnostics(tmpDir)
expect(diagnostics.buildStage).toEqual('compile-client')
expect(diagnostics.buildOptions).toEqual({
experimentalBuildMode: 'compile',
useBuildWorker: 'false',
})
})
})
59 changes: 59 additions & 0 deletions packages/next/src/diagnostics/build-diagnostics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { mkdir, readFile, writeFile } from 'fs/promises'
import { join } from 'path'
import { traceGlobals } from '../trace/shared'

const DIAGNOSTICS_DIR = 'diagnostics'
const DIAGNOSTICS_FILE = 'build-diagnostics.json'

interface BuildDiagnostics {
// The current stage of the build process. This should be updated as the
// build progresses so it's what stage the build was in when an error
// happened.
buildStage?: string
// Additional debug information about the configuration for the build.
buildOptions?: Record<string, string>
}

async function getDiagnosticsDir(): Promise<string> {
const distDir = traceGlobals.get('distDir')
const diagnosticsDir = join(distDir, DIAGNOSTICS_DIR)
await mkdir(diagnosticsDir, { recursive: true })
return diagnosticsDir
}

/**
* Saves the exact version of Next.js that was used to build the app to a diagnostics file.
*/
export async function recordFrameworkVersion(version: string): Promise<void> {
const diagnosticsDir = await getDiagnosticsDir()
const frameworkVersionFile = join(diagnosticsDir, 'framework.json')
await writeFile(frameworkVersionFile, JSON.stringify({ version }))
}

/**
* Saves build diagnostics information to a file. This method can be called
* multiple times during a build to save additional information that can help
* debug a build such as what stage the build was in when a failure happened.
* Each time this method is called, the new information will be merged with any
* existing build diagnostics that previously existed.
*/
export async function updateBuildDiagnostics(
diagnostics: BuildDiagnostics
): Promise<void> {
const diagnosticsDir = await getDiagnosticsDir()
const diagnosticsFile = join(diagnosticsDir, DIAGNOSTICS_FILE)

const existingDiagnostics: BuildDiagnostics = JSON.parse(
await readFile(diagnosticsFile, 'utf8').catch(() => '{}')
) as BuildDiagnostics
const updatedBuildOptions = {
...(existingDiagnostics.buildOptions ?? {}),
...(diagnostics.buildOptions ?? {}),
}
const updatedDiagnostics = {
...existingDiagnostics,
...diagnostics,
buildOptions: updatedBuildOptions,
}
await writeFile(diagnosticsFile, JSON.stringify(updatedDiagnostics, null, 2))
}
9 changes: 9 additions & 0 deletions packages/next/taskfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -2339,6 +2339,7 @@ export async function next_compile(task, opts) {
'lib_esm',
'client',
'client_esm',
'diagnostics',
'telemetry',
'trace',
'shared',
Expand Down Expand Up @@ -2581,6 +2582,13 @@ export async function trace(task, opts) {
.target('dist/trace')
}

export async function diagnostics(task, opts) {
await task
.source('src/diagnostics/**/*.+(js|ts|tsx)')
.swc('server', { dev: opts.dev })
.target('dist/diagnostics')
}

export async function build(task, opts) {
await task.serial(
['precompile', 'compile', 'generate_types', 'rewrite_compiled_references'],
Expand Down Expand Up @@ -2643,6 +2651,7 @@ export default async function (task) {
await task.watch('src/export', 'nextbuildstatic_esm', opts)
await task.watch('src/client', 'client', opts)
await task.watch('src/client', 'client_esm', opts)
await task.watch('src/diagnostics', 'diagnostics', opts)
await task.watch('src/lib', 'lib', opts)
await task.watch('src/lib', 'lib_esm', opts)
await task.watch('src/cli', 'cli', opts)
Expand Down
Loading