From b8ef91eda86b5e9d97bc0b8cc51d9ab10dd5adb8 Mon Sep 17 00:00:00 2001 From: Dale Bustad Date: Mon, 1 Mar 2021 23:21:54 -0800 Subject: [PATCH 01/16] Replace OpenTelemetry with custom tracing. --- packages/next/telemetry/trace/index.ts | 65 +++++++++++++++++++ packages/next/telemetry/trace/to-console.ts | 9 +++ packages/next/telemetry/trace/to-localhost.ts | 9 +++ packages/next/telemetry/trace/to-telemetry.ts | 9 +++ 4 files changed, 92 insertions(+) create mode 100644 packages/next/telemetry/trace/index.ts create mode 100644 packages/next/telemetry/trace/to-console.ts create mode 100644 packages/next/telemetry/trace/to-localhost.ts create mode 100644 packages/next/telemetry/trace/to-telemetry.ts diff --git a/packages/next/telemetry/trace/index.ts b/packages/next/telemetry/trace/index.ts new file mode 100644 index 0000000000000..c55a1ddb74607 --- /dev/null +++ b/packages/next/telemetry/trace/index.ts @@ -0,0 +1,65 @@ +import { randomBytes } from 'crypto' +import reportToConsole from './to-console' +import reportToLocalHost from './to-localhost' +import reportToTelemetry from './to-telemetry' + +let idCounter = 0 +const idUniqToProcess = randomBytes(16).toString('base64').slice(0, 22) +const getId = () => `${idUniqToProcess}:${idCounter++}` + +const noop = ( + _spanName: string, + _duration: number, + _id: string | null, + _parentId: string | null, + _attrs: Object +) => {} + +enum TARGET { + CONSOLE = 'CONSOLE', + LOCALHOST = 'LOCALHOST', + TELEMETRY = 'TELEMETRY', +} + +const target = + process.env.TRACE_TARGET && process.env.TRACE_TARGET in TARGET + ? TARGET[process.env.TRACE_TARGET as TARGET] + : TARGET.TELEMETRY +const traceLevel = process.env.TRACE_LEVEL + ? Number.parseInt(process.env.TRACE_LEVEL) + : 1 + +let report = noop +if (target === TARGET.CONSOLE) { + report = reportToConsole +} else if (target === TARGET.LOCALHOST) { + report = reportToLocalHost +} else { + report = reportToTelemetry +} + +// This function reports durations in microseconds. This gives 1000x +// the precision of something like Date.now(), which reports in +// milliseconds. Additionally, ~285 years can be safely represented +// as microseconds as a float64 in both JSON and JavaScript. +const trace = (spanName: string, parentId: string | null, attrs: Object) => { + const endSpan = () => { + const id = getId() + const end: bigint = process.hrtime.bigint() + const duration = end - start + if (duration > Number.MAX_SAFE_INTEGER) { + throw new Error(`Duration is too long to express as float64: ${duration}`) + } + report(spanName, Number(duration), id, parentId, attrs) + } + + const start: bigint = process.hrtime.bigint() + return endSpan +} + +module.exports = { + TARGET, + primary: traceLevel >= 1 ? trace : noop, + secondary: traceLevel >= 2 ? trace : noop, + sensitive: traceLevel >= 3 ? trace : noop, +} diff --git a/packages/next/telemetry/trace/to-console.ts b/packages/next/telemetry/trace/to-console.ts new file mode 100644 index 0000000000000..c77c25785dde2 --- /dev/null +++ b/packages/next/telemetry/trace/to-console.ts @@ -0,0 +1,9 @@ +const reportToConsole = ( + _spanName: string, + _duration: number, + _id: string | null, + _parentId: string | null, + _attrs: Object +) => {} + +export default reportToConsole diff --git a/packages/next/telemetry/trace/to-localhost.ts b/packages/next/telemetry/trace/to-localhost.ts new file mode 100644 index 0000000000000..115917aab0ec7 --- /dev/null +++ b/packages/next/telemetry/trace/to-localhost.ts @@ -0,0 +1,9 @@ +const reportToLocalHost = ( + _spanName: string, + _duration: number, + _id: string | null, + _parentId: string | null, + _attrs: Object +) => {} + +export default reportToLocalHost diff --git a/packages/next/telemetry/trace/to-telemetry.ts b/packages/next/telemetry/trace/to-telemetry.ts new file mode 100644 index 0000000000000..371a9f34f7ba8 --- /dev/null +++ b/packages/next/telemetry/trace/to-telemetry.ts @@ -0,0 +1,9 @@ +const reportToTelemetry = ( + _spanName: string, + _duration: number, + _id: string | null, + _parentId: string | null, + _attrs: Object +) => {} + +export default reportToTelemetry From df6768a02c313f3a2cda0ca41589a103d212ffa7 Mon Sep 17 00:00:00 2001 From: Dale Bustad Date: Tue, 2 Mar 2021 20:52:45 -0800 Subject: [PATCH 02/16] Replace OpenTelemetry tracing throughout Next. --- bench/instrument.js | 39 -- package.json | 5 - packages/next/bin/next.ts | 14 - packages/next/build/index.ts | 401 +++++------ packages/next/build/tracer.ts | 100 --- packages/next/build/utils.ts | 238 +++---- .../webpack/loaders/babel-loader/src/cache.js | 26 +- .../webpack/loaders/babel-loader/src/index.js | 258 ++++--- .../loaders/next-client-pages-loader.ts | 38 +- .../loaders/next-serverless-loader/index.ts | 6 +- .../webpack/plugins/build-manifest-plugin.ts | 253 +++---- .../webpack/plugins/build-stats-plugin.ts | 75 +- .../webpack/plugins/css-minimizer-plugin.ts | 145 ++-- .../build/webpack/plugins/profiling-plugin.ts | 40 +- .../terser-webpack-plugin/src/index.js | 307 ++++---- packages/next/export/index.ts | 90 +-- packages/next/export/worker.ts | 661 +++++++++--------- packages/next/telemetry/trace/autoparent.ts | 71 ++ packages/next/telemetry/trace/index.ts | 79 +-- packages/next/telemetry/trace/report/index.ts | 26 + .../next/telemetry/trace/report/to-console.ts | 25 + .../trace/{ => report}/to-localhost.ts | 4 +- .../trace/{ => report}/to-telemetry.ts | 4 +- packages/next/telemetry/trace/to-console.ts | 9 - packages/next/telemetry/trace/trace.ts | 77 ++ packages/next/telemetry/trace/types.ts | 7 + 26 files changed, 1459 insertions(+), 1539 deletions(-) delete mode 100644 bench/instrument.js delete mode 100644 packages/next/build/tracer.ts create mode 100644 packages/next/telemetry/trace/autoparent.ts create mode 100644 packages/next/telemetry/trace/report/index.ts create mode 100644 packages/next/telemetry/trace/report/to-console.ts rename packages/next/telemetry/trace/{ => report}/to-localhost.ts (74%) rename packages/next/telemetry/trace/{ => report}/to-telemetry.ts (74%) delete mode 100644 packages/next/telemetry/trace/to-console.ts create mode 100644 packages/next/telemetry/trace/trace.ts create mode 100644 packages/next/telemetry/trace/types.ts diff --git a/bench/instrument.js b/bench/instrument.js deleted file mode 100644 index 38176de31cbeb..0000000000000 --- a/bench/instrument.js +++ /dev/null @@ -1,39 +0,0 @@ -// Disable automatic instrumentation -process.env.OTEL_NO_PATCH_MODULES = '*' - -const { NodeTracerProvider } = require('@opentelemetry/node') -const { SimpleSpanProcessor } = require('@opentelemetry/tracing') -const { ZipkinExporter } = require('@opentelemetry/exporter-zipkin') - -const tracerProvider = new NodeTracerProvider({ - // All automatic instrumentation plugins have to be disabled as it affects worker_thread/child_process bootup performance - plugins: { - mongodb: { enabled: false, path: '@opentelemetry/plugin-mongodb' }, - grpc: { enabled: false, path: '@opentelemetry/plugin-grpc' }, - '@grpc/grpc-js': { enabled: false, path: '@opentelemetry/plugin-grpc-js' }, - http: { enabled: false, path: '@opentelemetry/plugin-http' }, - https: { enabled: false, path: '@opentelemetry/plugin-https' }, - mysql: { enabled: false, path: '@opentelemetry/plugin-mysql' }, - pg: { enabled: false, path: '@opentelemetry/plugin-pg' }, - redis: { enabled: false, path: '@opentelemetry/plugin-redis' }, - ioredis: { enabled: false, path: '@opentelemetry/plugin-ioredis' }, - 'pg-pool': { enabled: false, path: '@opentelemetry/plugin-pg-pool' }, - express: { enabled: false, path: '@opentelemetry/plugin-express' }, - '@hapi/hapi': { - enabled: false, - path: '@opentelemetry/hapi-instrumentation', - }, - koa: { enabled: false, path: '@opentelemetry/koa-instrumentation' }, - dns: { enabled: false, path: '@opentelemetry/plugin-dns' }, - }, -}) - -tracerProvider.addSpanProcessor( - new SimpleSpanProcessor( - new ZipkinExporter({ - serviceName: 'next-js', - }) - ) -) - -tracerProvider.register() diff --git a/package.json b/package.json index 40bfcb93cfb7e..f795c568cf8ff 100644 --- a/package.json +++ b/package.json @@ -45,11 +45,6 @@ "@babel/preset-react": "7.12.10", "@fullhuman/postcss-purgecss": "1.3.0", "@mdx-js/loader": "0.18.0", - "@opentelemetry/exporter-zipkin": "0.14.0", - "@opentelemetry/node": "0.14.0", - "@opentelemetry/plugin-http": "0.14.0", - "@opentelemetry/plugin-https": "0.14.0", - "@opentelemetry/tracing": "0.14.0", "@testing-library/react": "11.2.5", "@types/cheerio": "0.22.16", "@types/fs-extra": "8.1.0", diff --git a/packages/next/bin/next.ts b/packages/next/bin/next.ts index bd84e06b5b81c..c97020cb8bc5e 100755 --- a/packages/next/bin/next.ts +++ b/packages/next/bin/next.ts @@ -2,7 +2,6 @@ import * as log from '../build/output/log' import arg from 'next/dist/compiled/arg/index.js' import { NON_STANDARD_NODE_ENV } from '../lib/constants' -import opentelemetryApi from '@opentelemetry/api' ;['react', 'react-dom'].forEach((dependency) => { try { // When 'npm link' is used it checks the clone location. Not the project. @@ -107,19 +106,6 @@ if (typeof React.Suspense === 'undefined') { process.on('SIGTERM', () => process.exit(0)) process.on('SIGINT', () => process.exit(0)) -commands[command]() - .then((exec) => exec(forwardedArgs)) - .then(async () => { - if (command === 'build') { - // @ts-ignore getDelegate exists - const tp = opentelemetryApi.trace.getTracerProvider().getDelegate() - if (tp.shutdown) { - await tp.shutdown() - } - process.exit(0) - } - }) - if (command === 'dev') { const { CONFIG_FILE } = require('../next-server/lib/constants') const { watchFile } = require('fs') diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index 5957660eea408..6082f8297d229 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -70,7 +70,7 @@ import { generateBuildId } from './generate-build-id' import { isWriteable } from './is-writeable' import * as Log from './output/log' import createSpinner from './spinner' -import { traceAsyncFn, traceFn, tracer } from './tracer' +import { trace } from '../telemetry/trace' import { collectPages, detectConflictingPaths, @@ -86,7 +86,6 @@ import { import getBaseWebpackConfig from './webpack-config' import { PagesManifest } from './webpack/plugins/pages-manifest-plugin' import { writeBuildId } from './write-build-id' -import opentelemetryApi from '@opentelemetry/api' import { normalizeLocalePath } from '../next-server/lib/i18n/normalize-locale-path' const staticCheckWorker = require.resolve('./utils') @@ -118,31 +117,26 @@ export default async function build( reactProductionProfiling = false, debugOutput = false ): Promise { - const span = tracer.startSpan('next-build') - return traceAsyncFn(span, async () => { + const nextBuildSpan = trace('next-build') + + return nextBuildSpan.traceAsyncFn(async () => { // attempt to load global env values so they are available in next.config.js - const { loadedEnvFiles } = traceFn(tracer.startSpan('load-dotenv'), () => - loadEnvConfig(dir, false, Log) - ) + const { loadedEnvFiles } = nextBuildSpan + .traceChild('load-dotenv') + .traceFn(() => loadEnvConfig(dir, false, Log)) - const config = await traceAsyncFn( - tracer.startSpan('load-next-config'), - () => loadConfig(PHASE_PRODUCTION_BUILD, dir, conf) - ) + const config = await nextBuildSpan + .traceChild('load-next-config') + .traceAsyncFn(() => loadConfig(PHASE_PRODUCTION_BUILD, dir, conf)) const { target } = config - const buildId = await traceAsyncFn( - tracer.startSpan('generate-buildid'), - () => generateBuildId(config.generateBuildId, nanoid) - ) + const buildId = await nextBuildSpan + .traceChild('generate-buildid') + .traceAsyncFn(() => generateBuildId(config.generateBuildId, nanoid)) const distDir = path.join(dir, config.distDir) - const { - headers, - rewrites, - redirects, - } = await traceAsyncFn(tracer.startSpan('load-custom-routes'), () => - loadCustomRoutes(config) - ) + const { headers, rewrites, redirects } = await nextBuildSpan + .traceChild('load-custom-routes') + .traceAsyncFn(() => loadCustomRoutes(config)) if (ciEnvironment.isCI && !ciEnvironment.hasNextSupport) { const cacheDir = path.join(distDir, 'cache') @@ -181,16 +175,17 @@ export default async function build( ) const ignoreTypeScriptErrors = Boolean(config.typescript?.ignoreBuildErrors) - await traceAsyncFn(tracer.startSpan('verify-typescript-setup'), () => - verifyTypeScriptSetup(dir, pagesDir, !ignoreTypeScriptErrors) - ) + await nextBuildSpan + .traceChild('verify-typescript-setup') + .traceAsyncFn(() => + verifyTypeScriptSetup(dir, pagesDir, !ignoreTypeScriptErrors) + ) const isLikeServerless = isTargetLikeServerless(target) - const pagePaths: string[] = await traceAsyncFn( - tracer.startSpan('collect-pages'), - () => collectPages(pagesDir, config.pageExtensions) - ) + const pagePaths: string[] = await nextBuildSpan + .traceChild('collect-pages') + .traceAsyncFn(() => collectPages(pagesDir, config.pageExtensions)) // needed for static exporting since we want to replace with HTML // files @@ -203,19 +198,21 @@ export default async function build( previewModeEncryptionKey: crypto.randomBytes(32).toString('hex'), } - const mappedPages = traceFn(tracer.startSpan('create-pages-mapping'), () => - createPagesMapping(pagePaths, config.pageExtensions) - ) - const entrypoints = traceFn(tracer.startSpan('create-entrypoints'), () => - createEntrypoints( - mappedPages, - target, - buildId, - previewProps, - config, - loadedEnvFiles + const mappedPages = nextBuildSpan + .traceChild('create-pages-mapping') + .traceFn(() => createPagesMapping(pagePaths, config.pageExtensions)) + const entrypoints = nextBuildSpan + .traceChild('create-entrypoints') + .traceFn(() => + createEntrypoints( + mappedPages, + target, + buildId, + previewProps, + config, + loadedEnvFiles + ) ) - ) const pageKeys = Object.keys(mappedPages) const conflictingPublicFiles: string[] = [] const hasCustomErrorPage = mappedPages['/_error'].startsWith( @@ -235,9 +232,9 @@ export default async function build( } } - await traceAsyncFn( - tracer.startSpan('public-dir-conflict-check'), - async () => { + await nextBuildSpan + .traceChild('public-dir-conflict-check') + .traceAsyncFn(async () => { // Check if pages conflict with files in `public` // Only a page of public file can be served, not both. for (const page in mappedPages) { @@ -261,8 +258,7 @@ export default async function build( )}` ) } - } - ) + }) const nestedReservedPages = pageKeys.filter((page) => { return ( @@ -339,7 +335,7 @@ export default async function build( defaultLocale: string localeDetection?: false } - } = traceFn(tracer.startSpan('generate-routes-manifest'), () => ({ + } = nextBuildSpan.traceChild('generate-routes-manifest').traceFn(() => ({ version: 3, pages404: true, basePath: config.basePath, @@ -361,9 +357,9 @@ export default async function build( i18n: config.i18n || undefined, })) - const distDirCreated = await traceAsyncFn( - tracer.startSpan('create-distdir'), - async () => { + const distDirCreated = await nextBuildSpan + .traceChild('create-dist-dir') + .traceAsyncFn(async () => { try { await promises.mkdir(distDir, { recursive: true }) return true @@ -373,8 +369,7 @@ export default async function build( } throw err } - } - ) + }) if (!distDirCreated || !(await isWriteable(distDir))) { throw new Error( @@ -384,13 +379,15 @@ export default async function build( // We need to write the manifest with rewrites before build // so serverless can import the manifest - await traceAsyncFn(tracer.startSpan('write-routes-manifest'), () => - promises.writeFile( - routesManifestPath, - JSON.stringify(routesManifest), - 'utf8' + await nextBuildSpan + .traceChild('write-routes-manifest') + .traceAsyncFn(() => + promises.writeFile( + routesManifestPath, + JSON.stringify(routesManifest), + 'utf8' + ) ) - ) const manifestPath = path.join( distDir, @@ -398,9 +395,9 @@ export default async function build( PAGES_MANIFEST ) - const requiredServerFiles = traceFn( - tracer.startSpan('generate-required-server-files'), - () => ({ + const requiredServerFiles = nextBuildSpan + .traceChild('generate-required-server-files') + .traceFn(() => ({ version: 1, config: { ...config, @@ -425,12 +422,11 @@ export default async function build( .filter(nonNullable) .map((file) => path.join(config.distDir, file)), ignore: [] as string[], - }) - ) + })) - const configs = await traceAsyncFn( - tracer.startSpan('generate-webpack-config'), - () => + const configs = await nextBuildSpan + .traceChild('generate-webpack-config') + .traceAsyncFn(() => Promise.all([ getBaseWebpackConfig(dir, { buildId, @@ -453,7 +449,7 @@ export default async function build( rewrites, }), ]) - ) + ) const clientConfig = configs[0] @@ -488,10 +484,9 @@ export default async function build( } } } else { - result = await traceAsyncFn( - tracer.startSpan('run-webpack-compiler'), - () => runCompiler(configs) - ) + result = await nextBuildSpan + .traceChild('run-webpack-compiler') + .traceAsyncFn(() => runCompiler(configs)) } const webpackBuildEnd = process.hrtime(webpackBuildStart) @@ -499,9 +494,9 @@ export default async function build( buildSpinner.stopAndPersist() } - result = traceFn(tracer.startSpan('format-webpack-messages'), () => - formatWebpackMessages(result) - ) + result = nextBuildSpan + .traceChild('format-webpack-messages') + .traceFn(() => formatWebpackMessages(result)) if (result.errors.length > 0) { // Only keep the first error. Others are often indicative @@ -582,9 +577,8 @@ export default async function build( const analysisBegin = process.hrtime() let hasSsrAmpPages = false - const staticCheckSpan = tracer.startSpan('static-check') - const { hasNonStaticErrorPage } = await traceAsyncFn( - staticCheckSpan, + const staticCheckSpan = nextBuildSpan.traceChild('static-check') + const { hasNonStaticErrorPage } = await staticCheckSpan.traceAsyncFn( async () => { process.env.NEXT_PHASE = PHASE_PRODUCTION_BUILD @@ -601,8 +595,10 @@ export default async function build( serverRuntimeConfig: config.serverRuntimeConfig, } - const nonStaticErrorPage = await traceAsyncFn( - tracer.startSpan('check-static-error-page'), + const nonStaticErrorPageSpan = staticCheckSpan.traceChild( + 'check-static-error-page' + ) + const nonStaticErrorPage = nonStaticErrorPageSpan.traceAsyncFn( async () => hasCustomErrorPage && (await hasCustomGetInitialProps( @@ -646,143 +642,135 @@ export default async function build( await Promise.all( pageKeys.map(async (page) => { - return traceAsyncFn( - tracer.startSpan('check-page', { attributes: { page } }), - async () => { - const actualPage = normalizePagePath(page) - const [selfSize, allSize] = await getJsPageSizeInKb( - actualPage, - distDir, - buildManifest - ) + const checkPageSpan = staticCheckSpan.traceChild('check-page', { + page, + }) + return checkPageSpan.traceAsyncFn(async () => { + const actualPage = normalizePagePath(page) + const [selfSize, allSize] = await getJsPageSizeInKb( + actualPage, + distDir, + buildManifest + ) - let isSsg = false - let isStatic = false - let isHybridAmp = false - let ssgPageRoutes: string[] | null = null + let isSsg = false + let isStatic = false + let isHybridAmp = false + let ssgPageRoutes: string[] | null = null - const nonReservedPage = !page.match( - /^\/(_app|_error|_document|api(\/|$))/ - ) + const nonReservedPage = !page.match( + /^\/(_app|_error|_document|api(\/|$))/ + ) - if (nonReservedPage) { - try { - let workerResult = await traceAsyncFn( - tracer.startSpan('is-page-static'), - () => { - const spanContext = {} - - opentelemetryApi.propagation.inject( - opentelemetryApi.context.active(), - spanContext - ) - return staticCheckWorkers.isPageStatic( - page, - distDir, - isLikeServerless, - runtimeEnvConfig, - config.i18n?.locales, - config.i18n?.defaultLocale, - spanContext - ) - } - ) + if (nonReservedPage) { + try { + let workerResult = await checkPageSpan + .traceChild('is-page-static') + .traceAsyncFn(() => { + const spanContext = {} + + return staticCheckWorkers.isPageStatic( + page, + distDir, + isLikeServerless, + runtimeEnvConfig, + config.i18n?.locales, + config.i18n?.defaultLocale, + spanContext + ) + }) - if ( - workerResult.isStatic === false && - (workerResult.isHybridAmp || workerResult.isAmpOnly) - ) { - hasSsrAmpPages = true - } + if ( + workerResult.isStatic === false && + (workerResult.isHybridAmp || workerResult.isAmpOnly) + ) { + hasSsrAmpPages = true + } - if (workerResult.isHybridAmp) { - isHybridAmp = true - hybridAmpPages.add(page) - } + if (workerResult.isHybridAmp) { + isHybridAmp = true + hybridAmpPages.add(page) + } - if (workerResult.isNextImageImported) { - isNextImageImported = true - } + if (workerResult.isNextImageImported) { + isNextImageImported = true + } - if (workerResult.hasStaticProps) { - ssgPages.add(page) - isSsg = true + if (workerResult.hasStaticProps) { + ssgPages.add(page) + isSsg = true - if ( - workerResult.prerenderRoutes && - workerResult.encodedPrerenderRoutes - ) { - additionalSsgPaths.set( - page, - workerResult.prerenderRoutes - ) - additionalSsgPathsEncoded.set( - page, - workerResult.encodedPrerenderRoutes - ) - ssgPageRoutes = workerResult.prerenderRoutes - } - - if (workerResult.prerenderFallback === 'blocking') { - ssgBlockingFallbackPages.add(page) - } else if (workerResult.prerenderFallback === true) { - ssgStaticFallbackPages.add(page) - } - } else if (workerResult.hasServerProps) { - serverPropsPages.add(page) - } else if ( - workerResult.isStatic && - customAppGetInitialProps === false + if ( + workerResult.prerenderRoutes && + workerResult.encodedPrerenderRoutes ) { - staticPages.add(page) - isStatic = true + additionalSsgPaths.set(page, workerResult.prerenderRoutes) + additionalSsgPathsEncoded.set( + page, + workerResult.encodedPrerenderRoutes + ) + ssgPageRoutes = workerResult.prerenderRoutes } - if (hasPages404 && page === '/404') { - if ( - !workerResult.isStatic && - !workerResult.hasStaticProps - ) { - throw new Error( - `\`pages/404\` ${STATIC_STATUS_PAGE_GET_INITIAL_PROPS_ERROR}` - ) - } - // we need to ensure the 404 lambda is present since we use - // it when _app has getInitialProps - if ( - customAppGetInitialProps && - !workerResult.hasStaticProps - ) { - staticPages.delete(page) - } + if (workerResult.prerenderFallback === 'blocking') { + ssgBlockingFallbackPages.add(page) + } else if (workerResult.prerenderFallback === true) { + ssgStaticFallbackPages.add(page) } + } else if (workerResult.hasServerProps) { + serverPropsPages.add(page) + } else if ( + workerResult.isStatic && + customAppGetInitialProps === false + ) { + staticPages.add(page) + isStatic = true + } + if (hasPages404 && page === '/404') { if ( - STATIC_STATUS_PAGES.includes(page) && !workerResult.isStatic && !workerResult.hasStaticProps ) { throw new Error( - `\`pages${page}\` ${STATIC_STATUS_PAGE_GET_INITIAL_PROPS_ERROR}` + `\`pages/404\` ${STATIC_STATUS_PAGE_GET_INITIAL_PROPS_ERROR}` ) } - } catch (err) { - if (err.message !== 'INVALID_DEFAULT_EXPORT') throw err - invalidPages.add(page) + // we need to ensure the 404 lambda is present since we use + // it when _app has getInitialProps + if ( + customAppGetInitialProps && + !workerResult.hasStaticProps + ) { + staticPages.delete(page) + } } - } - pageInfos.set(page, { - size: selfSize, - totalSize: allSize, - static: isStatic, - isSsg, - isHybridAmp, - ssgPageRoutes, - initialRevalidateSeconds: false, - }) + if ( + STATIC_STATUS_PAGES.includes(page) && + !workerResult.isStatic && + !workerResult.hasStaticProps + ) { + throw new Error( + `\`pages${page}\` ${STATIC_STATUS_PAGE_GET_INITIAL_PROPS_ERROR}` + ) + } + } catch (err) { + if (err.message !== 'INVALID_DEFAULT_EXPORT') throw err + invalidPages.add(page) + } } - ) + + pageInfos.set(page, { + size: selfSize, + totalSize: allSize, + static: isStatic, + isSsg, + isHybridAmp, + ssgPageRoutes, + initialRevalidateSeconds: false, + }) + }) }) ) staticCheckWorkers.end() @@ -917,7 +905,8 @@ export default async function build( const hasPages500 = usedStaticStatusPages.includes('/500') const useDefaultStatic500 = !hasPages500 && !hasNonStaticErrorPage - await traceAsyncFn(tracer.startSpan('static-generation'), async () => { + const staticGenerationSpan = nextBuildSpan.traceChild('static-generation') + await staticGenerationSpan.traceAsyncFn(async () => { const combinedPages = [...staticPages, ...ssgPages] detectConflictingPaths( @@ -1061,9 +1050,9 @@ export default async function build( ext: 'html' | 'json', additionalSsgFile = false ) => { - return traceAsyncFn( - tracer.startSpan('move-exported-page'), - async () => { + return staticGenerationSpan + .traceChild('move-exported-page') + .traceAsyncFn(async () => { file = `${file}.${ext}` const orig = path.join(exportOptions.outdir, file) const pagePath = getPagePath(originPage, distDir, isLikeServerless) @@ -1163,8 +1152,7 @@ export default async function build( await promises.rename(updatedOrig, updatedDest) } } - } - ) + }) } // Only move /404 to /404 when there is no custom 404 as in that case we don't know about the 404 page @@ -1400,7 +1388,6 @@ export default async function build( }), 'utf8' ) - await promises.writeFile( path.join(distDir, EXPORT_MARKER), JSON.stringify({ @@ -1423,7 +1410,7 @@ export default async function build( allPageInfos.set(key, info) }) - await traceAsyncFn(tracer.startSpan('print-tree-view'), () => + await nextBuildSpan.traceChild('print-tree-view').traceAsyncFn(() => printTreeView(Object.keys(mappedPages), allPageInfos, isLikeServerless, { distPath: distDir, buildId: buildId, @@ -1435,9 +1422,9 @@ export default async function build( ) if (debugOutput) { - traceFn(tracer.startSpan('print-custom-routes'), () => - printCustomRoutes({ redirects, rewrites, headers }) - ) + nextBuildSpan + .traceChild('print-custom-routes') + .traceFn(() => printCustomRoutes({ redirects, rewrites, headers })) } if (config.analyticsId) { @@ -1449,9 +1436,9 @@ export default async function build( console.log('') } - await traceAsyncFn(tracer.startSpan('telemetry-flush'), () => - telemetry.flush() - ) + await nextBuildSpan + .traceChild('telemetry-flush') + .traceAsyncFn(() => telemetry.flush()) }) } diff --git a/packages/next/build/tracer.ts b/packages/next/build/tracer.ts deleted file mode 100644 index e1da864eb6f07..0000000000000 --- a/packages/next/build/tracer.ts +++ /dev/null @@ -1,100 +0,0 @@ -import api, { Span } from '@opentelemetry/api' - -export const tracer = api.trace.getTracer('next', process.env.__NEXT_VERSION) - -const compilerStacks = new WeakMap() -const compilerStoppedSpans = new WeakMap() - -export function stackPush(compiler: any, spanName: string, attrs?: any): any { - let stack = compilerStacks.get(compiler) - let span - - if (!stack) { - compilerStacks.set(compiler, (stack = [])) - span = tracer.startSpan(spanName, attrs ? attrs() : undefined) - } else { - const parent = stack[stack.length - 1] - if (parent) { - tracer.withSpan(parent, () => { - span = tracer.startSpan(spanName, attrs ? attrs() : undefined) - }) - } else { - span = tracer.startSpan(spanName, attrs ? attrs() : undefined) - } - } - - stack.push(span) - return span -} - -export function stackPop(compiler: any, span: any, associatedName?: string) { - let stack = compilerStacks.get(compiler) - if (!stack) { - console.warn( - 'Attempted to pop from non-existent stack. Compiler reference must be bad.' - ) - return - } - - let stoppedSpans: Set = compilerStoppedSpans.get(compiler) - if (!stoppedSpans) { - stoppedSpans = new Set() - compilerStoppedSpans.set(compiler, stoppedSpans) - } - if (stoppedSpans.has(span)) { - console.warn( - `Attempted to terminate tracing span that was already stopped for ${associatedName}` - ) - return - } - - while (true) { - let poppedSpan = stack.pop() - - if (poppedSpan === span) { - stoppedSpans.add(poppedSpan) - span.end() - stoppedSpans.add(span) - break - } else if (poppedSpan === undefined || stack.indexOf(span) === -1) { - // We've either reached the top of the stack or the stack doesn't contain - // the span for another reason. - console.warn(`Tracing span was not found in stack for: ${associatedName}`) - stoppedSpans.add(span) - span.end() - break - } else if (stack.indexOf(span) !== -1) { - console.warn( - `Attempted to pop span that was not at top of stack for: ${associatedName}` - ) - stoppedSpans.add(poppedSpan) - poppedSpan.end() - } - } -} - -export function traceFn ReturnType>( - span: Span, - fn: T -): ReturnType { - return tracer.withSpan(span, () => { - try { - return fn() - } finally { - span.end() - } - }) -} - -export function traceAsyncFn ReturnType>( - span: Span, - fn: T -): Promise> { - return tracer.withSpan(span, async () => { - try { - return await fn() - } finally { - span.end() - } - }) -} diff --git a/packages/next/build/utils.ts b/packages/next/build/utils.ts index 44b641e1f49ca..1230f9f721b96 100644 --- a/packages/next/build/utils.ts +++ b/packages/next/build/utils.ts @@ -29,9 +29,8 @@ import { removePathTrailingSlash } from '../client/normalize-trailing-slash' import { UnwrapPromise } from '../lib/coalesced-function' import { normalizeLocalePath } from '../next-server/lib/i18n/normalize-locale-path' import * as Log from './output/log' -import opentelemetryApi from '@opentelemetry/api' -import { tracer, traceAsyncFn } from './tracer' import { loadComponents } from '../next-server/server/load-components' +import { trace } from '../telemetry/trace' const fileGzipStats: { [k: string]: Promise } = {} const fsStatGzip = (file: string) => { @@ -719,7 +718,7 @@ export async function isPageStatic( runtimeEnvConfig: any, locales?: string[], defaultLocale?: string, - spanContext?: any + parentId?: any ): Promise<{ isStatic?: boolean isAmpOnly?: boolean @@ -731,132 +730,115 @@ export async function isPageStatic( prerenderFallback?: boolean | 'blocking' isNextImageImported?: boolean }> { - return opentelemetryApi.context.with( - opentelemetryApi.propagation.extract( - opentelemetryApi.context.active(), - spanContext - ), - () => { - return traceAsyncFn( - tracer.startSpan('is-page-static-utils'), - async () => { - try { - require('../next-server/lib/runtime-config').setConfig( - runtimeEnvConfig - ) - const components = await loadComponents(distDir, page, serverless) - const mod = components.ComponentMod - const Comp = mod.default || mod - - if ( - !Comp || - !isValidElementType(Comp) || - typeof Comp === 'string' - ) { - throw new Error('INVALID_DEFAULT_EXPORT') - } - - const hasGetInitialProps = !!(Comp as any).getInitialProps - const hasStaticProps = !!(await mod.getStaticProps) - const hasStaticPaths = !!(await mod.getStaticPaths) - const hasServerProps = !!(await mod.getServerSideProps) - const hasLegacyServerProps = !!(await mod.unstable_getServerProps) - const hasLegacyStaticProps = !!(await mod.unstable_getStaticProps) - const hasLegacyStaticPaths = !!(await mod.unstable_getStaticPaths) - const hasLegacyStaticParams = !!(await mod.unstable_getStaticParams) - - if (hasLegacyStaticParams) { - throw new Error( - `unstable_getStaticParams was replaced with getStaticPaths. Please update your code.` - ) - } - - if (hasLegacyStaticPaths) { - throw new Error( - `unstable_getStaticPaths was replaced with getStaticPaths. Please update your code.` - ) - } - - if (hasLegacyStaticProps) { - throw new Error( - `unstable_getStaticProps was replaced with getStaticProps. Please update your code.` - ) - } - - if (hasLegacyServerProps) { - throw new Error( - `unstable_getServerProps was replaced with getServerSideProps. Please update your code.` - ) - } - - // A page cannot be prerendered _and_ define a data requirement. That's - // contradictory! - if (hasGetInitialProps && hasStaticProps) { - throw new Error(SSG_GET_INITIAL_PROPS_CONFLICT) - } - - if (hasGetInitialProps && hasServerProps) { - throw new Error(SERVER_PROPS_GET_INIT_PROPS_CONFLICT) - } - - if (hasStaticProps && hasServerProps) { - throw new Error(SERVER_PROPS_SSG_CONFLICT) - } - - const pageIsDynamic = isDynamicRoute(page) - // A page cannot have static parameters if it is not a dynamic page. - if (hasStaticProps && hasStaticPaths && !pageIsDynamic) { - throw new Error( - `getStaticPaths can only be used with dynamic pages, not '${page}'.` + - `\nLearn more: https://nextjs.org/docs/routing/dynamic-routes` - ) - } - - if (hasStaticProps && pageIsDynamic && !hasStaticPaths) { - throw new Error( - `getStaticPaths is required for dynamic SSG pages and is missing for '${page}'.` + - `\nRead more: https://err.sh/next.js/invalid-getstaticpaths-value` - ) - } - - let prerenderRoutes: Array | undefined - let encodedPrerenderRoutes: Array | undefined - let prerenderFallback: boolean | 'blocking' | undefined - if (hasStaticProps && hasStaticPaths) { - ;({ - paths: prerenderRoutes, - fallback: prerenderFallback, - encodedPaths: encodedPrerenderRoutes, - } = await buildStaticPaths( - page, - mod.getStaticPaths, - locales, - defaultLocale - )) - } - - const isNextImageImported = (global as any).__NEXT_IMAGE_IMPORTED - const config = mod.config || {} - return { - isStatic: - !hasStaticProps && !hasGetInitialProps && !hasServerProps, - isHybridAmp: config.amp === 'hybrid', - isAmpOnly: config.amp === true, - prerenderRoutes, - prerenderFallback, - encodedPrerenderRoutes, - hasStaticProps, - hasServerProps, - isNextImageImported, - } - } catch (err) { - if (err.code === 'MODULE_NOT_FOUND') return {} - throw err - } - } - ) + const isPageStaticSpan = trace('is-page-static-utils', parentId) + return isPageStaticSpan.traceAsyncFn(async () => { + try { + require('../next-server/lib/runtime-config').setConfig(runtimeEnvConfig) + const components = await loadComponents(distDir, page, serverless) + const mod = components.ComponentMod + const Comp = mod.default || mod + + if (!Comp || !isValidElementType(Comp) || typeof Comp === 'string') { + throw new Error('INVALID_DEFAULT_EXPORT') + } + + const hasGetInitialProps = !!(Comp as any).getInitialProps + const hasStaticProps = !!(await mod.getStaticProps) + const hasStaticPaths = !!(await mod.getStaticPaths) + const hasServerProps = !!(await mod.getServerSideProps) + const hasLegacyServerProps = !!(await mod.unstable_getServerProps) + const hasLegacyStaticProps = !!(await mod.unstable_getStaticProps) + const hasLegacyStaticPaths = !!(await mod.unstable_getStaticPaths) + const hasLegacyStaticParams = !!(await mod.unstable_getStaticParams) + + if (hasLegacyStaticParams) { + throw new Error( + `unstable_getStaticParams was replaced with getStaticPaths. Please update your code.` + ) + } + + if (hasLegacyStaticPaths) { + throw new Error( + `unstable_getStaticPaths was replaced with getStaticPaths. Please update your code.` + ) + } + + if (hasLegacyStaticProps) { + throw new Error( + `unstable_getStaticProps was replaced with getStaticProps. Please update your code.` + ) + } + + if (hasLegacyServerProps) { + throw new Error( + `unstable_getServerProps was replaced with getServerSideProps. Please update your code.` + ) + } + + // A page cannot be prerendered _and_ define a data requirement. That's + // contradictory! + if (hasGetInitialProps && hasStaticProps) { + throw new Error(SSG_GET_INITIAL_PROPS_CONFLICT) + } + + if (hasGetInitialProps && hasServerProps) { + throw new Error(SERVER_PROPS_GET_INIT_PROPS_CONFLICT) + } + + if (hasStaticProps && hasServerProps) { + throw new Error(SERVER_PROPS_SSG_CONFLICT) + } + + const pageIsDynamic = isDynamicRoute(page) + // A page cannot have static parameters if it is not a dynamic page. + if (hasStaticProps && hasStaticPaths && !pageIsDynamic) { + throw new Error( + `getStaticPaths can only be used with dynamic pages, not '${page}'.` + + `\nLearn more: https://nextjs.org/docs/routing/dynamic-routes` + ) + } + + if (hasStaticProps && pageIsDynamic && !hasStaticPaths) { + throw new Error( + `getStaticPaths is required for dynamic SSG pages and is missing for '${page}'.` + + `\nRead more: https://err.sh/next.js/invalid-getstaticpaths-value` + ) + } + + let prerenderRoutes: Array | undefined + let encodedPrerenderRoutes: Array | undefined + let prerenderFallback: boolean | 'blocking' | undefined + if (hasStaticProps && hasStaticPaths) { + ;({ + paths: prerenderRoutes, + fallback: prerenderFallback, + encodedPaths: encodedPrerenderRoutes, + } = await buildStaticPaths( + page, + mod.getStaticPaths, + locales, + defaultLocale + )) + } + + const isNextImageImported = (global as any).__NEXT_IMAGE_IMPORTED + const config = mod.config || {} + return { + isStatic: !hasStaticProps && !hasGetInitialProps && !hasServerProps, + isHybridAmp: config.amp === 'hybrid', + isAmpOnly: config.amp === true, + prerenderRoutes, + prerenderFallback, + encodedPrerenderRoutes, + hasStaticProps, + hasServerProps, + isNextImageImported, + } + } catch (err) { + if (err.code === 'MODULE_NOT_FOUND') return {} + throw err } - ) + }) } export async function hasCustomGetInitialProps( diff --git a/packages/next/build/webpack/loaders/babel-loader/src/cache.js b/packages/next/build/webpack/loaders/babel-loader/src/cache.js index 406b239a737eb..570b67affea52 100644 --- a/packages/next/build/webpack/loaders/babel-loader/src/cache.js +++ b/packages/next/build/webpack/loaders/babel-loader/src/cache.js @@ -1,25 +1,23 @@ import { createHash } from 'crypto' -import { tracer, traceAsyncFn, traceFn } from '../../../../tracer' +import { trace } from '../../../../../telemetry/trace' import transform from './transform' import cacache from 'next/dist/compiled/cacache' async function read(cacheDirectory, etag) { - const cachedResult = await traceAsyncFn( - tracer.startSpan('read-cache-file'), - async () => await cacache.get(cacheDirectory, etag) + const cachedResult = await trace('read-cache-file').traceAsyncFn( + cacache.get(cacheDirectory, etag) ) - return JSON.parse(cachedResult.data) } function write(cacheDirectory, etag, data) { - return traceAsyncFn(tracer.startSpan('write-cache-file'), () => + return trace('write-cache-file').traceAsyncFn( cacache.put(cacheDirectory, etag, JSON.stringify(data)) ) } const etag = function (source, identifier, options) { - return traceFn(tracer.startSpan('etag'), () => { + return trace('etag').traceFn(() => { const hash = createHash('md4') const contents = JSON.stringify({ source, options, identifier }) @@ -31,8 +29,9 @@ const etag = function (source, identifier, options) { } export default async function handleCache(params) { - const span = tracer.startSpan('handle-cache') - return traceAsyncFn(span, async () => { + const span = trace('handle-cache') + + return span.traceAsyncFn(async () => { const { source, options = {}, cacheIdentifier, cacheDirectory } = params const file = etag(source, cacheIdentifier) @@ -47,12 +46,9 @@ export default async function handleCache(params) { // Otherwise just transform the file // return it to the user asap and write it in cache - const result = await traceAsyncFn( - tracer.startSpan('transform'), - async () => { - return transform(source, options) - } - ) + const result = await trace('transform').traceAsyncFn(async () => { + return transform(source, options) + }) await write(cacheDirectory, file, result) diff --git a/packages/next/build/webpack/loaders/babel-loader/src/index.js b/packages/next/build/webpack/loaders/babel-loader/src/index.js index 17234d6bad3af..04c3cf6a7a449 100644 --- a/packages/next/build/webpack/loaders/babel-loader/src/index.js +++ b/packages/next/build/webpack/loaders/babel-loader/src/index.js @@ -1,6 +1,6 @@ // import babel from 'next/dist/compiled/babel/core' import loaderUtils from 'next/dist/compiled/loader-utils' -import { tracer, traceAsyncFn, traceFn } from '../../../../tracer' +import { trace } from '../../../../../telemetry/trace' import cache from './cache' import transform from './transform' @@ -22,150 +22,142 @@ export default function makeLoader(callback) { } async function loader(source, inputSourceMap, overrides) { - // Provided by profiling-plugin.ts - return tracer.withSpan(this.currentTraceSpan, () => { - const span = tracer.startSpan('babel-loader') - return traceAsyncFn(span, async () => { - const filename = this.resourcePath - span.setAttribute('filename', filename) - - let loaderOptions = loaderUtils.getOptions(this) || {} - - let customOptions - if (overrides && overrides.customOptions) { - const result = await traceAsyncFn( - tracer.startSpan('loader-overrides-customoptions'), + // this.currentTraceSpan is provided by profiling-plugin.ts + const loaderSpan = trace('babel-loader', this.currentTraceSpan.id) + + return loaderSpan.traceAsyncFn(async () => { + const filename = this.resourcePath + loaderSpan.setAttribute('filename', filename) + + let loaderOptions = loaderUtils.getOptions(this) || {} + + let customOptions + if (overrides && overrides.customOptions) { + const customoptionsSpan = trace('loader-overrides-customoptions') + const result = await customoptionsSpan.traceAsyncFn( + async () => + await overrides.customOptions.call(this, loaderOptions, { + source, + map: inputSourceMap, + }) + ) + customOptions = result.custom + loaderOptions = result.loader + } + + // Standardize on 'sourceMaps' as the key passed through to Webpack, so that + // users may safely use either one alongside our default use of + // 'this.sourceMap' below without getting error about conflicting aliases. + if ( + Object.prototype.hasOwnProperty.call(loaderOptions, 'sourceMap') && + !Object.prototype.hasOwnProperty.call(loaderOptions, 'sourceMaps') + ) { + loaderOptions = Object.assign({}, loaderOptions, { + sourceMaps: loaderOptions.sourceMap, + }) + delete loaderOptions.sourceMap + } + + const programmaticOptions = Object.assign({}, loaderOptions, { + filename, + inputSourceMap: inputSourceMap || undefined, + + // Set the default sourcemap behavior based on Webpack's mapping flag, + // but allow users to override if they want. + sourceMaps: + loaderOptions.sourceMaps === undefined + ? this.sourceMap + : loaderOptions.sourceMaps, + + // Ensure that Webpack will get a full absolute path in the sourcemap + // so that it can properly map the module back to its internal cached + // modules. + sourceFileName: filename, + caller: { + name: 'babel-loader', + + // Provide plugins with insight into webpack target. + // https://github.com/babel/babel-loader/issues/787 + target: this.target, + + // Webpack >= 2 supports ESM and dynamic import. + supportsStaticESM: true, + supportsDynamicImport: true, + + // Webpack 5 supports TLA behind a flag. We enable it by default + // for Babel, and then webpack will throw an error if the experimental + // flag isn't enabled. + supportsTopLevelAwait: true, + ...loaderOptions.caller, + }, + }) + // Remove loader related options + delete programmaticOptions.cacheDirectory + delete programmaticOptions.cacheIdentifier + + const partialConfigSpan = trace('babel-load-partial-config-async') + const config = partialConfigSpan.traceFn(() => { + return babel.loadPartialConfig(programmaticOptions) + }) + + if (config) { + let options = config.options + if (overrides && overrides.config) { + const overridesSpan = trace('loader-overrides-config') + options = await overridesSpan.traceAsyncFn( async () => - await overrides.customOptions.call(this, loaderOptions, { + await overrides.config.call(this, config, { source, map: inputSourceMap, + customOptions, }) ) - customOptions = result.custom - loaderOptions = result.loader } - // Standardize on 'sourceMaps' as the key passed through to Webpack, so that - // users may safely use either one alongside our default use of - // 'this.sourceMap' below without getting error about conflicting aliases. - if ( - Object.prototype.hasOwnProperty.call(loaderOptions, 'sourceMap') && - !Object.prototype.hasOwnProperty.call(loaderOptions, 'sourceMaps') - ) { - loaderOptions = Object.assign({}, loaderOptions, { - sourceMaps: loaderOptions.sourceMap, + if (options.sourceMaps === 'inline') { + // Babel has this weird behavior where if you set "inline", we + // inline the sourcemap, and set 'result.map = null'. This results + // in bad behavior from Babel since the maps get put into the code, + // which Webpack does not expect, and because the map we return to + // Webpack is null, which is also bad. To avoid that, we override the + // behavior here so "inline" just behaves like 'true'. + options.sourceMaps = true + } + + const { cacheDirectory, cacheIdentifier } = loaderOptions + + let result + if (cacheDirectory) { + result = await cache({ + source, + options, + cacheDirectory, + cacheIdentifier, + cacheCompression: false, + }) + } else { + const transformSpan = trace('transform') + transformSpan.setAttribute('filename', filename) + transformSpan.setAttribute('cache', 'DISABLED') + result = await transformSpan.traceAsyncFn(async () => { + return transform(source, options) }) - delete loaderOptions.sourceMap } - const programmaticOptions = Object.assign({}, loaderOptions, { - filename, - inputSourceMap: inputSourceMap || undefined, - - // Set the default sourcemap behavior based on Webpack's mapping flag, - // but allow users to override if they want. - sourceMaps: - loaderOptions.sourceMaps === undefined - ? this.sourceMap - : loaderOptions.sourceMaps, - - // Ensure that Webpack will get a full absolute path in the sourcemap - // so that it can properly map the module back to its internal cached - // modules. - sourceFileName: filename, - caller: { - name: 'babel-loader', - - // Provide plugins with insight into webpack target. - // https://github.com/babel/babel-loader/issues/787 - target: this.target, - - // Webpack >= 2 supports ESM and dynamic import. - supportsStaticESM: true, - supportsDynamicImport: true, - - // Webpack 5 supports TLA behind a flag. We enable it by default - // for Babel, and then webpack will throw an error if the experimental - // flag isn't enabled. - supportsTopLevelAwait: true, - ...loaderOptions.caller, - }, - }) - // Remove loader related options - delete programmaticOptions.cacheDirectory - delete programmaticOptions.cacheIdentifier - - const config = traceFn( - tracer.startSpan('babel-load-partial-config-async'), - () => { - return babel.loadPartialConfig(programmaticOptions) - } - ) + // TODO: Babel should really provide the full list of config files that + // were used so that this can also handle files loaded with 'extends'. + if (typeof config.babelrc === 'string') { + this.addDependency(config.babelrc) + } - if (config) { - let options = config.options - if (overrides && overrides.config) { - options = await traceAsyncFn( - tracer.startSpan('loader-overrides-config'), - async () => - await overrides.config.call(this, config, { - source, - map: inputSourceMap, - customOptions, - }) - ) - } - - if (options.sourceMaps === 'inline') { - // Babel has this weird behavior where if you set "inline", we - // inline the sourcemap, and set 'result.map = null'. This results - // in bad behavior from Babel since the maps get put into the code, - // which Webpack does not expect, and because the map we return to - // Webpack is null, which is also bad. To avoid that, we override the - // behavior here so "inline" just behaves like 'true'. - options.sourceMaps = true - } - - const { cacheDirectory, cacheIdentifier } = loaderOptions - - let result - if (cacheDirectory) { - result = await cache({ - source, - options, - cacheDirectory, - cacheIdentifier, - cacheCompression: false, - }) - } else { - result = await traceAsyncFn( - tracer.startSpan('transform', { - attributes: { - filename, - cache: 'DISABLED', - }, - }), - async () => { - return transform(source, options) - } - ) - } - - // TODO: Babel should really provide the full list of config files that - // were used so that this can also handle files loaded with 'extends'. - if (typeof config.babelrc === 'string') { - this.addDependency(config.babelrc) - } - - if (result) { - const { code, map } = result - - return [code, map] - } + if (result) { + const { code, map } = result + + return [code, map] } + } - // If the file was ignored, pass through the original content. - return [source, inputSourceMap] - }) + // If the file was ignored, pass through the original content. + return [source, inputSourceMap] }) } diff --git a/packages/next/build/webpack/loaders/next-client-pages-loader.ts b/packages/next/build/webpack/loaders/next-client-pages-loader.ts index cb7725fb03cc5..b1cf19d7cf97a 100644 --- a/packages/next/build/webpack/loaders/next-client-pages-loader.ts +++ b/packages/next/build/webpack/loaders/next-client-pages-loader.ts @@ -1,5 +1,4 @@ import loaderUtils from 'next/dist/compiled/loader-utils' -import { tracer, traceFn } from '../../tracer' export type ClientPagesLoaderOptions = { absolutePagePath: string @@ -8,27 +7,28 @@ export type ClientPagesLoaderOptions = { // this parameter: https://www.typescriptlang.org/docs/handbook/functions.html#this-parameters function nextClientPagesLoader(this: any) { - return tracer.withSpan(this.currentTraceSpan, () => { - const span = tracer.startSpan('next-client-pages-loader') - return traceFn(span, () => { - const { absolutePagePath, page } = loaderUtils.getOptions( - this - ) as ClientPagesLoaderOptions + const pagesLoaderSpan = this.currentTraceSpan.traceChild( + 'next-client-pages-loader' + ) - span.setAttribute('absolutePagePath', absolutePagePath) + return pagesLoaderSpan.traceFn(() => { + const { absolutePagePath, page } = loaderUtils.getOptions( + this + ) as ClientPagesLoaderOptions - const stringifiedAbsolutePagePath = JSON.stringify(absolutePagePath) - const stringifiedPage = JSON.stringify(page) + pagesLoaderSpan.setAttribute('absolutePagePath', absolutePagePath) - return ` - (window.__NEXT_P = window.__NEXT_P || []).push([ - ${stringifiedPage}, - function () { - return require(${stringifiedAbsolutePagePath}); - } - ]); - ` - }) + const stringifiedAbsolutePagePath = JSON.stringify(absolutePagePath) + const stringifiedPage = JSON.stringify(page) + + return ` + (window.__NEXT_P = window.__NEXT_P || []).push([ + ${stringifiedPage}, + function () { + return require(${stringifiedAbsolutePagePath}); + } + ]); + ` }) } diff --git a/packages/next/build/webpack/loaders/next-serverless-loader/index.ts b/packages/next/build/webpack/loaders/next-serverless-loader/index.ts index 2dd6f884153a2..4db9d06268a86 100644 --- a/packages/next/build/webpack/loaders/next-serverless-loader/index.ts +++ b/packages/next/build/webpack/loaders/next-serverless-loader/index.ts @@ -11,7 +11,7 @@ import { ROUTES_MANIFEST, REACT_LOADABLE_MANIFEST, } from '../../../../next-server/lib/constants' -import { tracer, traceFn } from '../../../tracer' +import { trace } from '../../../../telemetry/trace' export type ServerlessLoaderQuery = { page: string @@ -34,8 +34,8 @@ export type ServerlessLoaderQuery = { } const nextServerlessLoader: webpack.loader.Loader = function () { - const span = tracer.startSpan('next-serverless-loader') - return traceFn(span, () => { + const loaderSpan = trace('next-serverless-loader') + return loaderSpan.traceFn(() => { const { distDir, absolutePagePath, diff --git a/packages/next/build/webpack/plugins/build-manifest-plugin.ts b/packages/next/build/webpack/plugins/build-manifest-plugin.ts index 5cfe31ba85d69..4309753153cc6 100644 --- a/packages/next/build/webpack/plugins/build-manifest-plugin.ts +++ b/packages/next/build/webpack/plugins/build-manifest-plugin.ts @@ -17,7 +17,6 @@ import getRouteFromEntrypoint from '../../../next-server/server/get-route-from-e import { ampFirstEntryNamesMap } from './next-drop-client-page-plugin' import { Rewrite } from '../../../lib/load-custom-routes' import { getSortedRoutes } from '../../../next-server/lib/router/utils' -import { tracer, traceFn } from '../../tracer' import { spans } from './profiling-plugin' type DeepMutable = { -readonly [P in keyof T]: DeepMutable } @@ -31,37 +30,39 @@ function generateClientManifest( assetMap: BuildManifest, rewrites: Rewrite[] ): string { - return tracer.withSpan(spans.get(compiler), () => { - const span = tracer.startSpan('NextJsBuildManifest-generateClientManifest') - return traceFn(span, () => { - const clientManifest: ClientBuildManifest = { - // TODO: update manifest type to include rewrites - __rewrites: rewrites as any, + const compilerSpan = spans.get(compiler) + const genClientManifestSpan = compilerSpan?.traceChild( + 'NextJsBuildManifest-generateClientManifest' + ) + + return genClientManifestSpan?.traceFn(() => { + const clientManifest: ClientBuildManifest = { + // TODO: update manifest type to include rewrites + __rewrites: rewrites as any, + } + const appDependencies = new Set(assetMap.pages['/_app']) + const sortedPageKeys = getSortedRoutes(Object.keys(assetMap.pages)) + + sortedPageKeys.forEach((page) => { + const dependencies = assetMap.pages[page] + + if (page === '/_app') return + // Filter out dependencies in the _app entry, because those will have already + // been loaded by the client prior to a navigation event + const filteredDeps = dependencies.filter( + (dep) => !appDependencies.has(dep) + ) + + // The manifest can omit the page if it has no requirements + if (filteredDeps.length) { + clientManifest[page] = filteredDeps } - const appDependencies = new Set(assetMap.pages['/_app']) - const sortedPageKeys = getSortedRoutes(Object.keys(assetMap.pages)) - - sortedPageKeys.forEach((page) => { - const dependencies = assetMap.pages[page] - - if (page === '/_app') return - // Filter out dependencies in the _app entry, because those will have already - // been loaded by the client prior to a navigation event - const filteredDeps = dependencies.filter( - (dep) => !appDependencies.has(dep) - ) - - // The manifest can omit the page if it has no requirements - if (filteredDeps.length) { - clientManifest[page] = filteredDeps - } - }) - // provide the sorted pages as an array so we don't rely on the object's keys - // being in order and we don't slow down look-up time for page assets - clientManifest.sortedPages = sortedPageKeys - - return devalue(clientManifest) }) + // provide the sorted pages as an array so we don't rely on the object's keys + // being in order and we don't slow down look-up time for page assets + clientManifest.sortedPages = sortedPageKeys + + return devalue(clientManifest) }) } @@ -103,125 +104,125 @@ export default class BuildManifestPlugin { } createAssets(compiler: any, compilation: any, assets: any) { - return tracer.withSpan(spans.get(compiler), () => { - const span = tracer.startSpan('NextJsBuildManifest-createassets') - return traceFn(span, () => { - const namedChunks: Map = - compilation.namedChunks - const assetMap: DeepMutable = { - polyfillFiles: [], - devFiles: [], - ampDevFiles: [], - lowPriorityFiles: [], - pages: { '/_app': [] }, - ampFirstPages: [], - } - - const ampFirstEntryNames = ampFirstEntryNamesMap.get(compilation) - if (ampFirstEntryNames) { - for (const entryName of ampFirstEntryNames) { - const pagePath = getRouteFromEntrypoint(entryName) - if (!pagePath) { - continue - } + const compilerSpan = spans.get(compiler) + const createAssetsSpan = compilerSpan?.traceChild( + 'NextJsBuildManifest-createassets' + ) + return createAssetsSpan?.traceFn(() => { + const namedChunks: Map = + compilation.namedChunks + const assetMap: DeepMutable = { + polyfillFiles: [], + devFiles: [], + ampDevFiles: [], + lowPriorityFiles: [], + pages: { '/_app': [] }, + ampFirstPages: [], + } - assetMap.ampFirstPages.push(pagePath) + const ampFirstEntryNames = ampFirstEntryNamesMap.get(compilation) + if (ampFirstEntryNames) { + for (const entryName of ampFirstEntryNames) { + const pagePath = getRouteFromEntrypoint(entryName) + if (!pagePath) { + continue } - } - - const mainJsChunk = namedChunks.get(CLIENT_STATIC_FILES_RUNTIME_MAIN) - const mainJsFiles: string[] = getFilesArray(mainJsChunk?.files).filter( - isJsFile - ) - - const polyfillChunk = namedChunks.get( - CLIENT_STATIC_FILES_RUNTIME_POLYFILLS - ) - - // Create a separate entry for polyfills - assetMap.polyfillFiles = getFilesArray(polyfillChunk?.files).filter( - isJsFile - ) + assetMap.ampFirstPages.push(pagePath) + } + } - const reactRefreshChunk = namedChunks.get( - CLIENT_STATIC_FILES_RUNTIME_REACT_REFRESH - ) - assetMap.devFiles = getFilesArray(reactRefreshChunk?.files).filter( - isJsFile - ) + const mainJsChunk = namedChunks.get(CLIENT_STATIC_FILES_RUNTIME_MAIN) - for (const entrypoint of compilation.entrypoints.values()) { - const isAmpRuntime = - entrypoint.name === CLIENT_STATIC_FILES_RUNTIME_AMP + const mainJsFiles: string[] = getFilesArray(mainJsChunk?.files).filter( + isJsFile + ) - if (isAmpRuntime) { - for (const file of entrypoint.getFiles()) { - if (!(isJsFile(file) || file.endsWith('.css'))) { - continue - } + const polyfillChunk = namedChunks.get( + CLIENT_STATIC_FILES_RUNTIME_POLYFILLS + ) - assetMap.ampDevFiles.push(file.replace(/\\/g, '/')) - } - continue - } - const pagePath = getRouteFromEntrypoint(entrypoint.name) + // Create a separate entry for polyfills + assetMap.polyfillFiles = getFilesArray(polyfillChunk?.files).filter( + isJsFile + ) - if (!pagePath) { - continue - } + const reactRefreshChunk = namedChunks.get( + CLIENT_STATIC_FILES_RUNTIME_REACT_REFRESH + ) + assetMap.devFiles = getFilesArray(reactRefreshChunk?.files).filter( + isJsFile + ) - const filesForEntry: string[] = [] + for (const entrypoint of compilation.entrypoints.values()) { + const isAmpRuntime = entrypoint.name === CLIENT_STATIC_FILES_RUNTIME_AMP - // getFiles() - helper function to read the files for an entrypoint from stats object + if (isAmpRuntime) { for (const file of entrypoint.getFiles()) { if (!(isJsFile(file) || file.endsWith('.css'))) { continue } - filesForEntry.push(file.replace(/\\/g, '/')) + assetMap.ampDevFiles.push(file.replace(/\\/g, '/')) } - - assetMap.pages[pagePath] = [...mainJsFiles, ...filesForEntry] + continue } + const pagePath = getRouteFromEntrypoint(entrypoint.name) - // Add the runtime build manifest file (generated later in this file) - // as a dependency for the app. If the flag is false, the file won't be - // downloaded by the client. - assetMap.lowPriorityFiles.push( - `${CLIENT_STATIC_FILES_PATH}/${this.buildId}/_buildManifest.js` - ) - - // Add the runtime ssg manifest file as a lazy-loaded file dependency. - // We also stub this file out for development mode (when it is not - // generated). - const srcEmptySsgManifest = `self.__SSG_MANIFEST=new Set;self.__SSG_MANIFEST_CB&&self.__SSG_MANIFEST_CB()` - - const ssgManifestPath = `${CLIENT_STATIC_FILES_PATH}/${this.buildId}/_ssgManifest.js` - assetMap.lowPriorityFiles.push(ssgManifestPath) - assets[ssgManifestPath] = new sources.RawSource(srcEmptySsgManifest) + if (!pagePath) { + continue + } - assetMap.pages = Object.keys(assetMap.pages) - .sort() - // eslint-disable-next-line - .reduce((a, c) => ((a[c] = assetMap.pages[c]), a), {} as any) + const filesForEntry: string[] = [] - assets[BUILD_MANIFEST] = new sources.RawSource( - JSON.stringify(assetMap, null, 2) - ) + // getFiles() - helper function to read the files for an entrypoint from stats object + for (const file of entrypoint.getFiles()) { + if (!(isJsFile(file) || file.endsWith('.css'))) { + continue + } - const clientManifestPath = `${CLIENT_STATIC_FILES_PATH}/${this.buildId}/_buildManifest.js` + filesForEntry.push(file.replace(/\\/g, '/')) + } - assets[clientManifestPath] = new sources.RawSource( - `self.__BUILD_MANIFEST = ${generateClientManifest( - compiler, - assetMap, - this.rewrites - )};self.__BUILD_MANIFEST_CB && self.__BUILD_MANIFEST_CB()` - ) + assetMap.pages[pagePath] = [...mainJsFiles, ...filesForEntry] + } - return assets - }) + // Add the runtime build manifest file (generated later in this file) + // as a dependency for the app. If the flag is false, the file won't be + // downloaded by the client. + assetMap.lowPriorityFiles.push( + `${CLIENT_STATIC_FILES_PATH}/${this.buildId}/_buildManifest.js` + ) + + // Add the runtime ssg manifest file as a lazy-loaded file dependency. + // We also stub this file out for development mode (when it is not + // generated). + const srcEmptySsgManifest = `self.__SSG_MANIFEST=new Set;self.__SSG_MANIFEST_CB&&self.__SSG_MANIFEST_CB()` + + const ssgManifestPath = `${CLIENT_STATIC_FILES_PATH}/${this.buildId}/_ssgManifest.js` + assetMap.lowPriorityFiles.push(ssgManifestPath) + assets[ssgManifestPath] = new sources.RawSource(srcEmptySsgManifest) + + assetMap.pages = Object.keys(assetMap.pages) + .sort() + // eslint-disable-next-line + .reduce((a, c) => ((a[c] = assetMap.pages[c]), a), {} as any) + + assets[BUILD_MANIFEST] = new sources.RawSource( + JSON.stringify(assetMap, null, 2) + ) + + const clientManifestPath = `${CLIENT_STATIC_FILES_PATH}/${this.buildId}/_buildManifest.js` + + assets[clientManifestPath] = new sources.RawSource( + `self.__BUILD_MANIFEST = ${generateClientManifest( + compiler, + assetMap, + this.rewrites + )};self.__BUILD_MANIFEST_CB && self.__BUILD_MANIFEST_CB()` + ) + + return assets }) } diff --git a/packages/next/build/webpack/plugins/build-stats-plugin.ts b/packages/next/build/webpack/plugins/build-stats-plugin.ts index bc8a6daf4c918..e41b0c67b466b 100644 --- a/packages/next/build/webpack/plugins/build-stats-plugin.ts +++ b/packages/next/build/webpack/plugins/build-stats-plugin.ts @@ -4,7 +4,7 @@ import path from 'path' import bfj from 'next/dist/compiled/bfj' import { spans } from './profiling-plugin' import { webpack } from 'next/dist/compiled/webpack/webpack' -import { tracer, traceAsyncFn } from '../../tracer' +import { trace } from '../../../telemetry/trace' const STATS_VERSION = 0 @@ -75,46 +75,45 @@ export default class BuildStatsPlugin { compiler.hooks.done.tapAsync( 'NextJsBuildStats', async (stats, callback) => { - tracer.withSpan(spans.get(compiler), async () => { - try { - const writeStatsSpan = tracer.startSpan('NextJsBuildStats') - await traceAsyncFn(writeStatsSpan, () => { - return new Promise((resolve, reject) => { - const statsJson = reduceSize( - stats.toJson({ - all: false, - cached: true, - reasons: true, - entrypoints: true, - chunks: true, - errors: false, - warnings: false, - maxModules: Infinity, - chunkModules: true, - modules: true, - // @ts-ignore this option exists - ids: true, - }) - ) - const fileStream = fs.createWriteStream( - path.join(this.distDir, 'next-stats.json') - ) - const jsonStream = bfj.streamify({ - version: STATS_VERSION, - stats: statsJson, + const compilerSpan = spans.get(compiler) + try { + const writeStatsSpan = trace('NextJsBuildStats', compilerSpan?.id) + await writeStatsSpan.traceAsyncFn(() => { + return new Promise((resolve, reject) => { + const statsJson = reduceSize( + stats.toJson({ + all: false, + cached: true, + reasons: true, + entrypoints: true, + chunks: true, + errors: false, + warnings: false, + maxModules: Infinity, + chunkModules: true, + modules: true, + // @ts-ignore this option exists + ids: true, }) - jsonStream.pipe(fileStream) - jsonStream.on('error', reject) - fileStream.on('error', reject) - jsonStream.on('dataError', reject) - fileStream.on('close', resolve) + ) + const fileStream = fs.createWriteStream( + path.join(this.distDir, 'next-stats.json') + ) + const jsonStream = bfj.streamify({ + version: STATS_VERSION, + stats: statsJson, }) + jsonStream.pipe(fileStream) + jsonStream.on('error', reject) + fileStream.on('error', reject) + jsonStream.on('dataError', reject) + fileStream.on('close', resolve) }) - callback() - } catch (err) { - callback(err) - } - }) + }) + callback() + } catch (err) { + callback(err) + } } ) } diff --git a/packages/next/build/webpack/plugins/css-minimizer-plugin.ts b/packages/next/build/webpack/plugins/css-minimizer-plugin.ts index b417fdbb3c885..2e2b7a69ebbab 100644 --- a/packages/next/build/webpack/plugins/css-minimizer-plugin.ts +++ b/packages/next/build/webpack/plugins/css-minimizer-plugin.ts @@ -6,7 +6,7 @@ import { isWebpack5, sources, } from 'next/dist/compiled/webpack/webpack' -import { tracer, traceAsyncFn } from '../../tracer' +import { trace } from '../../../telemetry/trace' import { spans } from './profiling-plugin' // https://github.com/NMFR/optimize-css-assets-webpack-plugin/blob/0a410a9bf28c7b0e81a3470a13748e68ca2f50aa/src/index.js#L20 @@ -70,47 +70,44 @@ export class CssMinimizerPlugin { stage: webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_SIZE, }, async (assets: any) => { - return tracer.withSpan(spans.get(compiler), () => { - const span = tracer.startSpan('css-minimizer-plugin', { - attributes: { - webpackVersion: 5, - }, - }) - - return traceAsyncFn(span, async () => { - const files = Object.keys(assets) - await Promise.all( - files - .filter((file) => CSS_REGEX.test(file)) - .map(async (file) => { - const assetSpan = tracer.startSpan('minify-css', { - attributes: { - file, - }, - }) - return traceAsyncFn(span, async () => { - const asset = assets[file] - - const etag = cache.getLazyHashedEtag(asset) - - const cachedResult = await cache.getPromise(file, etag) - - assetSpan.setAttribute( - 'cache', - cachedResult ? 'HIT' : 'MISS' - ) - if (cachedResult) { - assets[file] = cachedResult - return - } - - const result = await this.optimizeAsset(file, asset) - await cache.storePromise(file, etag, result) - assets[file] = result - }) + const compilerSpan = spans.get(compiler) + const cssMinimizerSpan = trace( + 'css-minimizer-plugin', + compilerSpan?.id + ) + cssMinimizerSpan.setAttribute('webpackVersion', 5) + + return cssMinimizerSpan.traceAsyncFn(async () => { + const files = Object.keys(assets) + await Promise.all( + files + .filter((file) => CSS_REGEX.test(file)) + .map(async (file) => { + const assetSpan = trace('minify-css', cssMinimizerSpan.id) + assetSpan.setAttribute('file', file) + + return assetSpan.traceAsyncFn(async () => { + const asset = assets[file] + + const etag = cache.getLazyHashedEtag(asset) + + const cachedResult = await cache.getPromise(file, etag) + + assetSpan.setAttribute( + 'cache', + cachedResult ? 'HIT' : 'MISS' + ) + if (cachedResult) { + assets[file] = cachedResult + return + } + + const result = await this.optimizeAsset(file, asset) + await cache.storePromise(file, etag, result) + assets[file] = result }) - ) - }) + }) + ) }) } ) @@ -119,42 +116,38 @@ export class CssMinimizerPlugin { compilation.hooks.optimizeChunkAssets.tapPromise( 'CssMinimizerPlugin', (chunks: webpack.compilation.Chunk[]) => { - return tracer.withSpan(spans.get(compiler), () => { - const span = tracer.startSpan('css-minimizer-plugin', { - attributes: { - webpackVersion: 4, - compilationName: compilation.name, - }, - }) - - return traceAsyncFn(span, async () => { - const res = await Promise.all( - chunks - .reduce( - (acc, chunk) => acc.concat(chunk.files || []), - [] as string[] - ) - .filter((entry) => CSS_REGEX.test(entry)) - .map(async (file) => { - const assetSpan = tracer.startSpan('minify-css', { - attributes: { - file, - }, - }) - return traceAsyncFn(assetSpan, async () => { - const asset = compilation.assets[file] - // Makes trace attributes the same as webpack 5 - // When using webpack 4 the result is not cached - assetSpan.setAttribute('cache', 'MISS') - compilation.assets[file] = await this.optimizeAsset( - file, - asset - ) - }) + const compilerSpan = spans.get(compiler) + const cssMinimizerSpan = trace( + 'css-minimizer-plugin', + compilerSpan?.id + ) + cssMinimizerSpan.setAttribute('webpackVersion', 4) + cssMinimizerSpan.setAttribute('compilationName', compilation.name) + + return cssMinimizerSpan.traceAsyncFn(async () => { + return await Promise.all( + chunks + .reduce( + (acc, chunk) => acc.concat(chunk.files || []), + [] as string[] + ) + .filter((entry) => CSS_REGEX.test(entry)) + .map(async (file) => { + const assetSpan = trace('minify-css', cssMinimizerSpan.id) + assetSpan.setAttribute('file', file) + + return assetSpan.traceAsyncFn(async () => { + const asset = compilation.assets[file] + // Makes trace attributes the same as webpack 5 + // When using webpack 4 the result is not cached + assetSpan.setAttribute('cache', 'MISS') + compilation.assets[file] = await this.optimizeAsset( + file, + asset + ) }) - ) - return res - }) + }) + ) }) } ) diff --git a/packages/next/build/webpack/plugins/profiling-plugin.ts b/packages/next/build/webpack/plugins/profiling-plugin.ts index 146ca81d09bec..db179a11a1d84 100644 --- a/packages/next/build/webpack/plugins/profiling-plugin.ts +++ b/packages/next/build/webpack/plugins/profiling-plugin.ts @@ -1,14 +1,14 @@ -import { tracer, stackPush, stackPop } from '../../tracer' import { webpack, isWebpack5 } from 'next/dist/compiled/webpack/webpack' import { - Span, + traceLevel, trace, - ProxyTracerProvider, - NoopTracerProvider, -} from '@opentelemetry/api' + stackPush, + stackPop, + Span, +} from '../../../telemetry/trace' const pluginName = 'ProfilingPlugin' -export const spans = new WeakMap() +export const spans = new WeakMap() function getNormalModuleLoaderHook(compilation: any) { if (isWebpack5) { @@ -19,21 +19,12 @@ function getNormalModuleLoaderHook(compilation: any) { return compilation.hooks.normalModuleLoader } -function tracingIsEnabled() { - const tracerProvider: any = trace.getTracerProvider() - if (tracerProvider instanceof ProxyTracerProvider) { - const proxyDelegate: any = tracerProvider.getDelegate() - return !(proxyDelegate instanceof NoopTracerProvider) - } - return false -} - export class ProfilingPlugin { compiler: any apply(compiler: any) { // Only enable plugin when instrumentation is loaded - if (!tracingIsEnabled()) { + if (traceLevel === 0) { return } this.traceTopLevelHooks(compiler) @@ -46,7 +37,7 @@ export class ProfilingPlugin { startHook: any, stopHook: any, attrs?: any, - onSetSpan?: (span: Span | undefined) => void + onSetSpan?: (span: Span) => void ) { let span: Span | undefined startHook.tap(pluginName, () => { @@ -60,7 +51,7 @@ export class ProfilingPlugin { if (!span) { return } - stackPop(this.compiler, span, spanName) + stackPop(this.compiler, span) }) } @@ -72,7 +63,7 @@ export class ProfilingPlugin { } }) stopHook.tap(pluginName, () => { - stackPop(this.compiler, span, spanName) + stackPop(this.compiler, span) }) } @@ -115,11 +106,10 @@ export class ProfilingPlugin { if (!compilerSpan) { return } - tracer.withSpan(compilerSpan, () => { - const span = tracer.startSpan('build-module') - span.setAttribute('name', module.userRequest) - spans.set(module, span) - }) + + const span = trace('build-module', compilerSpan.id) + span.setAttribute('name', module.userRequest) + spans.set(module, span) }) getNormalModuleLoaderHook(compilation).tap( @@ -131,7 +121,7 @@ export class ProfilingPlugin { ) compilation.hooks.succeedModule.tap(pluginName, (module: any) => { - spans.get(module).end() + spans.get(module)?.stop() }) this.traceHookPair( diff --git a/packages/next/build/webpack/plugins/terser-webpack-plugin/src/index.js b/packages/next/build/webpack/plugins/terser-webpack-plugin/src/index.js index ca72c47b5351d..720d068d11abb 100644 --- a/packages/next/build/webpack/plugins/terser-webpack-plugin/src/index.js +++ b/packages/next/build/webpack/plugins/terser-webpack-plugin/src/index.js @@ -10,7 +10,6 @@ import pLimit from 'p-limit' import jestWorker from 'jest-worker' import crypto from 'crypto' import cacache from 'next/dist/compiled/cacache' -import { tracer, traceAsyncFn } from '../../../../tracer' import { spans } from '../../profiling-plugin' function getEcmaVersion(environment) { @@ -123,193 +122,187 @@ class TerserPlugin { { SourceMapSource, RawSource } ) { const compilerSpan = spans.get(compiler) + const terserSpan = compilerSpan.traceChild('terser-webpack-plugin-optimize') + terserSpan.setAttribute('webpackVersion', isWebpack5 ? 5 : 4) + terserSpan.setAttribute('compilationName', compilation.name) + + return terserSpan.traceAsyncFn(async () => { + let numberOfAssetsForMinify = 0 + const assetsList = isWebpack5 + ? Object.keys(assets) + : [ + ...Array.from(compilation.additionalChunkAssets || []), + ...Array.from(assets).reduce((acc, chunk) => { + return acc.concat(Array.from(chunk.files || [])) + }, []), + ] + + const assetsForMinify = await Promise.all( + assetsList + .filter((name) => { + if ( + !ModuleFilenameHelpers.matchObject.bind( + // eslint-disable-next-line no-undefined + undefined, + { test: /\.[cm]?js(\?.*)?$/i } + )(name) + ) { + return false + } - return tracer.withSpan(compilerSpan, () => { - const span = tracer.startSpan('terser-webpack-plugin-optimize', { - attributes: { - webpackVersion: isWebpack5 ? 5 : 4, - compilationName: compilation.name, - }, - }) - - return traceAsyncFn(span, async () => { - let numberOfAssetsForMinify = 0 - const assetsList = isWebpack5 - ? Object.keys(assets) - : [ - ...Array.from(compilation.additionalChunkAssets || []), - ...Array.from(assets).reduce((acc, chunk) => { - return acc.concat(Array.from(chunk.files || [])) - }, []), - ] - - const assetsForMinify = await Promise.all( - assetsList - .filter((name) => { - if ( - !ModuleFilenameHelpers.matchObject.bind( - // eslint-disable-next-line no-undefined - undefined, - { test: /\.[cm]?js(\?.*)?$/i } - )(name) - ) { - return false - } + const res = compilation.getAsset(name) + if (!res) { + console.log(name) + return false + } - const res = compilation.getAsset(name) - if (!res) { - console.log(name) - return false - } + const { info } = res - const { info } = res + // Skip double minimize assets from child compilation + if (info.minimized) { + return false + } - // Skip double minimize assets from child compilation - if (info.minimized) { - return false - } + return true + }) + .map(async (name) => { + const { info, source } = compilation.getAsset(name) - return true - }) - .map(async (name) => { - const { info, source } = compilation.getAsset(name) + const eTag = cache.getLazyHashedEtag(source) + const output = await cache.getPromise(name, eTag) - const eTag = cache.getLazyHashedEtag(source) - const output = await cache.getPromise(name, eTag) + if (!output) { + numberOfAssetsForMinify += 1 + } - if (!output) { - numberOfAssetsForMinify += 1 - } + return { name, info, inputSource: source, output, eTag } + }) + ) - return { name, info, inputSource: source, output, eTag } - }) - ) + const numberOfWorkers = Math.min( + numberOfAssetsForMinify, + optimizeOptions.availableNumberOfCores + ) - const numberOfWorkers = Math.min( - numberOfAssetsForMinify, - optimizeOptions.availableNumberOfCores - ) + let initializedWorker - let initializedWorker + // eslint-disable-next-line consistent-return + const getWorker = () => { + if (initializedWorker) { + return initializedWorker + } - // eslint-disable-next-line consistent-return - const getWorker = () => { - if (initializedWorker) { - return initializedWorker + initializedWorker = new jestWorker( + path.join(__dirname, './minify.js'), + { + numWorkers: numberOfWorkers, + enableWorkerThreads: true, } + ) - initializedWorker = new jestWorker( - path.join(__dirname, './minify.js'), - { - numWorkers: numberOfWorkers, - enableWorkerThreads: true, - } - ) + initializedWorker.getStdout().pipe(process.stdout) + initializedWorker.getStderr().pipe(process.stderr) - initializedWorker.getStdout().pipe(process.stdout) - initializedWorker.getStderr().pipe(process.stderr) + return initializedWorker + } - return initializedWorker - } + const limit = pLimit( + numberOfAssetsForMinify > 0 ? numberOfWorkers : Infinity + ) + const scheduledTasks = [] + + for (const asset of assetsForMinify) { + scheduledTasks.push( + limit(async () => { + const { name, inputSource, info, eTag } = asset + let { output } = asset + + const minifySpan = terserSpan.traceChild('minify-fs') + minifySpan.setAttribute('name', name) + minifySpan.setAttribute( + 'cache', + typeof output === 'undefined' ? 'MISS' : 'HIT' + ) - const limit = pLimit( - numberOfAssetsForMinify > 0 ? numberOfWorkers : Infinity - ) - const scheduledTasks = [] + return minifySpan.traceAsyncFn(async () => { + if (!output) { + const { + source: sourceFromInputSource, + map: inputSourceMap, + } = inputSource.sourceAndMap() - for (const asset of assetsForMinify) { - scheduledTasks.push( - limit(async () => { - const { name, inputSource, info, eTag } = asset - let { output } = asset + const input = Buffer.isBuffer(sourceFromInputSource) + ? sourceFromInputSource.toString() + : sourceFromInputSource - const assetSpan = tracer.startSpan('minify-js', { - attributes: { + const options = { name, - cache: typeof output === 'undefined' ? 'MISS' : 'HIT', - }, - }) - - return traceAsyncFn(assetSpan, async () => { - if (!output) { - const { - source: sourceFromInputSource, - map: inputSourceMap, - } = inputSource.sourceAndMap() - - const input = Buffer.isBuffer(sourceFromInputSource) - ? sourceFromInputSource.toString() - : sourceFromInputSource - - const options = { - name, - input, - inputSourceMap, - terserOptions: { ...this.options.terserOptions }, - } + input, + inputSourceMap, + terserOptions: { ...this.options.terserOptions }, + } - if (typeof options.terserOptions.module === 'undefined') { - if (typeof info.javascriptModule !== 'undefined') { - options.terserOptions.module = info.javascriptModule - } else if (/\.mjs(\?.*)?$/i.test(name)) { - options.terserOptions.module = true - } else if (/\.cjs(\?.*)?$/i.test(name)) { - options.terserOptions.module = false - } + if (typeof options.terserOptions.module === 'undefined') { + if (typeof info.javascriptModule !== 'undefined') { + options.terserOptions.module = info.javascriptModule + } else if (/\.mjs(\?.*)?$/i.test(name)) { + options.terserOptions.module = true + } else if (/\.cjs(\?.*)?$/i.test(name)) { + options.terserOptions.module = false } + } - try { - output = await getWorker().minify(options) - } catch (error) { - compilation.errors.push(buildError(error, name)) + try { + output = await getWorker().minify(options) + } catch (error) { + compilation.errors.push(buildError(error, name)) - return - } + return + } - if (output.map) { - output.source = new SourceMapSource( - output.code, - name, - output.map, - input, - /** @type {SourceMapRawSourceMap} */ (inputSourceMap), - true - ) - } else { - output.source = new RawSource(output.code) - } + if (output.map) { + output.source = new SourceMapSource( + output.code, + name, + output.map, + input, + /** @type {SourceMapRawSourceMap} */ (inputSourceMap), + true + ) + } else { + output.source = new RawSource(output.code) + } - if (isWebpack5) { - await cache.storePromise(name, eTag, { - source: output.source, - }) - } else { - await cache.storePromise(name, eTag, { - code: output.code, - map: output.map, - name, - input, - inputSourceMap, - }) - } + if (isWebpack5) { + await cache.storePromise(name, eTag, { + source: output.source, + }) + } else { + await cache.storePromise(name, eTag, { + code: output.code, + map: output.map, + name, + input, + inputSourceMap, + }) } + } - /** @type {AssetInfo} */ - const newInfo = { minimized: true } - const { source } = output + /** @type {AssetInfo} */ + const newInfo = { minimized: true } + const { source } = output - compilation.updateAsset(name, source, newInfo) - }) + compilation.updateAsset(name, source, newInfo) }) - ) - } + }) + ) + } - await Promise.all(scheduledTasks) + await Promise.all(scheduledTasks) - if (initializedWorker) { - await initializedWorker.end() - } - }) + if (initializedWorker) { + await initializedWorker.end() + } }) } diff --git a/packages/next/export/index.ts b/packages/next/export/index.ts index 85b50ff02002b..9b54d9d1d3b3d 100644 --- a/packages/next/export/index.ts +++ b/packages/next/export/index.ts @@ -46,8 +46,7 @@ import { PrerenderManifest } from '../build' import exportPage from './worker' import { PagesManifest } from '../build/webpack/plugins/pages-manifest-plugin' import { getPagePath } from '../next-server/server/require' -import { tracer, traceFn, traceAsyncFn } from '../build/tracer' -import opentelemetryApi from '@opentelemetry/api' +import { trace } from '../telemetry/trace' const exists = promisify(existsOrig) @@ -137,20 +136,21 @@ export default async function exportApp( options: ExportOptions, configuration?: NextConfig ): Promise { - const nextExportSpan = tracer.startSpan('next-export') - return traceAsyncFn(nextExportSpan, async () => { + const nextExportSpan = trace('next-export') + + return nextExportSpan.traceAsyncFn(async () => { dir = resolve(dir) // attempt to load global env values so they are available in next.config.js - traceFn(tracer.startSpan('load-dotenv'), () => - loadEnvConfig(dir, false, Log) - ) + nextExportSpan + .traceChild('load-dotenv') + .traceFn(() => loadEnvConfig(dir, false, Log)) const nextConfig = configuration || - (await traceAsyncFn(tracer.startSpan('load-next-config'), () => - loadConfig(PHASE_EXPORT, dir) - )) + (await nextExportSpan + .traceChild('load-next-config') + .traceAsyncFn(() => loadConfig(PHASE_EXPORT, dir))) const threads = options.threads || Math.max(cpus().length - 1, 1) const distDir = join(dir, nextConfig.distDir) @@ -276,9 +276,12 @@ export default async function exportApp( if (!options.silent) { Log.info('Copying "static" directory') } - await traceAsyncFn(tracer.startSpan('copy-static-directory'), () => - recursiveCopy(join(dir, 'static'), join(outDir, 'static')) - ) + /******************/ + await nextExportSpan + .traceChild('copy-static-directory') + .traceAsyncFn(() => + recursiveCopy(join(dir, 'static'), join(outDir, 'static')) + ) } // Copy .next/static directory @@ -289,12 +292,14 @@ export default async function exportApp( if (!options.silent) { Log.info('Copying "static build" directory') } - await traceAsyncFn(tracer.startSpan('copy-next-static-directory'), () => - recursiveCopy( - join(distDir, CLIENT_STATIC_FILES_PATH), - join(outDir, '_next', CLIENT_STATIC_FILES_PATH) + await nextExportSpan + .traceChild('copy-next-static-directory') + .traceAsyncFn(() => + recursiveCopy( + join(distDir, CLIENT_STATIC_FILES_PATH), + join(outDir, '_next', CLIENT_STATIC_FILES_PATH) + ) ) - ) } // Get the exportPathMap from the config file @@ -321,14 +326,14 @@ export default async function exportApp( } if (!options.buildExport) { - const { isNextImageImported } = await traceAsyncFn( - tracer.startSpan('is-next-image-imported'), - () => + const { isNextImageImported } = await nextExportSpan + .traceChild('is-next-image-imported') + .traceAsyncFn(() => promises .readFile(join(distDir, EXPORT_MARKER), 'utf8') .then((text) => JSON.parse(text)) .catch(() => ({})) - ) + ) if (isNextImageImported && loader === 'default' && !hasNextSupport) { throw new Error( @@ -376,9 +381,9 @@ export default async function exportApp( if (!options.silent && !options.buildExport) { Log.info(`Launching ${threads} workers`) } - const exportPathMap = await traceAsyncFn( - tracer.startSpan('run-export-path-map'), - () => + const exportPathMap = await nextExportSpan + .traceChild('run-export-path-map') + .traceAsyncFn(() => nextConfig.exportPathMap(defaultPathMap, { dev: false, dir, @@ -386,7 +391,7 @@ export default async function exportApp( distDir, buildId, }) - ) + ) if ( !options.buildExport && @@ -483,14 +488,16 @@ export default async function exportApp( if (!options.silent) { Log.info('Copying "public" directory') } - await traceAsyncFn(tracer.startSpan('copy-public-directory'), () => - recursiveCopy(publicDir, outDir, { - filter(path) { - // Exclude paths used by pages - return !exportPathMap[path] - }, - }) - ) + await nextExportSpan + .traceChild('copy-public-directory') + .traceAsyncFn(() => + recursiveCopy(publicDir, outDir, { + filter(path) { + // Exclude paths used by pages + return !exportPathMap[path] + }, + }) + ) } const worker = new Worker(require.resolve('./worker'), { @@ -508,17 +515,10 @@ export default async function exportApp( await Promise.all( filteredPaths.map(async (path) => { - const pageExportSpan = tracer.startSpan('export-page', { - attributes: { path }, - }) - return traceAsyncFn(pageExportSpan, async () => { - const spanContext = {} - - opentelemetryApi.propagation.inject( - opentelemetryApi.context.active(), - spanContext - ) + const pageExportSpan = nextExportSpan.traceChild('export-page') + pageExportSpan.setAttribute('path', path) + return pageExportSpan.traceAsyncFn(async () => { const result = await worker.default({ path, pathMap: exportPathMap[path], @@ -533,7 +533,7 @@ export default async function exportApp( optimizeFonts: nextConfig.experimental.optimizeFonts, optimizeImages: nextConfig.experimental.optimizeImages, optimizeCss: nextConfig.experimental.optimizeCss, - spanContext, + parentSpanId: pageExportSpan.id, }) for (const validation of result.ampValidations || []) { diff --git a/packages/next/export/worker.ts b/packages/next/export/worker.ts index ca2eaa5734db9..d52d2d9102a3f 100644 --- a/packages/next/export/worker.ts +++ b/packages/next/export/worker.ts @@ -16,8 +16,7 @@ import { GetStaticProps } from '../types' import { requireFontManifest } from '../next-server/server/require' import { FontManifest } from '../next-server/server/font-utils' import { normalizeLocalePath } from '../next-server/lib/i18n/normalize-locale-path' -import { tracer, traceAsyncFn } from '../build/tracer' -import opentelemetryApi from '@opentelemetry/api' +import { trace } from '../telemetry/trace' const envConfig = require('../next-server/lib/runtime-config') @@ -52,7 +51,7 @@ interface ExportPageInput { optimizeFonts: boolean optimizeImages: boolean optimizeCss: any - spanContext: any + parentSpanId: any } interface ExportPageResults { @@ -85,7 +84,7 @@ type ComponentModule = ComponentType<{}> & { } export default async function exportPage({ - spanContext, + parentSpanId, path, pathMap, distDir, @@ -100,367 +99,357 @@ export default async function exportPage({ optimizeImages, optimizeCss, }: ExportPageInput): Promise { - return opentelemetryApi.context.with( - opentelemetryApi.propagation.extract( - opentelemetryApi.context.active(), - spanContext - ), - () => { - return traceAsyncFn(tracer.startSpan('export-page-worker'), async () => { - let results: ExportPageResults = { - ampValidations: [], - } - - try { - const { query: originalQuery = {} } = pathMap - const { page } = pathMap - const filePath = normalizePagePath(path) - const isDynamic = isDynamicRoute(page) - const ampPath = `${filePath}.amp` - let renderAmpPath = ampPath - let query = { ...originalQuery } - let params: { [key: string]: string | string[] } | undefined - - let updatedPath = (query.__nextSsgPath as string) || path - let locale = query.__nextLocale || renderOpts.locale - delete query.__nextLocale - delete query.__nextSsgPath - - if (renderOpts.locale) { - const localePathResult = normalizeLocalePath( - path, - renderOpts.locales - ) - - if (localePathResult.detectedLocale) { - updatedPath = localePathResult.pathname - locale = localePathResult.detectedLocale + const exportPageSpan = trace('export-page-worker', parentSpanId) - if (locale === renderOpts.defaultLocale) { - renderAmpPath = `${normalizePagePath(updatedPath)}.amp` - } - } - } + return exportPageSpan.traceAsyncFn(async () => { + let results: ExportPageResults = { + ampValidations: [], + } - // We need to show a warning if they try to provide query values - // for an auto-exported page since they won't be available - const hasOrigQueryValues = Object.keys(originalQuery).length > 0 - const queryWithAutoExportWarn = () => { - if (hasOrigQueryValues) { - throw new Error( - `\nError: you provided query values for ${path} which is an auto-exported page. These can not be applied since the page can no longer be re-rendered on the server. To disable auto-export for this page add \`getInitialProps\`\n` - ) - } + try { + const { query: originalQuery = {} } = pathMap + const { page } = pathMap + const filePath = normalizePagePath(path) + const isDynamic = isDynamicRoute(page) + const ampPath = `${filePath}.amp` + let renderAmpPath = ampPath + let query = { ...originalQuery } + let params: { [key: string]: string | string[] } | undefined + + let updatedPath = (query.__nextSsgPath as string) || path + let locale = query.__nextLocale || renderOpts.locale + delete query.__nextLocale + delete query.__nextSsgPath + + if (renderOpts.locale) { + const localePathResult = normalizeLocalePath(path, renderOpts.locales) + + if (localePathResult.detectedLocale) { + updatedPath = localePathResult.pathname + locale = localePathResult.detectedLocale + + if (locale === renderOpts.defaultLocale) { + renderAmpPath = `${normalizePagePath(updatedPath)}.amp` } - - // Check if the page is a specified dynamic route - if (isDynamic && page !== path) { - params = - getRouteMatcher(getRouteRegex(page))(updatedPath) || undefined - if (params) { - // we have to pass these separately for serverless - if (!serverless) { - query = { - ...query, - ...params, - } - } - } else { - throw new Error( - `The provided export path '${updatedPath}' doesn't match the '${page}' page.\nRead more: https://err.sh/vercel/next.js/export-path-mismatch` - ) + } + } + + // We need to show a warning if they try to provide query values + // for an auto-exported page since they won't be available + const hasOrigQueryValues = Object.keys(originalQuery).length > 0 + const queryWithAutoExportWarn = () => { + if (hasOrigQueryValues) { + throw new Error( + `\nError: you provided query values for ${path} which is an auto-exported page. These can not be applied since the page can no longer be re-rendered on the server. To disable auto-export for this page add \`getInitialProps\`\n` + ) + } + } + + // Check if the page is a specified dynamic route + if (isDynamic && page !== path) { + params = getRouteMatcher(getRouteRegex(page))(updatedPath) || undefined + if (params) { + // we have to pass these separately for serverless + if (!serverless) { + query = { + ...query, + ...params, } } + } else { + throw new Error( + `The provided export path '${updatedPath}' doesn't match the '${page}' page.\nRead more: https://err.sh/vercel/next.js/export-path-mismatch` + ) + } + } + + const headerMocks = { + headers: {}, + getHeader: () => ({}), + setHeader: () => {}, + hasHeader: () => false, + removeHeader: () => {}, + getHeaderNames: () => [], + } + + const req = ({ + url: updatedPath, + ...headerMocks, + } as unknown) as IncomingMessage + const res = ({ + ...headerMocks, + } as unknown) as ServerResponse + + if (path === '/500' && page === '/_error') { + res.statusCode = 500 + } + + envConfig.setConfig({ + serverRuntimeConfig, + publicRuntimeConfig: renderOpts.runtimeConfig, + }) - const headerMocks = { - headers: {}, - getHeader: () => ({}), - setHeader: () => {}, - hasHeader: () => false, - removeHeader: () => {}, - getHeaderNames: () => [], - } + let htmlFilename = `${filePath}${sep}index.html` + if (!subFolders) htmlFilename = `${filePath}.html` + + const pageExt = extname(page) + const pathExt = extname(path) + // Make sure page isn't a folder with a dot in the name e.g. `v1.2` + if (pageExt !== pathExt && pathExt !== '') { + // If the path has an extension, use that as the filename instead + htmlFilename = path + } else if (path === '/') { + // If the path is the root, just use index.html + htmlFilename = 'index.html' + } + + const baseDir = join(outDir, dirname(htmlFilename)) + let htmlFilepath = join(outDir, htmlFilename) + + await promises.mkdir(baseDir, { recursive: true }) + let html + let curRenderOpts: RenderOpts = {} + let renderMethod = renderToHTML + + const renderedDuringBuild = (getStaticProps: any) => { + return !buildExport && getStaticProps && !isDynamicRoute(path) + } + + if (serverless) { + const curUrl = url.parse(req.url!, true) + req.url = url.format({ + ...curUrl, + query: { + ...curUrl.query, + ...query, + }, + }) + const { Component: mod, getServerSideProps } = await loadComponents( + distDir, + page, + serverless + ) + + if (getServerSideProps) { + throw new Error( + `Error for page ${page}: ${SERVER_PROPS_EXPORT_ERROR}` + ) + } - const req = ({ - url: updatedPath, - ...headerMocks, - } as unknown) as IncomingMessage - const res = ({ - ...headerMocks, - } as unknown) as ServerResponse + // if it was auto-exported the HTML is loaded here + if (typeof mod === 'string') { + html = mod + queryWithAutoExportWarn() + } else { + // for non-dynamic SSG pages we should have already + // prerendered the file + if (renderedDuringBuild((mod as ComponentModule).getStaticProps)) + return results - if (path === '/500' && page === '/_error') { - res.statusCode = 500 + if ( + (mod as ComponentModule).getStaticProps && + !htmlFilepath.endsWith('.html') + ) { + // make sure it ends with .html if the name contains a dot + htmlFilename += '.html' + htmlFilepath += '.html' } - envConfig.setConfig({ - serverRuntimeConfig, - publicRuntimeConfig: renderOpts.runtimeConfig, - }) + renderMethod = (mod as ComponentModule).renderReqToHTML + const result = await renderMethod( + req, + res, + 'export', + { + ampPath: renderAmpPath, + /// @ts-ignore + optimizeFonts, + /// @ts-ignore + optimizeImages, + /// @ts-ignore + optimizeCss, + distDir, + fontManifest: optimizeFonts + ? requireFontManifest(distDir, serverless) + : null, + locale: locale!, + locales: renderOpts.locales!, + }, + // @ts-ignore + params + ) + curRenderOpts = (result as any).renderOpts || {} + html = (result as any).html + } - let htmlFilename = `${filePath}${sep}index.html` - if (!subFolders) htmlFilename = `${filePath}.html` - - const pageExt = extname(page) - const pathExt = extname(path) - // Make sure page isn't a folder with a dot in the name e.g. `v1.2` - if (pageExt !== pathExt && pathExt !== '') { - // If the path has an extension, use that as the filename instead - htmlFilename = path - } else if (path === '/') { - // If the path is the root, just use index.html - htmlFilename = 'index.html' - } + if (!html && !(curRenderOpts as any).isNotFound) { + throw new Error(`Failed to render serverless page`) + } + } else { + const components = await loadComponents(distDir, page, serverless) + + if (components.getServerSideProps) { + throw new Error( + `Error for page ${page}: ${SERVER_PROPS_EXPORT_ERROR}` + ) + } - const baseDir = join(outDir, dirname(htmlFilename)) - let htmlFilepath = join(outDir, htmlFilename) + // for non-dynamic SSG pages we should have already + // prerendered the file + if (renderedDuringBuild(components.getStaticProps)) { + return results + } - await promises.mkdir(baseDir, { recursive: true }) - let html - let curRenderOpts: RenderOpts = {} - let renderMethod = renderToHTML + // TODO: de-dupe the logic here between serverless and server mode + if (components.getStaticProps && !htmlFilepath.endsWith('.html')) { + // make sure it ends with .html if the name contains a dot + htmlFilepath += '.html' + htmlFilename += '.html' + } - const renderedDuringBuild = (getStaticProps: any) => { - return !buildExport && getStaticProps && !isDynamicRoute(path) + if (typeof components.Component === 'string') { + html = components.Component + queryWithAutoExportWarn() + } else { + /** + * This sets environment variable to be used at the time of static export by head.tsx. + * Using this from process.env allows targeting both serverless and SSR by calling + * `process.env.__NEXT_OPTIMIZE_FONTS`. + * TODO(prateekbh@): Remove this when experimental.optimizeFonts are being cleaned up. + */ + if (optimizeFonts) { + process.env.__NEXT_OPTIMIZE_FONTS = JSON.stringify(true) } + if (optimizeImages) { + process.env.__NEXT_OPTIMIZE_IMAGES = JSON.stringify(true) + } + if (optimizeCss) { + process.env.__NEXT_OPTIMIZE_CSS = JSON.stringify(true) + } + curRenderOpts = { + ...components, + ...renderOpts, + ampPath: renderAmpPath, + params, + optimizeFonts, + optimizeImages, + optimizeCss, + fontManifest: optimizeFonts + ? requireFontManifest(distDir, serverless) + : null, + locale: locale as string, + } + // @ts-ignore + html = await renderMethod(req, res, page, query, curRenderOpts) + } + } + results.ssgNotFound = (curRenderOpts as any).isNotFound + + const validateAmp = async ( + rawAmpHtml: string, + ampPageName: string, + validatorPath?: string + ) => { + const validator = await AmpHtmlValidator.getInstance(validatorPath) + const result = validator.validateString(rawAmpHtml) + const errors = result.errors.filter((e) => e.severity === 'ERROR') + const warnings = result.errors.filter((e) => e.severity !== 'ERROR') + + if (warnings.length || errors.length) { + results.ampValidations.push({ + page: ampPageName, + result: { + errors, + warnings, + }, + }) + } + } - if (serverless) { - const curUrl = url.parse(req.url!, true) - req.url = url.format({ - ...curUrl, - query: { - ...curUrl.query, - ...query, - }, - }) - const { Component: mod, getServerSideProps } = await loadComponents( - distDir, - page, - serverless - ) - - if (getServerSideProps) { - throw new Error( - `Error for page ${page}: ${SERVER_PROPS_EXPORT_ERROR}` - ) - } + if (curRenderOpts.inAmpMode && !curRenderOpts.ampSkipValidation) { + if (!results.ssgNotFound) { + await validateAmp(html, path, curRenderOpts.ampValidatorPath) + } + } else if (curRenderOpts.hybridAmp) { + // we need to render the AMP version + let ampHtmlFilename = `${ampPath}${sep}index.html` + if (!subFolders) { + ampHtmlFilename = `${ampPath}.html` + } + const ampBaseDir = join(outDir, dirname(ampHtmlFilename)) + const ampHtmlFilepath = join(outDir, ampHtmlFilename) - // if it was auto-exported the HTML is loaded here - if (typeof mod === 'string') { - html = mod - queryWithAutoExportWarn() - } else { - // for non-dynamic SSG pages we should have already - // prerendered the file - if (renderedDuringBuild((mod as ComponentModule).getStaticProps)) - return results - - if ( - (mod as ComponentModule).getStaticProps && - !htmlFilepath.endsWith('.html') - ) { - // make sure it ends with .html if the name contains a dot - htmlFilename += '.html' - htmlFilepath += '.html' - } - - renderMethod = (mod as ComponentModule).renderReqToHTML - const result = await renderMethod( + try { + await promises.access(ampHtmlFilepath) + } catch (_) { + // make sure it doesn't exist from manual mapping + let ampHtml + if (serverless) { + req.url += (req.url!.includes('?') ? '&' : '?') + 'amp=1' + // @ts-ignore + ampHtml = ( + await (renderMethod as any)( req, res, 'export', - { - ampPath: renderAmpPath, - /// @ts-ignore - optimizeFonts, - /// @ts-ignore - optimizeImages, - /// @ts-ignore - optimizeCss, - distDir, - fontManifest: optimizeFonts - ? requireFontManifest(distDir, serverless) - : null, - locale: locale!, - locales: renderOpts.locales!, - }, - // @ts-ignore + curRenderOpts, params ) - curRenderOpts = (result as any).renderOpts || {} - html = (result as any).html - } - - if (!html && !(curRenderOpts as any).isNotFound) { - throw new Error(`Failed to render serverless page`) - } + ).html } else { - const components = await loadComponents(distDir, page, serverless) - - if (components.getServerSideProps) { - throw new Error( - `Error for page ${page}: ${SERVER_PROPS_EXPORT_ERROR}` - ) - } - - // for non-dynamic SSG pages we should have already - // prerendered the file - if (renderedDuringBuild(components.getStaticProps)) { - return results - } - - // TODO: de-dupe the logic here between serverless and server mode - if (components.getStaticProps && !htmlFilepath.endsWith('.html')) { - // make sure it ends with .html if the name contains a dot - htmlFilepath += '.html' - htmlFilename += '.html' - } - - if (typeof components.Component === 'string') { - html = components.Component - queryWithAutoExportWarn() - } else { - /** - * This sets environment variable to be used at the time of static export by head.tsx. - * Using this from process.env allows targeting both serverless and SSR by calling - * `process.env.__NEXT_OPTIMIZE_FONTS`. - * TODO(prateekbh@): Remove this when experimental.optimizeFonts are being cleaned up. - */ - if (optimizeFonts) { - process.env.__NEXT_OPTIMIZE_FONTS = JSON.stringify(true) - } - if (optimizeImages) { - process.env.__NEXT_OPTIMIZE_IMAGES = JSON.stringify(true) - } - if (optimizeCss) { - process.env.__NEXT_OPTIMIZE_CSS = JSON.stringify(true) - } - curRenderOpts = { - ...components, - ...renderOpts, - ampPath: renderAmpPath, - params, - optimizeFonts, - optimizeImages, - optimizeCss, - fontManifest: optimizeFonts - ? requireFontManifest(distDir, serverless) - : null, - locale: locale as string, - } + ampHtml = await renderMethod( + req, + res, + page, // @ts-ignore - html = await renderMethod(req, res, page, query, curRenderOpts) - } - } - results.ssgNotFound = (curRenderOpts as any).isNotFound - - const validateAmp = async ( - rawAmpHtml: string, - ampPageName: string, - validatorPath?: string - ) => { - const validator = await AmpHtmlValidator.getInstance(validatorPath) - const result = validator.validateString(rawAmpHtml) - const errors = result.errors.filter((e) => e.severity === 'ERROR') - const warnings = result.errors.filter((e) => e.severity !== 'ERROR') - - if (warnings.length || errors.length) { - results.ampValidations.push({ - page: ampPageName, - result: { - errors, - warnings, - }, - }) - } - } - - if (curRenderOpts.inAmpMode && !curRenderOpts.ampSkipValidation) { - if (!results.ssgNotFound) { - await validateAmp(html, path, curRenderOpts.ampValidatorPath) - } - } else if (curRenderOpts.hybridAmp) { - // we need to render the AMP version - let ampHtmlFilename = `${ampPath}${sep}index.html` - if (!subFolders) { - ampHtmlFilename = `${ampPath}.html` - } - const ampBaseDir = join(outDir, dirname(ampHtmlFilename)) - const ampHtmlFilepath = join(outDir, ampHtmlFilename) - - try { - await promises.access(ampHtmlFilepath) - } catch (_) { - // make sure it doesn't exist from manual mapping - let ampHtml - if (serverless) { - req.url += (req.url!.includes('?') ? '&' : '?') + 'amp=1' - // @ts-ignore - ampHtml = ( - await (renderMethod as any)( - req, - res, - 'export', - curRenderOpts, - params - ) - ).html - } else { - ampHtml = await renderMethod( - req, - res, - page, - // @ts-ignore - { ...query, amp: '1' }, - curRenderOpts as any - ) - } - - if (!curRenderOpts.ampSkipValidation) { - await validateAmp(ampHtml, page + '?amp=1') - } - await promises.mkdir(ampBaseDir, { recursive: true }) - await promises.writeFile(ampHtmlFilepath, ampHtml, 'utf8') - } - } - - if ((curRenderOpts as any).pageData) { - const dataFile = join( - pagesDataDir, - htmlFilename.replace(/\.html$/, '.json') - ) - - await promises.mkdir(dirname(dataFile), { recursive: true }) - await promises.writeFile( - dataFile, - JSON.stringify((curRenderOpts as any).pageData), - 'utf8' + { ...query, amp: '1' }, + curRenderOpts as any ) - - if (curRenderOpts.hybridAmp) { - await promises.writeFile( - dataFile.replace(/\.json$/, '.amp.json'), - JSON.stringify((curRenderOpts as any).pageData), - 'utf8' - ) - } } - results.fromBuildExportRevalidate = (curRenderOpts as any).revalidate - if (results.ssgNotFound) { - // don't attempt writing to disk if getStaticProps returned not found - return results + if (!curRenderOpts.ampSkipValidation) { + await validateAmp(ampHtml, page + '?amp=1') } - await promises.writeFile(htmlFilepath, html, 'utf8') - return results - } catch (error) { - console.error( - `\nError occurred prerendering page "${path}". Read more: https://err.sh/next.js/prerender-error\n` + - error.stack + await promises.mkdir(ampBaseDir, { recursive: true }) + await promises.writeFile(ampHtmlFilepath, ampHtml, 'utf8') + } + } + + if ((curRenderOpts as any).pageData) { + const dataFile = join( + pagesDataDir, + htmlFilename.replace(/\.html$/, '.json') + ) + + await promises.mkdir(dirname(dataFile), { recursive: true }) + await promises.writeFile( + dataFile, + JSON.stringify((curRenderOpts as any).pageData), + 'utf8' + ) + + if (curRenderOpts.hybridAmp) { + await promises.writeFile( + dataFile.replace(/\.json$/, '.amp.json'), + JSON.stringify((curRenderOpts as any).pageData), + 'utf8' ) - return { ...results, error: true } } - }) + } + results.fromBuildExportRevalidate = (curRenderOpts as any).revalidate + + if (results.ssgNotFound) { + // don't attempt writing to disk if getStaticProps returned not found + return results + } + await promises.writeFile(htmlFilepath, html, 'utf8') + return results + } catch (error) { + console.error( + `\nError occurred prerendering page "${path}". Read more: https://err.sh/next.js/prerender-error\n` + + error.stack + ) + return { ...results, error: true } } - ) + }) } diff --git a/packages/next/telemetry/trace/autoparent.ts b/packages/next/telemetry/trace/autoparent.ts new file mode 100644 index 0000000000000..f33bbcfb1062a --- /dev/null +++ b/packages/next/telemetry/trace/autoparent.ts @@ -0,0 +1,71 @@ +import { trace, Span } from './trace' + +const stacks = new WeakMap>() +const stoppedSpansSets = new WeakMap>() + +export function stackPush(keyObj: any, spanName: string, attrs?: any): Span { + let stack = stacks.get(keyObj) + let span + + if (!stack) { + stack = [] + stacks.set(keyObj, stack) + span = trace(spanName, undefined, attrs ? attrs() : undefined) + } else { + const parent = stack[stack.length - 1] + if (parent) { + span = trace(spanName, parent.id, attrs ? attrs() : undefined) + } else { + span = trace(spanName, undefined, attrs ? attrs() : undefined) + } + } + + stack.push(span) + return span +} + +export function stackPop(keyObj: any, span: any): void { + let stack = stacks.get(keyObj) + if (!stack) { + console.warn( + 'Attempted to pop from non-existent stack. Key reference must be bad.' + ) + return + } + + let stoppedSpans = stoppedSpansSets.get(keyObj) + if (!stoppedSpans) { + stoppedSpans = new Set() + stoppedSpansSets.set(keyObj, stoppedSpans) + } + if (stoppedSpans.has(span)) { + console.warn( + `Attempted to terminate tracing span that was already stopped for ${span.name}` + ) + return + } + + while (true) { + let poppedSpan = stack.pop() + + if (poppedSpan && poppedSpan === span) { + stoppedSpans.add(poppedSpan) + span.stop() + stoppedSpans.add(span) + break + } else if (poppedSpan === undefined || stack.indexOf(span) === -1) { + // We've either reached the top of the stack or the stack doesn't contain + // the span for another reason. + console.warn(`Tracing span was not found in stack for: ${span.name}`) + stoppedSpans.add(span) + span.stop() + break + } else if (stack.indexOf(span) !== -1) { + console.warn( + `Attempted to pop span that was not at top of stack for: ${span.name}` + ) + stoppedSpans.add(poppedSpan) + poppedSpan.stop() + } + } +} diff --git a/packages/next/telemetry/trace/index.ts b/packages/next/telemetry/trace/index.ts index c55a1ddb74607..03f7f5c09ec5e 100644 --- a/packages/next/telemetry/trace/index.ts +++ b/packages/next/telemetry/trace/index.ts @@ -1,65 +1,24 @@ -import { randomBytes } from 'crypto' -import reportToConsole from './to-console' -import reportToLocalHost from './to-localhost' -import reportToTelemetry from './to-telemetry' +import { trace, Span, SpanStatus } from './trace' +import { noop } from './report' +import { SpanId } from './types' +import { stackPush, stackPop } from './autoparent' -let idCounter = 0 -const idUniqToProcess = randomBytes(16).toString('base64').slice(0, 22) -const getId = () => `${idUniqToProcess}:${idCounter++}` - -const noop = ( - _spanName: string, - _duration: number, - _id: string | null, - _parentId: string | null, - _attrs: Object -) => {} - -enum TARGET { - CONSOLE = 'CONSOLE', - LOCALHOST = 'LOCALHOST', - TELEMETRY = 'TELEMETRY', -} - -const target = - process.env.TRACE_TARGET && process.env.TRACE_TARGET in TARGET - ? TARGET[process.env.TRACE_TARGET as TARGET] - : TARGET.TELEMETRY const traceLevel = process.env.TRACE_LEVEL ? Number.parseInt(process.env.TRACE_LEVEL) : 1 - -let report = noop -if (target === TARGET.CONSOLE) { - report = reportToConsole -} else if (target === TARGET.LOCALHOST) { - report = reportToLocalHost -} else { - report = reportToTelemetry -} - -// This function reports durations in microseconds. This gives 1000x -// the precision of something like Date.now(), which reports in -// milliseconds. Additionally, ~285 years can be safely represented -// as microseconds as a float64 in both JSON and JavaScript. -const trace = (spanName: string, parentId: string | null, attrs: Object) => { - const endSpan = () => { - const id = getId() - const end: bigint = process.hrtime.bigint() - const duration = end - start - if (duration > Number.MAX_SAFE_INTEGER) { - throw new Error(`Duration is too long to express as float64: ${duration}`) - } - report(spanName, Number(duration), id, parentId, attrs) - } - - const start: bigint = process.hrtime.bigint() - return endSpan -} - -module.exports = { - TARGET, - primary: traceLevel >= 1 ? trace : noop, - secondary: traceLevel >= 2 ? trace : noop, - sensitive: traceLevel >= 3 ? trace : noop, +const primary = traceLevel >= 1 ? trace : noop +const secondary = traceLevel >= 2 ? trace : noop +const sensitive = traceLevel >= 3 ? trace : noop + +export { + trace, + traceLevel, + primary, + secondary, + sensitive, + SpanId, + Span, + SpanStatus, + stackPush, + stackPop, } diff --git a/packages/next/telemetry/trace/report/index.ts b/packages/next/telemetry/trace/report/index.ts new file mode 100644 index 0000000000000..677569445c244 --- /dev/null +++ b/packages/next/telemetry/trace/report/index.ts @@ -0,0 +1,26 @@ +import { TARGET, SpanId } from '../types' +import reportToConsole from './to-console' +import reportToLocalHost from './to-localhost' +import reportToTelemetry from './to-telemetry' + +export const noop = ( + _spanName: string, + _duration: number, + _id: SpanId, + _parentId?: SpanId, + _attrs?: Object +) => {} + +const target = + process.env.TRACE_TARGET && process.env.TRACE_TARGET in TARGET + ? TARGET[process.env.TRACE_TARGET as TARGET] + : TARGET.TELEMETRY + +export let report = noop +if (target === TARGET.CONSOLE) { + report = reportToConsole +} else if (target === TARGET.LOCALHOST) { + report = reportToLocalHost +} else { + report = reportToTelemetry +} diff --git a/packages/next/telemetry/trace/report/to-console.ts b/packages/next/telemetry/trace/report/to-console.ts new file mode 100644 index 0000000000000..74ee483b80d6a --- /dev/null +++ b/packages/next/telemetry/trace/report/to-console.ts @@ -0,0 +1,25 @@ +const idToName = new Map() + +const reportToConsole = ( + spanName: string, + duration: number, + id: string, + parentId?: string, + attrs?: Object +) => { + idToName.set(id, spanName) + + const parentStr = + parentId && idToName.has(parentId) + ? `, parent: ${idToName.get(parentId)}` + : '' + const attrsStr = attrs + ? `, ${Object.entries(attrs) + .map(([key, val]) => `${key}: ${val}`) + .join(', ')}` + : '' + + console.log(`[trace] ${spanName} took ${duration} μs${parentStr}${attrsStr}`) +} + +export default reportToConsole diff --git a/packages/next/telemetry/trace/to-localhost.ts b/packages/next/telemetry/trace/report/to-localhost.ts similarity index 74% rename from packages/next/telemetry/trace/to-localhost.ts rename to packages/next/telemetry/trace/report/to-localhost.ts index 115917aab0ec7..ce0aad0ef4382 100644 --- a/packages/next/telemetry/trace/to-localhost.ts +++ b/packages/next/telemetry/trace/report/to-localhost.ts @@ -2,8 +2,8 @@ const reportToLocalHost = ( _spanName: string, _duration: number, _id: string | null, - _parentId: string | null, - _attrs: Object + _parentId?: string, + _attrs?: Object ) => {} export default reportToLocalHost diff --git a/packages/next/telemetry/trace/to-telemetry.ts b/packages/next/telemetry/trace/report/to-telemetry.ts similarity index 74% rename from packages/next/telemetry/trace/to-telemetry.ts rename to packages/next/telemetry/trace/report/to-telemetry.ts index 371a9f34f7ba8..4240fb3682122 100644 --- a/packages/next/telemetry/trace/to-telemetry.ts +++ b/packages/next/telemetry/trace/report/to-telemetry.ts @@ -2,8 +2,8 @@ const reportToTelemetry = ( _spanName: string, _duration: number, _id: string | null, - _parentId: string | null, - _attrs: Object + _parentId?: string, + _attrs?: Object ) => {} export default reportToTelemetry diff --git a/packages/next/telemetry/trace/to-console.ts b/packages/next/telemetry/trace/to-console.ts deleted file mode 100644 index c77c25785dde2..0000000000000 --- a/packages/next/telemetry/trace/to-console.ts +++ /dev/null @@ -1,9 +0,0 @@ -const reportToConsole = ( - _spanName: string, - _duration: number, - _id: string | null, - _parentId: string | null, - _attrs: Object -) => {} - -export default reportToConsole diff --git a/packages/next/telemetry/trace/trace.ts b/packages/next/telemetry/trace/trace.ts new file mode 100644 index 0000000000000..2161c8f446ab6 --- /dev/null +++ b/packages/next/telemetry/trace/trace.ts @@ -0,0 +1,77 @@ +import { randomBytes } from 'crypto' +import { SpanId } from './types' +import { report } from './report' + +const ONE_THOUSAND = BigInt('1000') + +let idCounter = 0 +const idUniqToProcess = randomBytes(16).toString('base64').slice(0, 22) +const getId = () => `${idUniqToProcess}:${idCounter++}` + +export enum SpanStatus { + Started, + Stopped, +} + +export class Span { + name: string + id: SpanId + parentId?: SpanId + duration: number | null + attrs: { [key: string]: any } + status: SpanStatus + + _start: bigint + + constructor(name: string, parentId?: SpanId, attrs?: Object) { + this.name = name + this.parentId = parentId + this.duration = null + this.attrs = attrs ? { ...attrs } : {} + this.status = SpanStatus.Started + this.id = getId() + this._start = process.hrtime.bigint() + } + + stop() { + const end: bigint = process.hrtime.bigint() + const duration = (end - this._start) / ONE_THOUSAND + this.status = SpanStatus.Stopped + if (duration > Number.MAX_SAFE_INTEGER) { + throw new Error(`Duration is too long to express as float64: ${duration}`) + } + report(this.name, Number(duration), this.id, this.parentId, this.attrs) + } + + traceChild(name: string, attrs?: Object) { + return new Span(name, this.id, attrs) + } + + setAttribute(key: string, value: any) { + this.attrs[key] = value + } + + traceFn(fn: any) { + try { + return fn() + } finally { + this.stop() + } + } + + async traceAsyncFn(fn: any) { + try { + return await fn() + } finally { + this.stop() + } + } +} + +// This function reports durations in microseconds. This gives 1000x +// the precision of something like Date.now(), which reports in +// milliseconds. Additionally, ~285 years can be safely represented +// as microseconds as a float64 in both JSON and JavaScript. +export const trace = (name: string, parentId?: SpanId, attrs?: Object) => { + return new Span(name, parentId, attrs) +} diff --git a/packages/next/telemetry/trace/types.ts b/packages/next/telemetry/trace/types.ts new file mode 100644 index 0000000000000..8459abb339911 --- /dev/null +++ b/packages/next/telemetry/trace/types.ts @@ -0,0 +1,7 @@ +export enum TARGET { + CONSOLE = 'CONSOLE', + LOCALHOST = 'LOCALHOST', + TELEMETRY = 'TELEMETRY', +} + +export type SpanId = string From 42bd4ef4037d8fc41814a58cfa19e0c9f20f7c62 Mon Sep 17 00:00:00 2001 From: Dale Bustad Date: Tue, 2 Mar 2021 20:55:44 -0800 Subject: [PATCH 03/16] Remove old trace commands, misc cleanup. --- package.json | 2 -- packages/next/bin/next.ts | 2 ++ .../next/build/webpack/loaders/babel-loader/src/cache.js | 4 ++-- packages/next/telemetry/trace/trace.ts | 8 ++++---- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index f795c568cf8ff..09c3db1ce14c2 100644 --- a/package.json +++ b/package.json @@ -34,8 +34,6 @@ "publish-stable": "lerna version --force-publish", "lint-staged": "lint-staged", "next": "node --trace-deprecation packages/next/dist/bin/next", - "trace": "node --trace-deprecation -r ./bench/instrument.js packages/next/dist/bin/next", - "trace-debug": "node --inspect --trace-deprecation -r ./bench/instrument.js packages/next/dist/bin/next", "debug": "node --inspect packages/next/dist/bin/next" }, "pre-commit": "lint-staged", diff --git a/packages/next/bin/next.ts b/packages/next/bin/next.ts index c97020cb8bc5e..f153fbbb9881e 100755 --- a/packages/next/bin/next.ts +++ b/packages/next/bin/next.ts @@ -106,6 +106,8 @@ if (typeof React.Suspense === 'undefined') { process.on('SIGTERM', () => process.exit(0)) process.on('SIGINT', () => process.exit(0)) +commands[command]().then((exec) => exec(forwardedArgs)) + if (command === 'dev') { const { CONFIG_FILE } = require('../next-server/lib/constants') const { watchFile } = require('fs') diff --git a/packages/next/build/webpack/loaders/babel-loader/src/cache.js b/packages/next/build/webpack/loaders/babel-loader/src/cache.js index 570b67affea52..a29a65a5a3f1c 100644 --- a/packages/next/build/webpack/loaders/babel-loader/src/cache.js +++ b/packages/next/build/webpack/loaders/babel-loader/src/cache.js @@ -4,14 +4,14 @@ import transform from './transform' import cacache from 'next/dist/compiled/cacache' async function read(cacheDirectory, etag) { - const cachedResult = await trace('read-cache-file').traceAsyncFn( + const cachedResult = await trace('read-cache-file').traceAsyncFn(() => cacache.get(cacheDirectory, etag) ) return JSON.parse(cachedResult.data) } function write(cacheDirectory, etag, data) { - return trace('write-cache-file').traceAsyncFn( + return trace('write-cache-file').traceAsyncFn(() => cacache.put(cacheDirectory, etag, JSON.stringify(data)) ) } diff --git a/packages/next/telemetry/trace/trace.ts b/packages/next/telemetry/trace/trace.ts index 2161c8f446ab6..901a6eb0e4da9 100644 --- a/packages/next/telemetry/trace/trace.ts +++ b/packages/next/telemetry/trace/trace.ts @@ -33,6 +33,10 @@ export class Span { this._start = process.hrtime.bigint() } + // Durations are reported as microseconds. This gives 1000x the precision + // of something like Date.now(), which reports in milliseconds. + // Additionally, ~285 years can be safely represented as microseconds as + // a float64 in both JSON and JavaScript. stop() { const end: bigint = process.hrtime.bigint() const duration = (end - this._start) / ONE_THOUSAND @@ -68,10 +72,6 @@ export class Span { } } -// This function reports durations in microseconds. This gives 1000x -// the precision of something like Date.now(), which reports in -// milliseconds. Additionally, ~285 years can be safely represented -// as microseconds as a float64 in both JSON and JavaScript. export const trace = (name: string, parentId?: SpanId, attrs?: Object) => { return new Span(name, parentId, attrs) } From b8b963db667e560d80fb00e658bcf41ad2d9f3c1 Mon Sep 17 00:00:00 2001 From: Dale Bustad Date: Tue, 2 Mar 2021 23:25:27 -0800 Subject: [PATCH 04/16] Add Zipkin as tracing target. --- packages/next/telemetry/trace/report/index.ts | 7 +-- .../next/telemetry/trace/report/to-console.ts | 1 + .../telemetry/trace/report/to-localhost.ts | 9 ---- .../telemetry/trace/report/to-telemetry.ts | 1 + .../next/telemetry/trace/report/to-zipkin.ts | 45 +++++++++++++++++++ packages/next/telemetry/trace/trace.ts | 18 +++++--- packages/next/telemetry/trace/types.ts | 2 +- 7 files changed, 64 insertions(+), 19 deletions(-) delete mode 100644 packages/next/telemetry/trace/report/to-localhost.ts create mode 100644 packages/next/telemetry/trace/report/to-zipkin.ts diff --git a/packages/next/telemetry/trace/report/index.ts b/packages/next/telemetry/trace/report/index.ts index 677569445c244..35a194b457b27 100644 --- a/packages/next/telemetry/trace/report/index.ts +++ b/packages/next/telemetry/trace/report/index.ts @@ -1,11 +1,12 @@ import { TARGET, SpanId } from '../types' import reportToConsole from './to-console' -import reportToLocalHost from './to-localhost' +import reportToZipkin from './to-zipkin' import reportToTelemetry from './to-telemetry' export const noop = ( _spanName: string, _duration: number, + _timestamp: number, _id: SpanId, _parentId?: SpanId, _attrs?: Object @@ -19,8 +20,8 @@ const target = export let report = noop if (target === TARGET.CONSOLE) { report = reportToConsole -} else if (target === TARGET.LOCALHOST) { - report = reportToLocalHost +} else if (target === TARGET.ZIPKIN) { + report = reportToZipkin } else { report = reportToTelemetry } diff --git a/packages/next/telemetry/trace/report/to-console.ts b/packages/next/telemetry/trace/report/to-console.ts index 74ee483b80d6a..1f53bfd53acf2 100644 --- a/packages/next/telemetry/trace/report/to-console.ts +++ b/packages/next/telemetry/trace/report/to-console.ts @@ -3,6 +3,7 @@ const idToName = new Map() const reportToConsole = ( spanName: string, duration: number, + _timestamp: number, id: string, parentId?: string, attrs?: Object diff --git a/packages/next/telemetry/trace/report/to-localhost.ts b/packages/next/telemetry/trace/report/to-localhost.ts deleted file mode 100644 index ce0aad0ef4382..0000000000000 --- a/packages/next/telemetry/trace/report/to-localhost.ts +++ /dev/null @@ -1,9 +0,0 @@ -const reportToLocalHost = ( - _spanName: string, - _duration: number, - _id: string | null, - _parentId?: string, - _attrs?: Object -) => {} - -export default reportToLocalHost diff --git a/packages/next/telemetry/trace/report/to-telemetry.ts b/packages/next/telemetry/trace/report/to-telemetry.ts index 4240fb3682122..f4e27defc33cc 100644 --- a/packages/next/telemetry/trace/report/to-telemetry.ts +++ b/packages/next/telemetry/trace/report/to-telemetry.ts @@ -1,6 +1,7 @@ const reportToTelemetry = ( _spanName: string, _duration: number, + _timestamp: number, _id: string | null, _parentId?: string, _attrs?: Object diff --git a/packages/next/telemetry/trace/report/to-zipkin.ts b/packages/next/telemetry/trace/report/to-zipkin.ts new file mode 100644 index 0000000000000..4a6f05fb55d7a --- /dev/null +++ b/packages/next/telemetry/trace/report/to-zipkin.ts @@ -0,0 +1,45 @@ +import { randomBytes } from 'crypto' +import fetch from 'node-fetch' + +let traceId = process.env.TRACE_ID +if (!traceId) { + traceId = process.env.TRACE_ID = randomBytes(8).toString('hex') +} + +const localEndpoint = { + serviceName: 'zipkin-query', + ipv4: '127.0.0.1', + port: 9411, +} +const zipkinUrl = `http://${localEndpoint.ipv4}:${localEndpoint.port}/api/v2/spans` + +const reportToLocalHost = ( + name: string, + duration: number, + timestamp: number, + id: string, + parentId?: string, + attrs?: Object +) => { + const body = [ + { + traceId, + parentId, + name, + id, + timestamp, + duration, + localEndpoint, + tags: attrs, + }, + ] + + // We intentionally do not block on I/O here. + fetch(zipkinUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }) +} + +export default reportToLocalHost diff --git a/packages/next/telemetry/trace/trace.ts b/packages/next/telemetry/trace/trace.ts index 901a6eb0e4da9..1578e0c953eea 100644 --- a/packages/next/telemetry/trace/trace.ts +++ b/packages/next/telemetry/trace/trace.ts @@ -2,11 +2,9 @@ import { randomBytes } from 'crypto' import { SpanId } from './types' import { report } from './report' -const ONE_THOUSAND = BigInt('1000') +const NUM_OF_MICROSEC_IN_SEC = BigInt('1000') -let idCounter = 0 -const idUniqToProcess = randomBytes(16).toString('base64').slice(0, 22) -const getId = () => `${idUniqToProcess}:${idCounter++}` +const getId = () => randomBytes(8).toString('hex') export enum SpanStatus { Started, @@ -39,12 +37,20 @@ export class Span { // a float64 in both JSON and JavaScript. stop() { const end: bigint = process.hrtime.bigint() - const duration = (end - this._start) / ONE_THOUSAND + const duration = (end - this._start) / NUM_OF_MICROSEC_IN_SEC this.status = SpanStatus.Stopped if (duration > Number.MAX_SAFE_INTEGER) { throw new Error(`Duration is too long to express as float64: ${duration}`) } - report(this.name, Number(duration), this.id, this.parentId, this.attrs) + const timestamp = this._start / NUM_OF_MICROSEC_IN_SEC + report( + this.name, + Number(duration), + Number(timestamp), + this.id, + this.parentId, + this.attrs + ) } traceChild(name: string, attrs?: Object) { diff --git a/packages/next/telemetry/trace/types.ts b/packages/next/telemetry/trace/types.ts index 8459abb339911..037aa969658a8 100644 --- a/packages/next/telemetry/trace/types.ts +++ b/packages/next/telemetry/trace/types.ts @@ -1,6 +1,6 @@ export enum TARGET { CONSOLE = 'CONSOLE', - LOCALHOST = 'LOCALHOST', + ZIPKIN = 'ZIPKIN', TELEMETRY = 'TELEMETRY', } From 93a59c3f842f0e03d5fee8975dcc10d98b11b46a Mon Sep 17 00:00:00 2001 From: Dale Bustad Date: Wed, 3 Mar 2021 00:28:22 -0800 Subject: [PATCH 05/16] Add whitelisted telemetry target for tracing. --- packages/next/build/index.ts | 3 +- packages/next/server/next-dev-server.ts | 3 + packages/next/telemetry/trace/index.ts | 3 +- packages/next/telemetry/trace/report/index.ts | 2 +- .../telemetry/trace/report/to-telemetry.ts | 33 ++++-- packages/next/telemetry/trace/shared.ts | 12 ++ packages/next/telemetry/trace/trace.ts | 2 +- packages/next/telemetry/trace/types.ts | 7 -- yarn.lock | 105 +----------------- 9 files changed, 47 insertions(+), 123 deletions(-) create mode 100644 packages/next/telemetry/trace/shared.ts delete mode 100644 packages/next/telemetry/trace/types.ts diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index 6082f8297d229..95b74863b526b 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -70,7 +70,7 @@ import { generateBuildId } from './generate-build-id' import { isWriteable } from './is-writeable' import * as Log from './output/log' import createSpinner from './spinner' -import { trace } from '../telemetry/trace' +import { trace, setGlobal } from '../telemetry/trace' import { collectPages, detectConflictingPaths, @@ -156,6 +156,7 @@ export default async function build( }) const telemetry = new Telemetry({ distDir }) + setGlobal('telemetry', telemetry) const publicDir = path.join(dir, 'public') const pagesDir = findPagesDir(dir) diff --git a/packages/next/server/next-dev-server.ts b/packages/next/server/next-dev-server.ts index 684cf43e39fda..fae4b50203983 100644 --- a/packages/next/server/next-dev-server.ts +++ b/packages/next/server/next-dev-server.ts @@ -34,6 +34,7 @@ import { normalizePagePath } from '../next-server/server/normalize-page-path' import Router, { Params, route } from '../next-server/server/router' import { eventCliSession } from '../telemetry/events' import { Telemetry } from '../telemetry/storage' +import { setGlobal } from '../telemetry/trace' import HotReloader from './hot-reloader' import { findPageFile } from './lib/find-page-file' import { getNodeOptionsWithoutInspect } from './lib/utils' @@ -297,6 +298,8 @@ export default class DevServer extends Server { isCustomServer: this.isCustomServer, }) ) + // This is required by the tracing subsystem. + setGlobal('telemetry', telemetry) } protected async close(): Promise { diff --git a/packages/next/telemetry/trace/index.ts b/packages/next/telemetry/trace/index.ts index 03f7f5c09ec5e..bb36c91c658e4 100644 --- a/packages/next/telemetry/trace/index.ts +++ b/packages/next/telemetry/trace/index.ts @@ -1,6 +1,6 @@ import { trace, Span, SpanStatus } from './trace' import { noop } from './report' -import { SpanId } from './types' +import { SpanId, setGlobal } from './shared' import { stackPush, stackPop } from './autoparent' const traceLevel = process.env.TRACE_LEVEL @@ -21,4 +21,5 @@ export { SpanStatus, stackPush, stackPop, + setGlobal, } diff --git a/packages/next/telemetry/trace/report/index.ts b/packages/next/telemetry/trace/report/index.ts index 35a194b457b27..e6a3a32406d18 100644 --- a/packages/next/telemetry/trace/report/index.ts +++ b/packages/next/telemetry/trace/report/index.ts @@ -1,4 +1,4 @@ -import { TARGET, SpanId } from '../types' +import { TARGET, SpanId } from '../shared' import reportToConsole from './to-console' import reportToZipkin from './to-zipkin' import reportToTelemetry from './to-telemetry' diff --git a/packages/next/telemetry/trace/report/to-telemetry.ts b/packages/next/telemetry/trace/report/to-telemetry.ts index f4e27defc33cc..51d46ca59637e 100644 --- a/packages/next/telemetry/trace/report/to-telemetry.ts +++ b/packages/next/telemetry/trace/report/to-telemetry.ts @@ -1,10 +1,27 @@ -const reportToTelemetry = ( - _spanName: string, - _duration: number, - _timestamp: number, - _id: string | null, - _parentId?: string, - _attrs?: Object -) => {} +import { traceGlobals } from '../shared' + +const TRACE_EVENT_WHITELIST = new Map( + Object.entries({ + 'webpack-invalidated': 'WEBPACK_INVALIDATED', + }) +) + +const reportToTelemetry = (spanName: string, duration: number) => { + const eventName = TRACE_EVENT_WHITELIST.get(spanName) + if (!eventName) { + return + } + const telemetry = traceGlobals.get('telemetry') + if (!telemetry) { + return + } + + telemetry.record({ + eventName, + payload: { + durationInMicroseconds: duration, + }, + }) +} export default reportToTelemetry diff --git a/packages/next/telemetry/trace/shared.ts b/packages/next/telemetry/trace/shared.ts new file mode 100644 index 0000000000000..149d01907a83d --- /dev/null +++ b/packages/next/telemetry/trace/shared.ts @@ -0,0 +1,12 @@ +export enum TARGET { + CONSOLE = 'CONSOLE', + ZIPKIN = 'ZIPKIN', + TELEMETRY = 'TELEMETRY', +} + +export type SpanId = string + +export const traceGlobals: Map = new Map() +export const setGlobal = (key, val) => { + traceGlobals.set(key, val) +} diff --git a/packages/next/telemetry/trace/trace.ts b/packages/next/telemetry/trace/trace.ts index 1578e0c953eea..24a3eda1136d1 100644 --- a/packages/next/telemetry/trace/trace.ts +++ b/packages/next/telemetry/trace/trace.ts @@ -1,5 +1,5 @@ import { randomBytes } from 'crypto' -import { SpanId } from './types' +import { SpanId } from './shared' import { report } from './report' const NUM_OF_MICROSEC_IN_SEC = BigInt('1000') diff --git a/packages/next/telemetry/trace/types.ts b/packages/next/telemetry/trace/types.ts deleted file mode 100644 index 037aa969658a8..0000000000000 --- a/packages/next/telemetry/trace/types.ts +++ /dev/null @@ -1,7 +0,0 @@ -export enum TARGET { - CONSOLE = 'CONSOLE', - ZIPKIN = 'ZIPKIN', - TELEMETRY = 'TELEMETRY', -} - -export type SpanId = string diff --git a/yarn.lock b/yarn.lock index 5423dfc08ee61..0e99788b11068 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2488,103 +2488,18 @@ "@octokit/openapi-types" "^4.0.2" "@types/node" ">= 8" -"@opentelemetry/api@0.14.0", "@opentelemetry/api@^0.14.0": +"@opentelemetry/api@0.14.0": version "0.14.0" resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-0.14.0.tgz#4e17d8d2f1da72b19374efa7b6526aa001267cae" integrity sha512-L7RMuZr5LzMmZiQSQDy9O1jo0q+DaLy6XpYJfIGfYSfoJA5qzYwUP3sP1uMIQ549DvxAgM3ng85EaPTM/hUHwQ== dependencies: "@opentelemetry/context-base" "^0.14.0" -"@opentelemetry/context-async-hooks@^0.14.0": - version "0.14.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/context-async-hooks/-/context-async-hooks-0.14.0.tgz#34b77e87060bea2489b16f55168c35a580c5de50" - integrity sha512-BgAPWbmelvL/xhI0B17aidyw24E1D69PlKjDZD1L5AI050Glg1+pa6W0jMSoZltnM27Bc+Pc9n22fqxa4kvLRg== - dependencies: - "@opentelemetry/context-base" "^0.14.0" - "@opentelemetry/context-base@^0.14.0": version "0.14.0" resolved "https://registry.yarnpkg.com/@opentelemetry/context-base/-/context-base-0.14.0.tgz#c67fc20a4d891447ca1a855d7d70fa79a3533001" integrity sha512-sDOAZcYwynHFTbLo6n8kIbLiVF3a3BLkrmehJUyEbT9F+Smbi47kLGS2gG2g0fjBLR/Lr1InPD7kXL7FaTqEkw== -"@opentelemetry/core@^0.14.0": - version "0.14.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-0.14.0.tgz#35870e58e3d084dfc4dea99c973f3cf7479b3c6a" - integrity sha512-HJ4VM0cV6c5qjdW7C7koB2IT4ADunCOehxnKFRslQkbDqAEA1w42AZ9679siYALpWYxNqcJyqF2jxCNtfNHa6Q== - dependencies: - "@opentelemetry/api" "^0.14.0" - "@opentelemetry/context-base" "^0.14.0" - semver "^7.1.3" - -"@opentelemetry/exporter-zipkin@0.14.0": - version "0.14.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-zipkin/-/exporter-zipkin-0.14.0.tgz#35281d96bd101c0c1a48837868ba8249a0332f7f" - integrity sha512-cg/86iRK64NSGEcWOTzTfqMByyAu8udi3HgcYP2r0WF8JseabfhrlnBp/4/ba4HuoaGe8uXVRmvDT0uAguhO8w== - dependencies: - "@opentelemetry/api" "^0.14.0" - "@opentelemetry/core" "^0.14.0" - "@opentelemetry/resources" "^0.14.0" - "@opentelemetry/tracing" "^0.14.0" - -"@opentelemetry/node@0.14.0": - version "0.14.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/node/-/node-0.14.0.tgz#20af865de191a6b14443d619bb08d49003e64b19" - integrity sha512-79egldZKRIqXArjLHD8SYTJUfY5PrcFZll4+ykfbz23vy/CXEVev+1S8bS8bkE08DUNzskDuiFYlLdWTY8fdrQ== - dependencies: - "@opentelemetry/api" "^0.14.0" - "@opentelemetry/context-async-hooks" "^0.14.0" - "@opentelemetry/core" "^0.14.0" - "@opentelemetry/tracing" "^0.14.0" - require-in-the-middle "^5.0.0" - semver "^7.1.3" - -"@opentelemetry/plugin-http@0.14.0", "@opentelemetry/plugin-http@^0.14.0": - version "0.14.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/plugin-http/-/plugin-http-0.14.0.tgz#5c5ac23bd3a5cbd8891fce857d52a86bb0dbf51c" - integrity sha512-j6VHSotl+geN+znYgGgn/GohTAirAtHum8Zpc52VZ/WQCrNfHcmEf4NYaGCuz5tW2EgmbH+66iDM/lgNadQuQg== - dependencies: - "@opentelemetry/api" "^0.14.0" - "@opentelemetry/core" "^0.14.0" - "@opentelemetry/semantic-conventions" "^0.14.0" - semver "^7.1.3" - shimmer "^1.2.1" - -"@opentelemetry/plugin-https@0.14.0": - version "0.14.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/plugin-https/-/plugin-https-0.14.0.tgz#60a0db56339de497b35aaae0de91b10ecff9b0ea" - integrity sha512-FyT3iaEO9CdGu1EeIe+JmAwfoYwjG9GtHiyWSovFEKO/0OLQLrXA2AQDgltOeelu/snPGB2MF92iPqbpfVdhHw== - dependencies: - "@opentelemetry/api" "^0.14.0" - "@opentelemetry/core" "^0.14.0" - "@opentelemetry/plugin-http" "^0.14.0" - "@opentelemetry/semantic-conventions" "^0.14.0" - semver "^7.1.3" - shimmer "^1.2.1" - -"@opentelemetry/resources@^0.14.0": - version "0.14.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-0.14.0.tgz#e89378931b4e02d4b6fb526d237d9594db2bb68a" - integrity sha512-7XVML4HxvoH6kWY+x0mhMc5m0a2YBvPCTSX7yAqyp9XIGvFpdjzAE2ggJ40DZrL1sPv9f0QYAbnIKFDVLBTfGA== - dependencies: - "@opentelemetry/api" "^0.14.0" - "@opentelemetry/core" "^0.14.0" - -"@opentelemetry/semantic-conventions@^0.14.0": - version "0.14.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-0.14.0.tgz#7b48f4300709b34cef0beed8099b79e4a5907886" - integrity sha512-iDGRLQwo+ka1ljlLo4KyuUmzsJwtPw+PyCjetQwn3m/pTXjdyWLGOTARBrpQGpkQp7k87RaCCg5AqZaKFU2G6g== - -"@opentelemetry/tracing@0.14.0", "@opentelemetry/tracing@^0.14.0": - version "0.14.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/tracing/-/tracing-0.14.0.tgz#f1e528b90def213a454ca410ca2d460acfacbe3f" - integrity sha512-sw9lXJQUQO0xaCvHWFUzIhgh2jGFgXubRQ6g1O84Q/ILU93ZMMVt+d97mihcrtBrV89Sy39HF8tAwekjpzv+cA== - dependencies: - "@opentelemetry/api" "^0.14.0" - "@opentelemetry/context-base" "^0.14.0" - "@opentelemetry/core" "^0.14.0" - "@opentelemetry/resources" "^0.14.0" - "@opentelemetry/semantic-conventions" "^0.14.0" - "@polka/url@^1.0.0-next.9": version "1.0.0-next.11" resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.11.tgz#aeb16f50649a91af79dbe36574b66d0f9e4d9f71" @@ -11071,10 +10986,6 @@ modify-values@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022" -module-details-from-path@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/module-details-from-path/-/module-details-from-path-1.0.3.tgz#114c949673e2a8a35e9d35788527aa37b679da2b" - moment@^2.24.0: version "2.24.0" resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" @@ -13990,15 +13901,6 @@ require-from-string@^1.1.0: version "1.2.1" resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-1.2.1.tgz#529c9ccef27380adfec9a2f965b649bbee636418" -require-in-the-middle@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/require-in-the-middle/-/require-in-the-middle-5.1.0.tgz#b768f800377b47526d026bbf5a7f727f16eb412f" - integrity sha512-M2rLKVupQfJ5lf9OvqFGIT+9iVLnTmjgbOmpil12hiSQNn5zJTKGPoIisETNjfK+09vP3rpm1zJajmErpr2sEQ== - dependencies: - debug "^4.1.1" - module-details-from-path "^1.0.3" - resolve "^1.12.0" - require-like@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/require-like/-/require-like-0.1.2.tgz#ad6f30c13becd797010c468afa775c0c0a6b47fa" @@ -14614,11 +14516,6 @@ shellwords@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" -shimmer@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/shimmer/-/shimmer-1.2.1.tgz#610859f7de327b587efebf501fb43117f9aff337" - integrity sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw== - side-channel@^1.0.2, side-channel@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" From 7878aac19b225160e169ae115e34fa690dd5eb4a Mon Sep 17 00:00:00 2001 From: Dale Bustad Date: Wed, 3 Mar 2021 00:37:30 -0800 Subject: [PATCH 06/16] Remove unused trace features. --- packages/next/telemetry/trace/index.ts | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/packages/next/telemetry/trace/index.ts b/packages/next/telemetry/trace/index.ts index bb36c91c658e4..4f03fdcaa5bb1 100644 --- a/packages/next/telemetry/trace/index.ts +++ b/packages/next/telemetry/trace/index.ts @@ -1,25 +1,5 @@ import { trace, Span, SpanStatus } from './trace' -import { noop } from './report' import { SpanId, setGlobal } from './shared' import { stackPush, stackPop } from './autoparent' -const traceLevel = process.env.TRACE_LEVEL - ? Number.parseInt(process.env.TRACE_LEVEL) - : 1 -const primary = traceLevel >= 1 ? trace : noop -const secondary = traceLevel >= 2 ? trace : noop -const sensitive = traceLevel >= 3 ? trace : noop - -export { - trace, - traceLevel, - primary, - secondary, - sensitive, - SpanId, - Span, - SpanStatus, - stackPush, - stackPop, - setGlobal, -} +export { trace, SpanId, Span, SpanStatus, stackPush, stackPop, setGlobal } From d6e8bea21d4ecd34573482ec6c8b2899fd44b7de Mon Sep 17 00:00:00 2001 From: Dale Bustad Date: Wed, 3 Mar 2021 01:03:39 -0800 Subject: [PATCH 07/16] Always enable profiling in ProfilingPlugin. --- .../next/build/webpack/plugins/profiling-plugin.ts | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/packages/next/build/webpack/plugins/profiling-plugin.ts b/packages/next/build/webpack/plugins/profiling-plugin.ts index db179a11a1d84..f19dd38a9ed86 100644 --- a/packages/next/build/webpack/plugins/profiling-plugin.ts +++ b/packages/next/build/webpack/plugins/profiling-plugin.ts @@ -1,11 +1,5 @@ import { webpack, isWebpack5 } from 'next/dist/compiled/webpack/webpack' -import { - traceLevel, - trace, - stackPush, - stackPop, - Span, -} from '../../../telemetry/trace' +import { trace, stackPush, stackPop, Span } from '../../../telemetry/trace' const pluginName = 'ProfilingPlugin' export const spans = new WeakMap() @@ -23,10 +17,6 @@ export class ProfilingPlugin { compiler: any apply(compiler: any) { - // Only enable plugin when instrumentation is loaded - if (traceLevel === 0) { - return - } this.traceTopLevelHooks(compiler) this.traceCompilationHooks(compiler) this.compiler = compiler From c4896bb32ebe86f4242cc5a92580a4d33d413afd Mon Sep 17 00:00:00 2001 From: Dale Bustad Date: Wed, 3 Mar 2021 01:05:04 -0800 Subject: [PATCH 08/16] Explicitly allow type any for the setGlobal map. --- packages/next/telemetry/trace/shared.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/next/telemetry/trace/shared.ts b/packages/next/telemetry/trace/shared.ts index 149d01907a83d..73aa00e2af36d 100644 --- a/packages/next/telemetry/trace/shared.ts +++ b/packages/next/telemetry/trace/shared.ts @@ -7,6 +7,6 @@ export enum TARGET { export type SpanId = string export const traceGlobals: Map = new Map() -export const setGlobal = (key, val) => { +export const setGlobal = (key: any, val: any) => { traceGlobals.set(key, val) } From 0e6e5839c7c9491358ce52cad919d1915c6c684f Mon Sep 17 00:00:00 2001 From: Dale Bustad Date: Wed, 3 Mar 2021 10:57:35 -0800 Subject: [PATCH 09/16] Change implicity any to explicit any. --- packages/next/build/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index 95b74863b526b..3ccd5c54c7f7e 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -340,9 +340,9 @@ export default async function build( version: 3, pages404: true, basePath: config.basePath, - redirects: redirects.map((r) => buildCustomRoute(r, 'redirect')), - rewrites: rewrites.map((r) => buildCustomRoute(r, 'rewrite')), - headers: headers.map((r) => buildCustomRoute(r, 'header')), + redirects: redirects.map((r: any) => buildCustomRoute(r, 'redirect')), + rewrites: rewrites.map((r: any) => buildCustomRoute(r, 'rewrite')), + headers: headers.map((r: any) => buildCustomRoute(r, 'header')), dynamicRoutes: getSortedRoutes(pageKeys) .filter(isDynamicRoute) .map((page) => { From afa2e4b3c96fbf06c3b8dc52b91457df67815db3 Mon Sep 17 00:00:00 2001 From: Dale Bustad Date: Tue, 9 Mar 2021 22:27:21 -0800 Subject: [PATCH 10/16] Emit trace info to STDOUT instead of STDERR. --- packages/next/telemetry/trace/autoparent.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/next/telemetry/trace/autoparent.ts b/packages/next/telemetry/trace/autoparent.ts index f33bbcfb1062a..e508488914e96 100644 --- a/packages/next/telemetry/trace/autoparent.ts +++ b/packages/next/telemetry/trace/autoparent.ts @@ -27,7 +27,7 @@ export function stackPush(keyObj: any, spanName: string, attrs?: any): Span { export function stackPop(keyObj: any, span: any): void { let stack = stacks.get(keyObj) if (!stack) { - console.warn( + console.info( 'Attempted to pop from non-existent stack. Key reference must be bad.' ) return @@ -39,7 +39,7 @@ export function stackPop(keyObj: any, span: any): void { stoppedSpansSets.set(keyObj, stoppedSpans) } if (stoppedSpans.has(span)) { - console.warn( + console.info( `Attempted to terminate tracing span that was already stopped for ${span.name}` ) return @@ -56,12 +56,12 @@ export function stackPop(keyObj: any, span: any): void { } else if (poppedSpan === undefined || stack.indexOf(span) === -1) { // We've either reached the top of the stack or the stack doesn't contain // the span for another reason. - console.warn(`Tracing span was not found in stack for: ${span.name}`) + console.info(`Tracing span was not found in stack for: ${span.name}`) stoppedSpans.add(span) span.stop() break } else if (stack.indexOf(span) !== -1) { - console.warn( + console.info( `Attempted to pop span that was not at top of stack for: ${span.name}` ) stoppedSpans.add(poppedSpan) From 22612ed7c92cf501a0194abe3297dc9859d22a40 Mon Sep 17 00:00:00 2001 From: Dale Bustad Date: Tue, 9 Mar 2021 23:58:45 -0800 Subject: [PATCH 11/16] Don't assume module context will always be available. --- packages/next/build/webpack/loaders/babel-loader/src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/next/build/webpack/loaders/babel-loader/src/index.js b/packages/next/build/webpack/loaders/babel-loader/src/index.js index 04c3cf6a7a449..9e87d5d589220 100644 --- a/packages/next/build/webpack/loaders/babel-loader/src/index.js +++ b/packages/next/build/webpack/loaders/babel-loader/src/index.js @@ -23,7 +23,7 @@ export default function makeLoader(callback) { async function loader(source, inputSourceMap, overrides) { // this.currentTraceSpan is provided by profiling-plugin.ts - const loaderSpan = trace('babel-loader', this.currentTraceSpan.id) + const loaderSpan = trace('babel-loader', this.currentTraceSpan?.id) return loaderSpan.traceAsyncFn(async () => { const filename = this.resourcePath From 5947170a46f63701bfcf35fa76535aefa2876215 Mon Sep 17 00:00:00 2001 From: Dale Bustad Date: Wed, 10 Mar 2021 00:35:57 -0800 Subject: [PATCH 12/16] Pass correct spanId to worker. --- packages/next/build/index.ts | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index 3ccd5c54c7f7e..31170682e724f 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -665,21 +665,20 @@ export default async function build( if (nonReservedPage) { try { - let workerResult = await checkPageSpan - .traceChild('is-page-static') - .traceAsyncFn(() => { - const spanContext = {} - - return staticCheckWorkers.isPageStatic( - page, - distDir, - isLikeServerless, - runtimeEnvConfig, - config.i18n?.locales, - config.i18n?.defaultLocale, - spanContext - ) - }) + let isPageStaticSpan = checkPageSpan.traceChild( + 'is-page-static' + ) + let workerResult = isPageStaticSpan.traceAsyncFn(() => { + return staticCheckWorkers.isPageStatic( + page, + distDir, + isLikeServerless, + runtimeEnvConfig, + config.i18n?.locales, + config.i18n?.defaultLocale, + isPageStaticSpan.id + ) + }) if ( workerResult.isStatic === false && From c88abebd29c212d2b992069f06931e0d898faab2 Mon Sep 17 00:00:00 2001 From: Dale Bustad Date: Wed, 10 Mar 2021 00:39:16 -0800 Subject: [PATCH 13/16] Add missing awaits. --- packages/next/build/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index 31170682e724f..0e57931bd042e 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -599,7 +599,7 @@ export default async function build( const nonStaticErrorPageSpan = staticCheckSpan.traceChild( 'check-static-error-page' ) - const nonStaticErrorPage = nonStaticErrorPageSpan.traceAsyncFn( + const nonStaticErrorPage = await nonStaticErrorPageSpan.traceAsyncFn( async () => hasCustomErrorPage && (await hasCustomGetInitialProps( @@ -668,7 +668,7 @@ export default async function build( let isPageStaticSpan = checkPageSpan.traceChild( 'is-page-static' ) - let workerResult = isPageStaticSpan.traceAsyncFn(() => { + let workerResult = await isPageStaticSpan.traceAsyncFn(() => { return staticCheckWorkers.isPageStatic( page, distDir, From 6e55bf2fc3a83b2d95a8b8acaf5e08589f6a8c3a Mon Sep 17 00:00:00 2001 From: Dale Bustad Date: Wed, 10 Mar 2021 00:50:11 -0800 Subject: [PATCH 14/16] Remove debug artifact. --- packages/next/export/index.ts | 1 - packages/next/telemetry/trace/report/index.ts | 18 +++++++++--------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/next/export/index.ts b/packages/next/export/index.ts index 9b54d9d1d3b3d..aaee1cfc66aab 100644 --- a/packages/next/export/index.ts +++ b/packages/next/export/index.ts @@ -276,7 +276,6 @@ export default async function exportApp( if (!options.silent) { Log.info('Copying "static" directory') } - /******************/ await nextExportSpan .traceChild('copy-static-directory') .traceAsyncFn(() => diff --git a/packages/next/telemetry/trace/report/index.ts b/packages/next/telemetry/trace/report/index.ts index e6a3a32406d18..9640b24f153a8 100644 --- a/packages/next/telemetry/trace/report/index.ts +++ b/packages/next/telemetry/trace/report/index.ts @@ -3,21 +3,21 @@ import reportToConsole from './to-console' import reportToZipkin from './to-zipkin' import reportToTelemetry from './to-telemetry' -export const noop = ( - _spanName: string, - _duration: number, - _timestamp: number, - _id: SpanId, - _parentId?: SpanId, - _attrs?: Object -) => {} +type Reporter = ( + spanName: string, + duration: number, + timestamp: number, + id: SpanId, + parentId?: SpanId, + attrs?: Object +) => void const target = process.env.TRACE_TARGET && process.env.TRACE_TARGET in TARGET ? TARGET[process.env.TRACE_TARGET as TARGET] : TARGET.TELEMETRY -export let report = noop +export let report: Reporter if (target === TARGET.CONSOLE) { report = reportToConsole } else if (target === TARGET.ZIPKIN) { From 74449b614ecfd054ec483615a7d827a0789c6313 Mon Sep 17 00:00:00 2001 From: Dale Bustad Date: Wed, 10 Mar 2021 12:18:39 -0800 Subject: [PATCH 15/16] Add message for when TRACE_TARGET has invalid value. --- packages/next/telemetry/trace/report/index.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/next/telemetry/trace/report/index.ts b/packages/next/telemetry/trace/report/index.ts index 9640b24f153a8..2f8130cab963e 100644 --- a/packages/next/telemetry/trace/report/index.ts +++ b/packages/next/telemetry/trace/report/index.ts @@ -17,6 +17,12 @@ const target = ? TARGET[process.env.TRACE_TARGET as TARGET] : TARGET.TELEMETRY +if (process.env.TRACE_TARGET && !target) { + console.info( + 'For TRACE_TARGET, please specify one of: CONSOLE, ZIPKIN, TELEMETRY' + ) +} + export let report: Reporter if (target === TARGET.CONSOLE) { report = reportToConsole From ff337ccb82dad5a6aeb1218a5d920641cdab6541 Mon Sep 17 00:00:00 2001 From: Dale Bustad Date: Wed, 10 Mar 2021 12:27:09 -0800 Subject: [PATCH 16/16] Address PR feedback. --- packages/next/build/webpack/plugins/profiling-plugin.ts | 2 +- packages/next/telemetry/trace/report/to-zipkin.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/next/build/webpack/plugins/profiling-plugin.ts b/packages/next/build/webpack/plugins/profiling-plugin.ts index f19dd38a9ed86..8bab027fcec6a 100644 --- a/packages/next/build/webpack/plugins/profiling-plugin.ts +++ b/packages/next/build/webpack/plugins/profiling-plugin.ts @@ -63,7 +63,7 @@ export class ProfilingPlugin { compiler.hooks.compile, compiler.hooks.done, () => { - return { attributes: { name: compiler.name } } + return { name: compiler.name } }, (span) => spans.set(compiler, span) ) diff --git a/packages/next/telemetry/trace/report/to-zipkin.ts b/packages/next/telemetry/trace/report/to-zipkin.ts index 4a6f05fb55d7a..c65ab8279272e 100644 --- a/packages/next/telemetry/trace/report/to-zipkin.ts +++ b/packages/next/telemetry/trace/report/to-zipkin.ts @@ -39,7 +39,7 @@ const reportToLocalHost = ( method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), - }) + }).catch(() => {}) } export default reportToLocalHost