diff --git a/packages/build/src/core/report_metrics.ts b/packages/build/src/core/report_metrics.ts index 63a8c592c4..89135fb23b 100644 --- a/packages/build/src/core/report_metrics.ts +++ b/packages/build/src/core/report_metrics.ts @@ -1,7 +1,7 @@ import { closeClient, formatTags, InputStatsDOptions, startClient, validateStatsDOptions } from '../report/statsd.js' export type Metric = { - type: 'increment' + type: 'increment' | 'timing' name: string value: number tags: Record diff --git a/packages/build/src/plugins_core/edge_functions/index.ts b/packages/build/src/plugins_core/edge_functions/index.ts index b6ff4e9331..e5dd15075e 100644 --- a/packages/build/src/plugins_core/edge_functions/index.ts +++ b/packages/build/src/plugins_core/edge_functions/index.ts @@ -165,10 +165,23 @@ const getMetrics = (manifest): Metric[] => { const numUserEfs = totalEfs.length - numGenEfs - return [ + const metrics: Metric[] = [ { type: 'increment', name: 'buildbot.build.functions', value: numGenEfs, tags: { type: 'edge:generated' } }, { type: 'increment', name: 'buildbot.build.functions', value: numUserEfs, tags: { type: 'edge:user' } }, ] + + const tarballDurationMs = manifest.bundling_timing?.tarball_ms + + if (typeof tarballDurationMs === 'number') { + metrics.push({ + type: 'timing', + name: 'buildbot.build.edge_functions.bundling_ms', + value: tarballDurationMs, + tags: { format: 'tarball' }, + }) + } + + return metrics } // We run this core step if at least one of the functions directories (the // one configured by the user or the internal one) exists. We use a dynamic diff --git a/packages/edge-bundler/node/bundler.test.ts b/packages/edge-bundler/node/bundler.test.ts index 3df83ac861..b354cb333b 100644 --- a/packages/edge-bundler/node/bundler.test.ts +++ b/packages/edge-bundler/node/bundler.test.ts @@ -787,6 +787,8 @@ describe.skipIf(lt(denoVersion, '2.4.3'))( const manifestFile = await readFile(resolve(distPath, 'manifest.json'), 'utf8') const manifest = JSON.parse(manifestFile) + expect(manifest.bundling_timing).toEqual({ tarball_ms: expect.any(Number) }) + const tarballPath = join(distPath, manifest.bundles[0].asset) const tarballResult = await runTarball(tarballPath) expect(tarballResult).toStrictEqual(expectedOutput) @@ -879,6 +881,7 @@ describe.skipIf(lt(denoVersion, '2.4.3'))( const manifestFile = await readFile(resolve(distPath, 'manifest.json'), 'utf8') const manifest = JSON.parse(manifestFile) + expect(manifest.bundling_timing).toEqual({ tarball_ms: expect.any(Number) }) expect(manifest.bundles.length).toBe(1) expect(manifest.bundles[0].format).toBe('eszip2') diff --git a/packages/edge-bundler/node/bundler.ts b/packages/edge-bundler/node/bundler.ts index 804fd182ba..9eb7430707 100644 --- a/packages/edge-bundler/node/bundler.ts +++ b/packages/edge-bundler/node/bundler.ts @@ -128,19 +128,28 @@ export const bundle = async ( } const bundles: Bundle[] = [] + let tarballBundleDurationMs: number | undefined if (featureFlags.edge_bundler_generate_tarball || featureFlags.edge_bundler_dry_run_generate_tarball) { - const tarballPromise = bundleTarball({ - basePath, - buildID, - debug, - deno, - distDirectory, - functions, - featureFlags, - importMap: importMap.clone(), - vendorDirectory: vendor?.directory, - }) + const tarballPromise = (async () => { + const start = Date.now() + + try { + return await bundleTarball({ + basePath, + buildID, + debug, + deno, + distDirectory, + functions, + featureFlags, + importMap: importMap.clone(), + vendorDirectory: vendor?.directory, + }) + } finally { + tarballBundleDurationMs = Date.now() - start + } + })() if (featureFlags.edge_bundler_dry_run_generate_tarball) { try { @@ -214,6 +223,7 @@ export const bundle = async ( internalFunctionConfig, importMap: importMapSpecifier, layers: deployConfig.layers, + bundlingTiming: tarballBundleDurationMs === undefined ? undefined : { tarball_ms: tarballBundleDurationMs }, }) await vendor?.cleanup() diff --git a/packages/edge-bundler/node/manifest.ts b/packages/edge-bundler/node/manifest.ts index 09039b963a..241cc33fa0 100644 --- a/packages/edge-bundler/node/manifest.ts +++ b/packages/edge-bundler/node/manifest.ts @@ -49,9 +49,14 @@ export interface EdgeFunctionConfig { traffic_rules?: TrafficRules } +interface BundlingTiming { + tarball_ms?: number +} + interface Manifest { bundler_version: string bundles: { asset: string; format: string }[] + bundling_timing?: BundlingTiming import_map?: string layers: { name: string; flag: string }[] routes: Route[] @@ -68,6 +73,7 @@ interface GenerateManifestOptions { internalFunctionConfig?: Record layers?: Layer[] userFunctionConfig?: Record + bundlingTiming?: BundlingTiming } const removeEmptyConfigValues = (functionConfig: EdgeFunctionConfig) => @@ -149,6 +155,7 @@ const generateManifest = ({ internalFunctionConfig = {}, importMap, layers = [], + bundlingTiming, }: GenerateManifestOptions) => { const preCacheRoutes: Route[] = [] const postCacheRoutes: Route[] = [] @@ -259,6 +266,9 @@ const generateManifest = ({ layers, import_map: importMap, function_config: sanitizeEdgeFunctionConfig(manifestFunctionConfig), + ...(bundlingTiming && Object.values(bundlingTiming).some((value) => value !== undefined) + ? { bundling_timing: bundlingTiming } + : {}), } const unroutedFunctions = functions.filter(({ name }) => !routedFunctions.has(name)).map(({ name }) => name) diff --git a/packages/edge-bundler/node/validation/manifest/schema.ts b/packages/edge-bundler/node/validation/manifest/schema.ts index 9ed1a60d4e..19310d6712 100644 --- a/packages/edge-bundler/node/validation/manifest/schema.ts +++ b/packages/edge-bundler/node/validation/manifest/schema.ts @@ -18,6 +18,14 @@ const excludedPatternsSchema = { }, } +const bundlingTimingSchema = { + type: 'object', + properties: { + tarball_ms: { type: 'number', minimum: 0 }, + }, + additionalProperties: false, +} + const headersSchema = { type: 'object', patternProperties: { @@ -112,6 +120,7 @@ const edgeManifestSchema = { }, import_map: { type: 'string' }, bundler_version: { type: 'string' }, + bundling_timing: bundlingTimingSchema, function_config: { type: 'object', additionalProperties: functionConfigSchema }, }, additionalProperties: false,