diff --git a/packages/core/core/src/PackagerRunner.js b/packages/core/core/src/PackagerRunner.js index 7876f275c5e..8fc1c60f268 100644 --- a/packages/core/core/src/PackagerRunner.js +++ b/packages/core/core/src/PackagerRunner.js @@ -396,47 +396,51 @@ export default class PackagerRunner { let packager = await this.config.getPackager(bundle.name); let {name, resolveFrom, plugin} = packager; - let measurement; try { - measurement = tracer.createMeasurement(name, 'packaging', bundle.name, { - type: bundle.type, - }); - return await plugin.package({ - config: configs.get(name)?.result, - bundleConfig: bundleConfigs.get(name)?.result, - bundle, - bundleGraph: new BundleGraph( - bundleGraph, - NamedBundle.get.bind(NamedBundle), - this.options, - ), - getSourceMapReference: map => { - return this.getSourceMapReference(bundle, map); - }, - options: this.pluginOptions, - logger: new PluginLogger({origin: name}), - tracer: new PluginTracer({origin: name, category: 'package'}), - getInlineBundleContents: async ( - bundle: BundleType, - bundleGraph: BundleGraphType, - ) => { - if (bundle.bundleBehavior !== 'inline') { - throw new Error( - 'Bundle is not inline and unable to retrieve contents', - ); - } - - let res = await this.getBundleResult( - bundleToInternalBundle(bundle), - // $FlowFixMe - bundleGraphToInternalBundleGraph(bundleGraph), - configs, - bundleConfigs, - ); - - return {contents: res.contents}; + return await tracer.measure( + { + name, + args: {bundle: {name: bundle.name, type: bundle.type}}, + categories: ['packaging'], }, - }); + () => + plugin.package({ + config: configs.get(name)?.result, + bundleConfig: bundleConfigs.get(name)?.result, + bundle, + bundleGraph: new BundleGraph( + bundleGraph, + NamedBundle.get.bind(NamedBundle), + this.options, + ), + getSourceMapReference: map => { + return this.getSourceMapReference(bundle, map); + }, + options: this.pluginOptions, + logger: new PluginLogger({origin: name}), + tracer: new PluginTracer({origin: name, category: 'package'}), + getInlineBundleContents: async ( + bundle: BundleType, + bundleGraph: BundleGraphType, + ) => { + if (bundle.bundleBehavior !== 'inline') { + throw new Error( + 'Bundle is not inline and unable to retrieve contents', + ); + } + + let res = await this.getBundleResult( + bundleToInternalBundle(bundle), + // $FlowFixMe + bundleGraphToInternalBundleGraph(bundleGraph), + configs, + bundleConfigs, + ); + + return {contents: res.contents}; + }, + }), + ); } catch (e) { throw new ThrowableDiagnostic({ diagnostic: errorToDiagnostic(e, { @@ -445,7 +449,6 @@ export default class PackagerRunner { }), }); } finally { - measurement && measurement.end(); // Add dev dependency for the packager. This must be done AFTER running it due to // the potential for lazy require() that aren't executed until the request runs. let devDepRequest = await createDevDependency( @@ -503,34 +506,37 @@ export default class PackagerRunner { }; for (let optimizer of optimizers) { - let measurement; try { - measurement = tracer.createMeasurement( - optimizer.name, - 'optimize', - bundle.name, - ); - let next = await optimizer.plugin.optimize({ - config: configs.get(optimizer.name)?.result, - bundleConfig: bundleConfigs.get(optimizer.name)?.result, - bundle, - bundleGraph, - contents: optimized.contents, - map: optimized.map, - getSourceMapReference: map => { - return this.getSourceMapReference(bundle, map); + await tracer.measure( + { + name: optimizer.name, + args: {bundle: bundle.name}, + categories: ['optimize'], }, - options: this.pluginOptions, - logger: new PluginLogger({origin: optimizer.name}), - tracer: new PluginTracer({ - origin: optimizer.name, - category: 'optimize', - }), - }); - - optimized.type = next.type ?? optimized.type; - optimized.contents = next.contents; - optimized.map = next.map; + async () => { + let next = await optimizer.plugin.optimize({ + config: configs.get(optimizer.name)?.result, + bundleConfig: bundleConfigs.get(optimizer.name)?.result, + bundle, + bundleGraph, + contents: optimized.contents, + map: optimized.map, + getSourceMapReference: map => { + return this.getSourceMapReference(bundle, map); + }, + options: this.pluginOptions, + logger: new PluginLogger({origin: optimizer.name}), + tracer: new PluginTracer({ + origin: optimizer.name, + category: 'optimize', + }), + }); + + optimized.type = next.type ?? optimized.type; + optimized.contents = next.contents; + optimized.map = next.map; + }, + ); } catch (e) { throw new ThrowableDiagnostic({ diagnostic: errorToDiagnostic(e, { @@ -539,7 +545,6 @@ export default class PackagerRunner { }), }); } finally { - measurement && measurement.end(); // Add dev dependency for the optimizer. This must be done AFTER running it due to // the potential for lazy require() that aren't executed until the request runs. let devDepRequest = await createDevDependency( diff --git a/packages/core/core/src/ReporterRunner.js b/packages/core/core/src/ReporterRunner.js index 5bc7200122e..f39cc88b3d9 100644 --- a/packages/core/core/src/ReporterRunner.js +++ b/packages/core/core/src/ReporterRunner.js @@ -95,26 +95,31 @@ export default class ReporterRunner { } for (let reporter of this.reporters) { - let measurement; try { + let fn = () => + reporter.plugin.report({ + event, + options: this.pluginOptions, + logger: new PluginLogger({origin: reporter.name}), + tracer: new PluginTracer({ + origin: reporter.name, + category: 'reporter', + }), + }); + // To avoid an infinite loop we don't measure trace events, as they'll // result in another trace! - if (event.type !== 'trace') { - measurement = tracer.createMeasurement(reporter.name, 'reporter'); - } - await reporter.plugin.report({ - event, - options: this.pluginOptions, - logger: new PluginLogger({origin: reporter.name}), - tracer: new PluginTracer({ - origin: reporter.name, - category: 'reporter', - }), - }); + await (event.type === 'trace' + ? fn() + : tracer.measure( + { + name: reporter.name, + categories: ['reporter'], + }, + fn, + )); } catch (reportError) { INTERNAL_ORIGINAL_CONSOLE.error(reportError); - } finally { - measurement && measurement.end(); } } } catch (err) { diff --git a/packages/core/core/src/Transformation.js b/packages/core/core/src/Transformation.js index ca25f703d85..1e796728934 100644 --- a/packages/core/core/src/Transformation.js +++ b/packages/core/core/src/Transformation.js @@ -374,24 +374,26 @@ export default class Transformation { } try { - const measurement = tracer.createMeasurement( - transformer.name, - 'transform', - fromProjectPathRelative(initialAsset.value.filePath), - ); - - let transformerResults = await this.runTransformer( - pipeline, - asset, - transformer.plugin, - transformer.name, - transformer.config, - transformer.configKeyPath, - this.parcelConfig, + let transformerResults = await tracer.measure( + { + name: transformer.name, + args: { + filename: fromProjectPathRelative(initialAsset.value.filePath), + }, + categories: ['transform'], + }, + () => + this.runTransformer( + pipeline, + asset, + transformer.plugin, + transformer.name, + transformer.config, + transformer.configKeyPath, + this.parcelConfig, + ), ); - measurement && measurement.end(); - for (let result of transformerResults) { if (result instanceof UncommittedAsset) { resultingAssets.push(result); diff --git a/packages/core/core/src/applyRuntimes.js b/packages/core/core/src/applyRuntimes.js index 01bd9507ef7..28bed7a47c2 100644 --- a/packages/core/core/src/applyRuntimes.js +++ b/packages/core/core/src/applyRuntimes.js @@ -96,98 +96,100 @@ export default async function applyRuntimes({ for (let bundle of bundles) { for (let runtime of runtimes) { - let measurement; try { const namedBundle = NamedBundle.get(bundle, bundleGraph, options); - measurement = tracer.createMeasurement( - runtime.name, - 'applyRuntime', - namedBundle.displayName, - ); - let applied = await runtime.plugin.apply({ - bundle: namedBundle, - bundleGraph: new BundleGraph( - bundleGraph, - NamedBundle.get.bind(NamedBundle), - options, - ), - config: configs.get(runtime.name)?.result, - options: pluginOptions, - logger: new PluginLogger({origin: runtime.name}), - tracer: new PluginTracer({ - origin: runtime.name, - category: 'applyRuntime', - }), - }); - if (applied) { - let runtimeAssets = Array.isArray(applied) ? applied : [applied]; - for (let { - code, - dependency, - filePath, - isEntry, - env, - priority, - } of runtimeAssets) { - let sourceName = path.join( - path.dirname(filePath), - `runtime-${hashString(code)}.${bundle.type}`, - ); - - let assetGroup = { - code, - filePath: toProjectPath(options.projectRoot, sourceName), - env: mergeEnvironments(options.projectRoot, bundle.env, env), - // Runtime assets should be considered source, as they should be - // e.g. compiled to run in the target environment - isSource: true, - }; - - let connectionBundle = bundle; - - if (priority === 'parallel' && !bundle.needsStableName) { - let bundleGroups = - bundleGraph.getBundleGroupsContainingBundle(bundle); - - connectionBundle = nullthrows( - bundleGraph.createBundle({ - type: bundle.type, - needsStableName: false, - env: bundle.env, - target: bundle.target, - uniqueKey: 'runtime-manifest:' + bundle.id, - shouldContentHash: options.shouldContentHash, - }), - ); - - for (let bundleGroup of bundleGroups) { - bundleGraph.addBundleToBundleGroup( - connectionBundle, - bundleGroup, + await tracer.measure( + { + name: runtime.name, + args: {bundle: namedBundle.displayName}, + categories: ['applyRuntime'], + }, + async () => { + let applied = await runtime.plugin.apply({ + bundle: namedBundle, + bundleGraph: new BundleGraph( + bundleGraph, + NamedBundle.get.bind(NamedBundle), + options, + ), + config: configs.get(runtime.name)?.result, + options: pluginOptions, + logger: new PluginLogger({origin: runtime.name}), + tracer: new PluginTracer({ + origin: runtime.name, + category: 'applyRuntime', + }), + }); + + if (applied) { + let runtimeAssets = Array.isArray(applied) ? applied : [applied]; + for (let { + code, + dependency, + filePath, + isEntry, + env, + priority, + } of runtimeAssets) { + let sourceName = path.join( + path.dirname(filePath), + `runtime-${hashString(code)}.${bundle.type}`, ); - } - bundleGraph.createBundleReference(bundle, connectionBundle); - nameRuntimeBundle(connectionBundle, bundle); + let assetGroup = { + code, + filePath: toProjectPath(options.projectRoot, sourceName), + env: mergeEnvironments(options.projectRoot, bundle.env, env), + // Runtime assets should be considered source, as they should be + // e.g. compiled to run in the target environment + isSource: true, + }; + + let connectionBundle = bundle; + + if (priority === 'parallel' && !bundle.needsStableName) { + let bundleGroups = + bundleGraph.getBundleGroupsContainingBundle(bundle); + + connectionBundle = nullthrows( + bundleGraph.createBundle({ + type: bundle.type, + needsStableName: false, + env: bundle.env, + target: bundle.target, + uniqueKey: 'runtime-manifest:' + bundle.id, + shouldContentHash: options.shouldContentHash, + }), + ); + + for (let bundleGroup of bundleGroups) { + bundleGraph.addBundleToBundleGroup( + connectionBundle, + bundleGroup, + ); + } + bundleGraph.createBundleReference(bundle, connectionBundle); + + nameRuntimeBundle(connectionBundle, bundle); + } + + connections.push({ + bundle: connectionBundle, + assetGroup, + dependency, + isEntry, + }); + } } - - connections.push({ - bundle: connectionBundle, - assetGroup, - dependency, - isEntry, - }); - } - } + }, + ); } catch (e) { throw new ThrowableDiagnostic({ diagnostic: errorToDiagnostic(e, { origin: runtime.name, }), }); - } finally { - measurement && measurement.end(); } } } diff --git a/packages/core/core/src/requests/BundleGraphRequest.js b/packages/core/core/src/requests/BundleGraphRequest.js index 466f76a1a1c..96b4da74187 100644 --- a/packages/core/core/src/requests/BundleGraphRequest.js +++ b/packages/core/core/src/requests/BundleGraphRequest.js @@ -93,23 +93,29 @@ export default function createBundleGraphRequest( run: async input => { let {options, api, invalidateReason} = input; let {optionsRef, requestedAssetIds, signal} = input.input; - let measurement = tracer.createMeasurement('building'); - let request = createAssetGraphRequest({ - name: 'Main', - entries: options.entries, - optionsRef, - shouldBuildLazily: options.shouldBuildLazily, - lazyIncludes: options.lazyIncludes, - lazyExcludes: options.lazyExcludes, - requestedAssetIds, - }); - let {assetGraph, changedAssets, assetRequests} = await api.runRequest( - request, + + let {assetGraph, changedAssets, assetRequests} = await tracer.measure( { - force: options.shouldBuildLazily && requestedAssetIds.size > 0, + name: 'building', + categories: ['core'], + }, + () => { + let request = createAssetGraphRequest({ + name: 'Main', + entries: options.entries, + optionsRef, + shouldBuildLazily: options.shouldBuildLazily, + lazyIncludes: options.lazyIncludes, + lazyExcludes: options.lazyExcludes, + requestedAssetIds, + }); + + return api.runRequest(request, { + force: options.shouldBuildLazily && requestedAssetIds.size > 0, + }); }, ); - measurement && measurement.end(); + assertSignalNotAborted(signal); // If any subrequests are invalid (e.g. dev dep requests or config requests), @@ -137,14 +143,22 @@ export default function createBundleGraphRequest( let {devDeps, invalidDevDeps} = await getDevDepRequests(input.api); invalidateDevDeps(invalidDevDeps, input.options, parcelConfig); - let bundlingMeasurement = tracer.createMeasurement('bundling'); - let builder = new BundlerRunner(input, parcelConfig, devDeps); - let res: BundleGraphResult = await builder.bundle({ - graph: assetGraph, - changedAssets: changedAssets, - assetRequests, - }); - bundlingMeasurement && bundlingMeasurement.end(); + let res = await tracer.measure>( + { + name: 'bundling', + categories: ['core'], + }, + () => { + let builder = new BundlerRunner(input, parcelConfig, devDeps); + + return builder.bundle({ + graph: assetGraph, + changedAssets: changedAssets, + assetRequests, + }); + }, + ); + for (let [id, asset] of changedAssets) { res.changedAssets.set(id, asset); } @@ -255,8 +269,11 @@ class BundlerRunner { await this.loadConfigs(); - let plugin = await this.config.getBundler(); - let {plugin: bundler, name, resolveFrom} = plugin; + let { + plugin: bundler, + name: pluginName, + resolveFrom, + } = await this.config.getBundler(); // if a previous asset graph hash is passed in, check if the bundle graph is also available let previousBundleGraphResult: ?BundleGraphRequestResult; @@ -273,9 +290,9 @@ class BundlerRunner { let internalBundleGraph; - let logger = new PluginLogger({origin: name}); + let logger = new PluginLogger({origin: pluginName}); let tracer = new PluginTracer({ - origin: name, + origin: pluginName, category: 'bundle', }); try { @@ -307,55 +324,54 @@ class BundlerRunner { this.options, ); - let measurement; - let measurementFilename; + let filename; if (tracer.enabled) { - measurementFilename = graph + filename = graph .getEntryAssets() .map(asset => fromProjectPathRelative(asset.filePath)) .join(', '); - measurement = tracer.createMeasurement( - plugin.name, - 'bundling:bundle', - measurementFilename, - ); } // this the normal bundle workflow (bundle, optimizing, run-times, naming) - await bundler.bundle({ - bundleGraph: mutableBundleGraph, - config: this.configs.get(plugin.name)?.result, - options: this.pluginOptions, - logger, - tracer, - }); - - measurement && measurement.end(); - - if (this.pluginOptions.mode === 'production') { - let optimizeMeasurement; - try { - if (tracer.enabled) { - optimizeMeasurement = tracer.createMeasurement( - plugin.name, - 'bundling:optimize', - nullthrows(measurementFilename), - ); - } - await bundler.optimize({ + await tracer.measure( + { + name: pluginName, + categories: ['bundling:bundle'], + args: {filename}, + }, + () => + bundler.bundle({ bundleGraph: mutableBundleGraph, - config: this.configs.get(plugin.name)?.result, + config: this.configs.get(pluginName)?.result, options: this.pluginOptions, logger, - }); + tracer, + }), + ); + + if (this.pluginOptions.mode === 'production') { + try { + await tracer.measure( + { + name: pluginName, + categories: ['bundling:optimize'], + args: {filename}, + }, + () => + bundler.optimize({ + bundleGraph: mutableBundleGraph, + config: this.configs.get(pluginName)?.result, + options: this.pluginOptions, + logger, + }), + ); } catch (e) { throw new ThrowableDiagnostic({ diagnostic: errorToDiagnostic(e, { - origin: plugin.name, + origin: pluginName, }), }); } finally { - optimizeMeasurement && optimizeMeasurement.end(); await dumpGraphToGraphViz( // $FlowFixMe[incompatible-call] internalBundleGraph._graph, @@ -368,7 +384,7 @@ class BundlerRunner { // the potential for lazy require() that aren't executed until the request runs. let devDepRequest = await createDevDependency( { - specifier: name, + specifier: pluginName, resolveFrom, }, this.previousDevDeps, @@ -390,7 +406,7 @@ class BundlerRunner { throw new ThrowableDiagnostic({ diagnostic: errorToDiagnostic(e, { - origin: name, + origin: pluginName, }), }); } finally { @@ -498,17 +514,23 @@ class BundlerRunner { ); for (let namer of namers) { - let measurement; try { - measurement = tracer.createMeasurement(namer.name, 'namer', bundle.id); - let name = await namer.plugin.name({ - bundle, - bundleGraph, - config: this.configs.get(namer.name)?.result, - options: this.pluginOptions, - logger: new PluginLogger({origin: namer.name}), - tracer: new PluginTracer({origin: namer.name, category: 'namer'}), - }); + let name = await tracer.measure( + { + name: namer.name, + args: {bundle: bundle.id}, + categories: ['namer'], + }, + () => + namer.plugin.name({ + bundle, + bundleGraph, + config: this.configs.get(namer.name)?.result, + options: this.pluginOptions, + logger: new PluginLogger({origin: namer.name}), + tracer: new PluginTracer({origin: namer.name, category: 'namer'}), + }), + ); if (name != null) { internalBundle.name = name; @@ -525,8 +547,6 @@ class BundlerRunner { origin: namer.name, }), }); - } finally { - measurement && measurement.end(); } } diff --git a/packages/core/core/src/requests/ParcelBuildRequest.js b/packages/core/core/src/requests/ParcelBuildRequest.js index b3057590783..3f2c8c5a7c2 100644 --- a/packages/core/core/src/requests/ParcelBuildRequest.js +++ b/packages/core/core/src/requests/ParcelBuildRequest.js @@ -93,14 +93,21 @@ async function run({input, api, options}) { ), }); - let packagingMeasurement = tracer.createMeasurement('packaging'); - let writeBundlesRequest = createWriteBundlesRequest({ - bundleGraph, - optionsRef, - }); + let bundleInfo = await tracer.measure( + { + name: 'packaging', + categories: ['core'], + }, + () => { + let writeBundlesRequest = createWriteBundlesRequest({ + bundleGraph, + optionsRef, + }); + + return api.runRequest(writeBundlesRequest); + }, + ); - let bundleInfo = await api.runRequest(writeBundlesRequest); - packagingMeasurement && packagingMeasurement.end(); assertSignalNotAborted(signal); return {bundleGraph, bundleInfo, changedAssets, assetRequests}; diff --git a/packages/core/core/src/requests/PathRequest.js b/packages/core/core/src/requests/PathRequest.js index 5917443e01d..5b9f4e74b56 100644 --- a/packages/core/core/src/requests/PathRequest.js +++ b/packages/core/core/src/requests/PathRequest.js @@ -282,26 +282,27 @@ export class ResolverRunner { let invalidateOnFileChange = []; let invalidateOnEnvChange = []; for (let resolver of resolvers) { - let measurement; try { - measurement = tracer.createMeasurement( - resolver.name, - 'resolve', - specifier, + let result = await tracer.measure( + { + name: resolver.name, + args: {specifier}, + categories: ['resolve'], + }, + () => + resolver.plugin.resolve({ + specifier, + pipeline, + dependency: dep, + options: this.pluginOptions, + logger: new PluginLogger({origin: resolver.name}), + tracer: new PluginTracer({ + origin: resolver.name, + category: 'resolver', + }), + config: this.configs.get(resolver.name)?.result, + }), ); - let result = await resolver.plugin.resolve({ - specifier, - pipeline, - dependency: dep, - options: this.pluginOptions, - logger: new PluginLogger({origin: resolver.name}), - tracer: new PluginTracer({ - origin: resolver.name, - category: 'resolver', - }), - config: this.configs.get(resolver.name)?.result, - }); - measurement && measurement.end(); if (result) { if (result.meta) { @@ -399,8 +400,6 @@ export class ResolverRunner { break; } finally { - measurement && measurement.end(); - // Add dev dependency for the resolver. This must be done AFTER running it due to // the potential for lazy require() that aren't executed until the request runs. let devDepRequest = await createDevDependency( diff --git a/packages/core/core/src/requests/WriteBundleRequest.js b/packages/core/core/src/requests/WriteBundleRequest.js index 1b8266cd995..a7083afaab8 100644 --- a/packages/core/core/src/requests/WriteBundleRequest.js +++ b/packages/core/core/src/requests/WriteBundleRequest.js @@ -246,35 +246,41 @@ async function runCompressor( devDeps: Map, api: RunAPI, ) { - let measurement; try { - measurement = tracer.createMeasurement( - compressor.name, - 'compress', - path.relative(options.projectRoot, filePath), + await tracer.measure( + { + name: compressor.name, + args: {filename: path.relative(options.projectRoot, filePath)}, + categories: ['compress'], + }, + async () => { + let res = await compressor.plugin.compress({ + stream, + options: new PluginOptions(options), + logger: new PluginLogger({origin: compressor.name}), + tracer: new PluginTracer({ + origin: compressor.name, + category: 'compress', + }), + }); + + if (res != null) { + await new Promise((resolve, reject) => + pipeline( + res.stream, + outputFS.createWriteStream( + filePath + (res.type != null ? '.' + res.type : ''), + writeOptions, + ), + err => { + if (err) reject(err); + else resolve(); + }, + ), + ); + } + }, ); - let res = await compressor.plugin.compress({ - stream, - options: new PluginOptions(options), - logger: new PluginLogger({origin: compressor.name}), - tracer: new PluginTracer({origin: compressor.name, category: 'compress'}), - }); - - if (res != null) { - await new Promise((resolve, reject) => - pipeline( - res.stream, - outputFS.createWriteStream( - filePath + (res.type != null ? '.' + res.type : ''), - writeOptions, - ), - err => { - if (err) reject(err); - else resolve(); - }, - ), - ); - } } catch (err) { throw new ThrowableDiagnostic({ diagnostic: errorToDiagnostic(err, { @@ -282,7 +288,6 @@ async function runCompressor( }), }); } finally { - measurement && measurement.end(); // Add dev deps for compressor plugins AFTER running them, to account for lazy require(). let devDepRequest = await createDevDependency( { diff --git a/packages/core/profiler/src/Tracer.js b/packages/core/profiler/src/Tracer.js index 34c61312e4b..d4a189a9190 100644 --- a/packages/core/profiler/src/Tracer.js +++ b/packages/core/profiler/src/Tracer.js @@ -3,6 +3,7 @@ import type { TraceEvent, IDisposable, + MeasurementOptions, PluginTracer as IPluginTracer, } from '@parcel/types'; import type { @@ -64,44 +65,44 @@ export default class Tracer { return this.#traceEmitter.addListener(cb); } - async wrap(name: string, fn: () => mixed): Promise { - let measurement = this.createMeasurement(name); - try { - await fn(); - } finally { - measurement && measurement.end(); + measure( + {args = {}, categories, name}: MeasurementOptions, + fn: () => T, + ): T { + if (!this.enabled) { + return fn(); } - } - createMeasurement( - name: string, - category: string = 'Core', - argumentName?: string, - otherArgs?: {[key: string]: mixed}, - ): ITraceMeasurement | null { - if (!this.enabled) return null; + let measurement = new TraceMeasurement(this, name, pid, tid, { + categories, + args, + }); - // We create `args` in a fairly verbose way to avoid object - // allocation where not required. - let args: {[key: string]: mixed}; - if (typeof argumentName === 'string') { - args = {name: argumentName}; - } - if (typeof otherArgs === 'object') { - if (typeof args == 'undefined') { - args = {}; + let result: T; + let hasFinally = false; + + try { + result = fn(); + // @ts-expect-error TypeScript types cannot infer that finally can exist + if ( + result != null && + typeof result === 'object' && + typeof result.finally === 'function' + ) { + hasFinally = true; + // @ts-expect-error + // $FlowFixMe[incompatible-use] This will run for a promise type, but it cannot be easily typed in Flow + result = result.finally(() => { + measurement?.end(); + }); } - for (const [k, v] of Object.entries(otherArgs)) { - args[k] = v; + } finally { + if (!hasFinally) { + measurement?.end(); } } - const data: TraceMeasurementData = { - categories: [category], - args, - }; - - return new TraceMeasurement(this, name, pid, tid, data); + return result; } get enabled(): boolean { @@ -128,6 +129,7 @@ type TracerOpts = {| origin: string, category: string, |}; + export class PluginTracer implements IPluginTracer { /** @private */ origin: string; @@ -151,13 +153,51 @@ export class PluginTracer implements IPluginTracer { argumentName?: string, otherArgs?: {[key: string]: mixed}, ): ITraceMeasurement | null { - return tracer.createMeasurement( - name, - `${this.category}:${this.origin}${ - typeof category === 'string' ? `:${category}` : '' - }`, - argumentName, - otherArgs, + if (!this.enabled) return null; + + // We create `args` in a fairly verbose way to avoid object + // allocation where not required. + let args: {[key: string]: mixed}; + if (typeof argumentName === 'string') { + args = {name: argumentName}; + } + if (typeof otherArgs === 'object') { + if (typeof args == 'undefined') { + args = {}; + } + for (const [k, v] of Object.entries(otherArgs)) { + args[k] = v; + } + } + + const data: TraceMeasurementData = { + categories: [ + `${this.category}:${this.origin}${ + typeof category === 'string' ? `:${category}` : '' + }`, + ], + args, + }; + + return new TraceMeasurement(tracer, name, pid, tid, data); + } + + measure(options: MeasurementOptions, fn: () => T): T { + if (!this.enabled) { + return fn(); + } + + return tracer.measure( + { + ...options, + // $FlowFixMe[cannot-spread-inexact] + args: { + origin: this.origin, + ...(options.args ?? {}), + }, + categories: [this.category, ...options.categories], + }, + fn, ); } } diff --git a/packages/core/profiler/test/Tracer.test.js b/packages/core/profiler/test/Tracer.test.js index 3dd68fcfc91..f1a36e67e35 100644 --- a/packages/core/profiler/test/Tracer.test.js +++ b/packages/core/profiler/test/Tracer.test.js @@ -5,74 +5,123 @@ import assert from 'assert'; describe('Tracer', () => { let onTrace; let traceDisposable; + let opts = {name: 'test', categories: ['tracer']}; + beforeEach(() => { onTrace = sinon.spy(); traceDisposable = tracer.onTrace(onTrace); tracer.enable(); }); + afterEach(() => { traceDisposable.dispose(); }); - it('returns no measurement when disabled', () => { - tracer.disable(); - const measurement = tracer.createMeasurement('test'); - assert(measurement == null); - assert(onTrace.notCalled); - }); - it('emits a basic trace event', () => { - const measurement = tracer.createMeasurement('test'); - measurement.end(); - sinon.assert.calledWith( - onTrace, - sinon.match({ - type: 'trace', - name: 'test', - args: undefined, - duration: sinon.match.number, - }), - ); - }); - it('emits a complex trace event', () => { - const measurement = tracer.createMeasurement('test', 'myPlugin', 'aaargh', { - extra: 'data', - }); - measurement.end(); - sinon.assert.calledWith( - onTrace, - sinon.match({ - type: 'trace', - name: 'test', - categories: ['myPlugin'], - args: {extra: 'data', name: 'aaargh'}, - duration: sinon.match.number, - }), - ); - }); - it('calling end twice on measurment should be a no-op', () => { - const measurement = tracer.createMeasurement('test'); - measurement.end(); - measurement.end(); - sinon.assert.calledOnce(onTrace); + describe('measure()', () => { + let cases = [ + ['synchronous', () => () => {}], + ['asynchronous', () => async () => {}], + ]; + + for (let [type, createFn] of cases) { + describe(`given a ${type} function`, () => { + it('does not trace when disabled', async () => { + tracer.disable(); + + let result = tracer.measure(opts, sinon.spy()); + if (type === 'asynchronous') { + sinon.assert.notCalled(onTrace); + await result; + } + + assert(onTrace.notCalled); + }); + + it('emits a basic trace event', async () => { + let result = tracer.measure(opts, createFn()); + if (type === 'asynchronous') { + sinon.assert.notCalled(onTrace); + await result; + } + + sinon.assert.calledOnce(onTrace); + sinon.assert.calledWith( + onTrace, + sinon.match({ + type: 'trace', + name: 'test', + args: {}, + categories: ['tracer'], + duration: sinon.match.number, + }), + ); + }); + + it('emits a complex trace event', async () => { + let result = tracer.measure( + {...opts, args: {hello: 'world'}}, + createFn(), + ); + if (type === 'asynchronous') { + sinon.assert.notCalled(onTrace); + await result; + } + + sinon.assert.calledOnce(onTrace); + sinon.assert.calledWith( + onTrace, + sinon.match({ + type: 'trace', + name: 'test', + args: {hello: 'world'}, + categories: ['tracer'], + duration: sinon.match.number, + }), + ); + }); + }); + } }); describe('PluginTracer', () => { - it('emits events with proper origin/category', () => { - const pluginTracer = new PluginTracer({ - origin: 'origin', - category: 'cat', + const pluginTracer = new PluginTracer({ + origin: 'origin', + category: 'cat', + }); + + describe(`measure()`, () => { + it('emits events with origin and category', () => { + pluginTracer.measure(opts, sinon.spy()); + + sinon.assert.calledOnce(onTrace); + sinon.assert.calledWith( + onTrace, + sinon.match({ + type: 'trace', + name: 'test', + args: {origin: 'origin'}, + categories: ['cat', 'tracer'], + duration: sinon.match.number, + }), + ); + }); + }); + + describe('createMeasurement()', () => { + it('emits events with origin and category', () => { + pluginTracer.createMeasurement('test', 'customCat').end(); + + sinon.assert.calledOnce(onTrace); + sinon.assert.calledWith( + onTrace, + sinon.match({ + type: 'trace', + name: 'test', + categories: ['cat:origin:customCat'], + duration: sinon.match.number, + }), + ); }); - const measurement = pluginTracer.createMeasurement('test', 'customCat'); - measurement.end(); - sinon.assert.calledWith( - onTrace, - sinon.match({ - type: 'trace', - name: 'test', - categories: ['cat:origin:customCat'], - duration: sinon.match.number, - }), - ); }); }); }); diff --git a/packages/core/types-internal/src/index.js b/packages/core/types-internal/src/index.js index 39b4e9bf9f1..cae678175fe 100644 --- a/packages/core/types-internal/src/index.js +++ b/packages/core/types-internal/src/index.js @@ -2066,6 +2066,12 @@ export type AsyncSubscription = {| unsubscribe(): Promise, |}; +export type MeasurementOptions = {| + args?: {[key: string]: mixed}, + categories: string[], + name: string, +|}; + export interface PluginTracer { /** Returns whether the tracer is enabled. Use this to avoid possibly expensive calculations * of arguments to `createMeasurement` - for example if you need to determine the entry of a bundle to pass it @@ -2093,4 +2099,6 @@ export interface PluginTracer { argumentName?: string, otherArgs?: {[key: string]: mixed}, ): TraceMeasurement | null; + + measure(options: MeasurementOptions, fn: () => T): T; } diff --git a/packages/optimizers/inline-requires/src/InlineRequires.js b/packages/optimizers/inline-requires/src/InlineRequires.js index f0871ffc86b..0a185749148 100644 --- a/packages/optimizers/inline-requires/src/InlineRequires.js +++ b/packages/optimizers/inline-requires/src/InlineRequires.js @@ -25,24 +25,25 @@ module.exports = new Optimizer({ return {publicIdToAssetSideEffects}; } - const measurement = tracer.createMeasurement( - '@parcel/optimizer-inline-requires', - 'generatePublicIdToAssetSideEffects', - bundle.name, - ); - - bundleGraph.traverse(node => { - if (node.type === 'asset') { - const publicId = bundleGraph.getAssetPublicId(node.value); - let sideEffectsMap = nullthrows(publicIdToAssetSideEffects); - sideEffectsMap.set(publicId, { - sideEffects: node.value.sideEffects, - filePath: node.value.filePath, + tracer.measure( + { + name: '@parcel/optimizer-inline-requires', + args: {bundleName: bundle.name}, + categories: ['generatePublicIdToAssetSideEffects'], + }, + () => { + bundleGraph.traverse(node => { + if (node.type === 'asset') { + const publicId = bundleGraph.getAssetPublicId(node.value); + let sideEffectsMap = nullthrows(publicIdToAssetSideEffects); + sideEffectsMap.set(publicId, { + sideEffects: node.value.sideEffects, + filePath: node.value.filePath, + }); + } }); - } - }); - - measurement && measurement.end(); + }, + ); return {publicIdToAssetSideEffects}; }, @@ -61,13 +62,14 @@ module.exports = new Optimizer({ } try { - const measurement = tracer.createMeasurement( - '@parcel/optimizer-inline-requires', - 'parse', - bundle.name, + const ast = await tracer.measure( + { + name: '@parcel/optimizer-inline-requires', + args: {bundle: bundle.name}, + categories: ['parse'], + }, + () => parse(contents.toString()), ); - const ast = await parse(contents.toString()); - measurement && measurement.end(); const visitor = new RequireInliningVisitor({ bundle, diff --git a/packages/reporters/tracer/src/TracerReporter.js b/packages/reporters/tracer/src/TracerReporter.js index 5a5b0023497..fd7e3d25b66 100644 --- a/packages/reporters/tracer/src/TracerReporter.js +++ b/packages/reporters/tracer/src/TracerReporter.js @@ -51,7 +51,7 @@ export default (new Reporter({ case 'trace': // Due to potential race conditions at the end of the build, we ignore any trace events that occur // after we've closed the write stream. - if (tracer === null) return; + if (tracer == null) return; tracer.completeEvent({ name: event.name, diff --git a/packages/transformers/babel/src/babel7.js b/packages/transformers/babel/src/babel7.js index ef613ffd036..016a9255a1b 100644 --- a/packages/transformers/babel/src/babel7.js +++ b/packages/transformers/babel/src/babel7.js @@ -87,13 +87,17 @@ export default async function babel7( if (pluginKey.startsWith(options.projectRoot)) { pluginKey = path.relative(options.projectRoot, pluginKey); } - const measurement = tracer.createMeasurement( - pluginKey, - nodeType, - path.relative(options.projectRoot, asset.filePath), + + tracer.measure( + { + name: pluginKey, + args: { + filename: path.relative(options.projectRoot, asset.filePath), + }, + categories: [nodeType], + }, + () => fn.apply(this, arguments), ); - fn.apply(this, arguments); - measurement && measurement.end(); }; }; }