diff --git a/README.md b/README.md index a506c6af0e1..9056329aa3d 100644 --- a/README.md +++ b/README.md @@ -165,6 +165,7 @@ These GitHub repositories provide supplementary resources for Rush Stack: | [/build-tests/eslint-bulk-suppressions-test-legacy](./build-tests/eslint-bulk-suppressions-test-legacy/) | Sample code to test eslint bulk suppressions for versions of eslint < 8.57.0 | | [/build-tests/hashed-folder-copy-plugin-webpack5-test](./build-tests/hashed-folder-copy-plugin-webpack5-test/) | Building this project exercises @rushstack/hashed-folder-copy-plugin with Webpack 5. NOTE - THIS TEST IS CURRENTLY EXPECTED TO BE BROKEN | | [/build-tests/heft-copy-files-test](./build-tests/heft-copy-files-test/) | Building this project tests copying files with Heft | +| [/build-tests/heft-example-lifecycle-plugin](./build-tests/heft-example-lifecycle-plugin/) | This is an example heft plugin for testing the lifecycle hooks | | [/build-tests/heft-example-plugin-01](./build-tests/heft-example-plugin-01/) | This is an example heft plugin that exposes hooks for other plugins | | [/build-tests/heft-example-plugin-02](./build-tests/heft-example-plugin-02/) | This is an example heft plugin that taps the hooks exposed from heft-example-plugin-01 | | [/build-tests/heft-fastify-test](./build-tests/heft-fastify-test/) | This project tests Heft support for the Fastify framework for Node.js services | diff --git a/apps/heft/src/cli/HeftActionRunner.ts b/apps/heft/src/cli/HeftActionRunner.ts index e60b78dbac7..da0c95caf70 100644 --- a/apps/heft/src/cli/HeftActionRunner.ts +++ b/apps/heft/src/cli/HeftActionRunner.ts @@ -403,14 +403,19 @@ export class HeftActionRunner { // Create operations for each task for (const task of phase.tasks) { - const taskOperation: Operation = _getOrCreateTaskOperation(internalHeftSession, task, operations); + const taskOperation: Operation = _getOrCreateTaskOperation( + internalHeftSession, + this._metricsCollector, + task, + operations + ); // Set the phase operation as a dependency of the task operation to ensure the phase operation runs first taskOperation.addDependency(phaseOperation); // Set all dependency tasks as dependencies of the task operation for (const dependencyTask of task.dependencyTasks) { taskOperation.addDependency( - _getOrCreateTaskOperation(internalHeftSession, dependencyTask, operations) + _getOrCreateTaskOperation(internalHeftSession, this._metricsCollector, dependencyTask, operations) ); } @@ -459,6 +464,7 @@ function _getOrCreatePhaseOperation( function _getOrCreateTaskOperation( this: void, internalHeftSession: InternalHeftSession, + metricsCollector: MetricsCollector, task: HeftTask, operations: Map ): Operation { @@ -469,6 +475,7 @@ function _getOrCreateTaskOperation( operation = new Operation({ groupName: task.parentPhase.phaseName, runner: new TaskOperationRunner({ + metricsCollector, internalHeftSession, task }) diff --git a/apps/heft/src/index.ts b/apps/heft/src/index.ts index 68d77c23d1d..4ca2a6217a4 100644 --- a/apps/heft/src/index.ts +++ b/apps/heft/src/index.ts @@ -63,7 +63,9 @@ export { type IHeftRecordMetricsHookOptions, type IMetricsData, type IPerformanceData as _IPerformanceData, - MetricsCollector as _MetricsCollector + MetricsCollector as _MetricsCollector, + type IHeftTaskRecordMetricsHookOptions, + type ITaskMetricsData } from './metrics/MetricsCollector'; export type { IScopedLogger } from './pluginFramework/logging/ScopedLogger'; diff --git a/apps/heft/src/metrics/MetricsCollector.ts b/apps/heft/src/metrics/MetricsCollector.ts index 82b70abf685..3896311cf57 100644 --- a/apps/heft/src/metrics/MetricsCollector.ts +++ b/apps/heft/src/metrics/MetricsCollector.ts @@ -6,6 +6,31 @@ import { AsyncParallelHook } from 'tapable'; import { performance } from 'perf_hooks'; import { InternalError } from '@rushstack/node-core-library'; +/** + * @public + */ +export interface ITaskMetricsData { + /** + * The name of the task that was executed. + */ + taskName: string; + + /** + * The name of the phase that the task belongs to. + */ + phaseName: string; + + /** + * The total execution duration of the task, in milliseconds. + */ + taskTotalExecutionMs: number; + + /** + * Whether or not the task encountered errors. + */ + encounteredError?: boolean; +} + /** * @public */ @@ -73,7 +98,7 @@ export interface IMetricsData { /** * @public */ -export interface IHeftRecordMetricsHookOptions { +export interface IRecordMetricsHookOptions { /** * @public */ @@ -82,11 +107,21 @@ export interface IHeftRecordMetricsHookOptions { /** * @public */ - metricData: IMetricsData; + metricData: T; } /** - * @internal + * @public + */ +export type IHeftRecordMetricsHookOptions = IRecordMetricsHookOptions; + +/** + * @public + */ +export type IHeftTaskRecordMetricsHookOptions = IRecordMetricsHookOptions; + +/** + * @public */ export interface IPerformanceData { taskTotalExecutionMs: number; @@ -101,6 +136,9 @@ export class MetricsCollector { public readonly recordMetricsHook: AsyncParallelHook = new AsyncParallelHook(['recordMetricsHookOptions']); + public readonly recordTaskMetricsHook: AsyncParallelHook = + new AsyncParallelHook(['recordTaskMetricsHookOptions']); + private _bootDurationMs: number | undefined; private _startTimeMs: number | undefined; @@ -166,4 +204,49 @@ export class MetricsCollector { metricData }); } + + /** + * Record metrics to the installed plugin(s). + * + * @param command - Describe the user command, e.g. `start` or `build` + * @param parameterMap - Optional map of parameters to their values + * @param performanceData - Optional performance data + */ + public async recordTaskAsync( + taskName: string, + phaseName: string, + performanceData?: Partial + ): Promise { + const { _bootDurationMs, _startTimeMs } = this; + if (_bootDurationMs === undefined || _startTimeMs === undefined) { + throw new InternalError('MetricsCollector has not been initialized with setStartTime() yet'); + } + + if (!taskName) { + throw new InternalError('The task name must be specified.'); + } + + if (!phaseName) { + throw new InternalError('The phase name must be specified.'); + } + + const filledPerformanceData: IPerformanceData = { + taskTotalExecutionMs: performance.now() - _startTimeMs, + ...(performanceData || {}) + }; + + const { taskTotalExecutionMs } = filledPerformanceData; + + const metricData: ITaskMetricsData = { + taskName: taskName, + phaseName: phaseName, + encounteredError: filledPerformanceData.encounteredError, + taskTotalExecutionMs: taskTotalExecutionMs + }; + + await this.recordTaskMetricsHook.promise({ + metricName: 'task_execution_heft', + metricData + }); + } } diff --git a/apps/heft/src/operations/runners/TaskOperationRunner.ts b/apps/heft/src/operations/runners/TaskOperationRunner.ts index 995d10ffbed..75c8d478359 100644 --- a/apps/heft/src/operations/runners/TaskOperationRunner.ts +++ b/apps/heft/src/operations/runners/TaskOperationRunner.ts @@ -34,29 +34,12 @@ import { type IWatchFileSystem, WatchFileSystemAdapter } from '../../utilities/WatchFileSystemAdapter'; +import type { MetricsCollector } from '../../metrics/MetricsCollector'; export interface ITaskOperationRunnerOptions { internalHeftSession: InternalHeftSession; task: HeftTask; -} - -/** - * Log out a start message, run a provided function, and log out an end message - */ -export async function runAndMeasureAsync( - fn: () => Promise, - startMessageFn: () => string, - endMessageFn: () => string, - logFn: (message: string) => void -): Promise { - logFn(startMessageFn()); - const startTime: number = performance.now(); - try { - return await fn(); - } finally { - const endTime: number = performance.now(); - logFn(`${endMessageFn()} (${endTime - startTime}ms)`); - } + metricsCollector: MetricsCollector; } export class TaskOperationRunner implements IOperationRunner { @@ -89,8 +72,9 @@ export class TaskOperationRunner implements IOperationRunner { context: IOperationRunnerContext, taskSession: HeftTaskSession ): Promise { + const { metricsCollector } = this._options; const { abortSignal, requestRun } = context; - const { hooks, logger } = taskSession; + const { hooks, logger, taskName, phaseName } = taskSession; // Need to clear any errors or warnings from the previous invocation, particularly // if this is an immediate rerun @@ -155,61 +139,70 @@ export class TaskOperationRunner implements IOperationRunner { return OperationStatus.NoOp; } - const runResult: OperationStatus = shouldRun - ? await runAndMeasureAsync( - async (): Promise => { - // Create the options and provide a utility method to obtain paths to copy - const runHookOptions: IHeftTaskRunHookOptions = { - abortSignal, - globAsync: glob + let runResult: OperationStatus = OperationStatus.Success; + if (shouldRun) { + const runTaskAsync = async (): Promise => { + // Create the options and provide a utility method to obtain paths to copy + const runHookOptions: IHeftTaskRunHookOptions = { + abortSignal, + globAsync: glob + }; + + // Run the plugin run hook + try { + if (shouldRunIncremental) { + const runIncrementalHookOptions: IHeftTaskRunIncrementalHookOptions = { + ...runHookOptions, + watchGlobAsync: ( + pattern: string | string[], + options: IGlobOptions = {} + ): Promise> => { + return watchGlobAsync(pattern, { + ...options, + fs: getWatchFileSystemAdapter() + }); + }, + get watchFs(): IWatchFileSystem { + return getWatchFileSystemAdapter(); + }, + requestRun: requestRun! }; + await hooks.runIncremental.promise(runIncrementalHookOptions); + } else { + await hooks.run.promise(runHookOptions); + } + } catch (e) { + // Log out using the task logger, and return an error status + if (!(e instanceof AlreadyReportedError)) { + logger.emitError(e as Error); + } + return OperationStatus.Failure; + } - // Run the plugin run hook - try { - if (shouldRunIncremental) { - const runIncrementalHookOptions: IHeftTaskRunIncrementalHookOptions = { - ...runHookOptions, - watchGlobAsync: ( - pattern: string | string[], - options: IGlobOptions = {} - ): Promise> => { - return watchGlobAsync(pattern, { - ...options, - fs: getWatchFileSystemAdapter() - }); - }, - get watchFs(): IWatchFileSystem { - return getWatchFileSystemAdapter(); - }, - requestRun: requestRun! - }; - await hooks.runIncremental.promise(runIncrementalHookOptions); - } else { - await hooks.run.promise(runHookOptions); - } - } catch (e) { - // Log out using the task logger, and return an error status - if (!(e instanceof AlreadyReportedError)) { - logger.emitError(e as Error); - } - return OperationStatus.Failure; - } - - if (abortSignal.aborted) { - return OperationStatus.Aborted; - } - - return OperationStatus.Success; - }, - () => `Starting ${shouldRunIncremental ? 'incremental ' : ''}task execution`, - () => { - const finishedWord: string = abortSignal.aborted ? 'Aborted' : 'Finished'; - return `${finishedWord} ${shouldRunIncremental ? 'incremental ' : ''}task execution`; - }, - terminal.writeVerboseLine.bind(terminal) - ) - : // This branch only occurs if only file operations are defined. - OperationStatus.Success; + if (abortSignal.aborted) { + return OperationStatus.Aborted; + } + + return OperationStatus.Success; + }; + + const startTime: number = performance.now(); + terminal.writeVerboseLine(`Starting ${shouldRunIncremental ? 'incremental ' : ''}task execution`); + try { + runResult = await runTaskAsync(); + } finally { + const endTime: number = performance.now(); + const finishedWord: string = abortSignal.aborted ? 'Aborted' : 'Finished'; + terminal.writeVerboseLine( + `${finishedWord} ${shouldRunIncremental ? 'incremental ' : ''}task execution (${ + endTime - startTime + }ms)` + ); + await metricsCollector.recordTaskAsync(taskName, phaseName, { + taskTotalExecutionMs: endTime - startTime + }); + } + } if (this._fileOperations) { const { copyOperations, deleteOperations } = this._fileOperations; diff --git a/apps/heft/src/pluginFramework/HeftLifecycle.ts b/apps/heft/src/pluginFramework/HeftLifecycle.ts index 95fc7ee3fb5..702316336ad 100644 --- a/apps/heft/src/pluginFramework/HeftLifecycle.ts +++ b/apps/heft/src/pluginFramework/HeftLifecycle.ts @@ -67,7 +67,8 @@ export class HeftLifecycle extends HeftPluginHost { clean: new AsyncParallelHook(), toolStart: new AsyncParallelHook(), toolFinish: new AsyncParallelHook(), - recordMetrics: internalHeftSession.metricsCollector.recordMetricsHook + recordMetrics: internalHeftSession.metricsCollector.recordMetricsHook, + recordTaskMetrics: internalHeftSession.metricsCollector.recordTaskMetricsHook }; } diff --git a/apps/heft/src/pluginFramework/HeftLifecycleSession.ts b/apps/heft/src/pluginFramework/HeftLifecycleSession.ts index 1105608446e..f30cc07f1d7 100644 --- a/apps/heft/src/pluginFramework/HeftLifecycleSession.ts +++ b/apps/heft/src/pluginFramework/HeftLifecycleSession.ts @@ -4,7 +4,11 @@ import * as path from 'path'; import type { AsyncParallelHook } from 'tapable'; -import type { IHeftRecordMetricsHookOptions, MetricsCollector } from '../metrics/MetricsCollector'; +import type { + IHeftRecordMetricsHookOptions, + IHeftTaskRecordMetricsHookOptions, + MetricsCollector +} from '../metrics/MetricsCollector'; import type { ScopedLogger, IScopedLogger } from './logging/ScopedLogger'; import type { IInternalHeftSessionOptions } from './InternalHeftSession'; import type { IHeftParameters } from './HeftParameterManager'; @@ -111,6 +115,16 @@ export interface IHeftLifecycleHooks { * @public */ recordMetrics: AsyncParallelHook; + + /** + * The `recordTaskMetrics` hook is called at the end of every Heft task execution. It is called after all + * tasks have completed execution (or been canceled). In a watch run, it will be called several times + * in between `toolStart` and (if the session is gracefully interrupted via Ctrl+C), `toolFinish`. + * In a non-watch run, it will be invoked exactly once between `toolStart` and `toolFinish`. + * To use it, call `recordMetrics.tapPromise(, )`. + * @public + */ + recordTaskMetrics: AsyncParallelHook; } /** diff --git a/apps/heft/src/pluginFramework/HeftTaskSession.ts b/apps/heft/src/pluginFramework/HeftTaskSession.ts index 152e6967b9b..218f23b0cf9 100644 --- a/apps/heft/src/pluginFramework/HeftTaskSession.ts +++ b/apps/heft/src/pluginFramework/HeftTaskSession.ts @@ -232,6 +232,7 @@ export interface IHeftTaskSessionOptions extends IHeftPhaseSessionOptions { } export class HeftTaskSession implements IHeftTaskSession { + public readonly phaseName: string; public readonly taskName: string; public readonly hooks: IHeftTaskHooks; public readonly tempFolderPath: string; @@ -280,6 +281,7 @@ export class HeftTaskSession implements IHeftTaskSession { this.logger = loggingManager.requestScopedLogger(`${phase.phaseName}:${task.taskName}`); this.metricsCollector = metricsCollector; this.taskName = task.taskName; + this.phaseName = phase.phaseName; this.hooks = { run: new AsyncParallelHook(['runHookOptions']), runIncremental: new AsyncParallelHook(['runIncrementalHookOptions']), diff --git a/build-tests/heft-example-lifecycle-plugin/.eslintrc.js b/build-tests/heft-example-lifecycle-plugin/.eslintrc.js new file mode 100644 index 00000000000..066bf07ecc8 --- /dev/null +++ b/build-tests/heft-example-lifecycle-plugin/.eslintrc.js @@ -0,0 +1,9 @@ +// This is a workaround for https://github.com/eslint/eslint/issues/3458 +require('local-eslint-config/patch/modern-module-resolution'); +// This is a workaround for https://github.com/microsoft/rushstack/issues/3021 +require('local-eslint-config/patch/custom-config-package-names'); + +module.exports = { + extends: ['local-eslint-config/profile/node-trusted-tool', 'local-eslint-config/mixins/friendly-locals'], + parserOptions: { tsconfigRootDir: __dirname } +}; diff --git a/build-tests/heft-example-lifecycle-plugin/config/heft.json b/build-tests/heft-example-lifecycle-plugin/config/heft.json new file mode 100644 index 00000000000..64d969be2eb --- /dev/null +++ b/build-tests/heft-example-lifecycle-plugin/config/heft.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/heft/v0/heft.schema.json", + + // TODO: Add comments + "phasesByName": { + "build": { + "cleanFiles": [{ "includeGlobs": ["dist", "lib"] }], + + "tasksByName": { + "typescript": { + "taskPlugin": { + "pluginPackage": "@rushstack/heft-typescript-plugin" + } + }, + "lint": { + "taskDependencies": ["typescript"], + "taskPlugin": { + "pluginPackage": "@rushstack/heft-lint-plugin" + } + } + } + } + } +} diff --git a/build-tests/heft-example-lifecycle-plugin/config/rush-project.json b/build-tests/heft-example-lifecycle-plugin/config/rush-project.json new file mode 100644 index 00000000000..514e557d5eb --- /dev/null +++ b/build-tests/heft-example-lifecycle-plugin/config/rush-project.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/rush-project.schema.json", + + "operationSettings": [ + { + "operationName": "_phase:build", + "outputFolderNames": ["lib", "dist"] + } + ] +} diff --git a/build-tests/heft-example-lifecycle-plugin/heft-plugin.json b/build-tests/heft-example-lifecycle-plugin/heft-plugin.json new file mode 100644 index 00000000000..d174b5b769b --- /dev/null +++ b/build-tests/heft-example-lifecycle-plugin/heft-plugin.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/heft/v0/heft-plugin.schema.json", + + "lifecyclePlugins": [ + { + "pluginName": "example-lifecycle-plugin", + "entryPoint": "./lib/index" + } + ] +} diff --git a/build-tests/heft-example-lifecycle-plugin/package.json b/build-tests/heft-example-lifecycle-plugin/package.json new file mode 100644 index 00000000000..f949cc11143 --- /dev/null +++ b/build-tests/heft-example-lifecycle-plugin/package.json @@ -0,0 +1,26 @@ +{ + "name": "heft-example-lifecycle-plugin", + "description": "This is an example heft plugin for testing the lifecycle hooks", + "version": "1.0.0", + "private": true, + "main": "./lib/index.js", + "typings": "./lib/index.d.ts", + "scripts": { + "build": "heft build --clean", + "start": "heft build-watch", + "_phase:build": "heft run --only build -- --clean" + }, + "dependencies": { + "tapable": "1.1.3" + }, + "devDependencies": { + "local-eslint-config": "workspace:*", + "@rushstack/heft": "workspace:*", + "@rushstack/heft-lint-plugin": "workspace:*", + "@rushstack/heft-typescript-plugin": "workspace:*", + "@types/node": "20.17.19", + "@types/tapable": "1.0.6", + "eslint": "~8.57.0", + "typescript": "~5.8.2" + } +} diff --git a/build-tests/heft-example-lifecycle-plugin/src/index.ts b/build-tests/heft-example-lifecycle-plugin/src/index.ts new file mode 100644 index 00000000000..f2fec5f4d0e --- /dev/null +++ b/build-tests/heft-example-lifecycle-plugin/src/index.ts @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type { + IHeftLifecyclePlugin, + IHeftLifecycleSession, + IHeftTaskRecordMetricsHookOptions +} from '@rushstack/heft'; + +export const PLUGIN_NAME: 'example-lifecycle-plugin' = 'example-lifecycle-plugin'; + +export default class ExampleLifecyclePlugin implements IHeftLifecyclePlugin { + public apply(session: IHeftLifecycleSession): void { + const { logger } = session; + session.hooks.recordTaskMetrics.tapPromise( + PLUGIN_NAME, + async (metrics: IHeftTaskRecordMetricsHookOptions) => { + const { taskName, taskTotalExecutionMs, phaseName } = metrics.metricData; + logger.terminal.writeLine( + `Finished ${phaseName}:${taskName} in ${taskTotalExecutionMs.toFixed(2)}ms` + ); + } + ); + } +} diff --git a/build-tests/heft-example-lifecycle-plugin/tsconfig.json b/build-tests/heft-example-lifecycle-plugin/tsconfig.json new file mode 100644 index 00000000000..2d179c7173f --- /dev/null +++ b/build-tests/heft-example-lifecycle-plugin/tsconfig.json @@ -0,0 +1,25 @@ +{ + "$schema": "http://json.schemastore.org/tsconfig", + + "compilerOptions": { + "outDir": "lib", + "rootDir": "src", + + "forceConsistentCasingInFileNames": true, + "jsx": "react", + "declaration": true, + "sourceMap": true, + "declarationMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "strictNullChecks": true, + "noUnusedLocals": true, + "types": ["node"], + + "module": "commonjs", + "target": "es2017", + "lib": ["es2017"] + }, + "include": ["src/**/*.ts", "src/**/*.tsx"], + "exclude": ["node_modules", "lib"] +} diff --git a/build-tests/heft-node-everything-test/config/heft.json b/build-tests/heft-node-everything-test/config/heft.json index fc24874e5d3..08d9c72b92e 100644 --- a/build-tests/heft-node-everything-test/config/heft.json +++ b/build-tests/heft-node-everything-test/config/heft.json @@ -4,6 +4,12 @@ { "$schema": "https://developer.microsoft.com/json-schemas/heft/v0/heft.schema.json", + "heftPlugins": [ + { + "pluginPackage": "heft-example-lifecycle-plugin" + } + ], + // TODO: Add comments "phasesByName": { "build": { diff --git a/build-tests/heft-node-everything-test/package.json b/build-tests/heft-node-everything-test/package.json index c85e317f85b..90e7090f6f0 100644 --- a/build-tests/heft-node-everything-test/package.json +++ b/build-tests/heft-node-everything-test/package.json @@ -23,6 +23,7 @@ "@types/heft-jest": "1.0.1", "@types/node": "20.17.19", "eslint": "~8.57.0", + "heft-example-lifecycle-plugin": "workspace:*", "heft-example-plugin-01": "workspace:*", "heft-example-plugin-02": "workspace:*", "tslint": "~5.20.1", diff --git a/common/changes/@rushstack/heft/sennyeya-heft-metrics_2025-05-13-15-54.json b/common/changes/@rushstack/heft/sennyeya-heft-metrics_2025-05-13-15-54.json new file mode 100644 index 00000000000..e1c8d461e35 --- /dev/null +++ b/common/changes/@rushstack/heft/sennyeya-heft-metrics_2025-05-13-15-54.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/heft", + "comment": "Add new hook for collecting granular task-level metrics.", + "type": "minor" + } + ], + "packageName": "@rushstack/heft" +} \ No newline at end of file diff --git a/common/config/rush/browser-approved-packages.json b/common/config/rush/browser-approved-packages.json index 01b44fa2e61..e34c53ccdde 100644 --- a/common/config/rush/browser-approved-packages.json +++ b/common/config/rush/browser-approved-packages.json @@ -58,6 +58,10 @@ "name": "dependency-path", "allowedCategories": [ "libraries" ] }, + { + "name": "heft-example-lifecycle-plugin", + "allowedCategories": [ "tests" ] + }, { "name": "local-web-rig", "allowedCategories": [ "libraries", "vscode-extensions" ] diff --git a/common/config/subspaces/default/pnpm-lock.yaml b/common/config/subspaces/default/pnpm-lock.yaml index ecc2d923870..7d251b20af8 100644 --- a/common/config/subspaces/default/pnpm-lock.yaml +++ b/common/config/subspaces/default/pnpm-lock.yaml @@ -1294,6 +1294,37 @@ importers: specifier: workspace:* version: link:../../apps/heft + ../../../build-tests/heft-example-lifecycle-plugin: + dependencies: + tapable: + specifier: 1.1.3 + version: 1.1.3 + devDependencies: + '@rushstack/heft': + specifier: workspace:* + version: link:../../apps/heft + '@rushstack/heft-lint-plugin': + specifier: workspace:* + version: link:../../heft-plugins/heft-lint-plugin + '@rushstack/heft-typescript-plugin': + specifier: workspace:* + version: link:../../heft-plugins/heft-typescript-plugin + '@types/node': + specifier: 20.17.19 + version: 20.17.19 + '@types/tapable': + specifier: 1.0.6 + version: 1.0.6 + eslint: + specifier: ~8.57.0 + version: 8.57.0 + local-eslint-config: + specifier: workspace:* + version: link:../../eslint/local-eslint-config + typescript: + specifier: ~5.8.2 + version: 5.8.2 + ../../../build-tests/heft-example-plugin-01: dependencies: tapable: @@ -1562,6 +1593,9 @@ importers: eslint: specifier: ~8.57.0 version: 8.57.0 + heft-example-lifecycle-plugin: + specifier: workspace:* + version: link:../heft-example-lifecycle-plugin heft-example-plugin-01: specifier: workspace:* version: link:../heft-example-plugin-01 diff --git a/common/reviews/api/heft.api.md b/common/reviews/api/heft.api.md index a50dc296624..20862cc35c6 100644 --- a/common/reviews/api/heft.api.md +++ b/common/reviews/api/heft.api.md @@ -151,6 +151,7 @@ export interface IHeftLifecycleCleanHookOptions { export interface IHeftLifecycleHooks { clean: AsyncParallelHook; recordMetrics: AsyncParallelHook; + recordTaskMetrics: AsyncParallelHook; toolFinish: AsyncParallelHook; toolStart: AsyncParallelHook; } @@ -199,13 +200,10 @@ export interface IHeftPlugin; // @public export interface IHeftTaskFileOperations { @@ -224,6 +222,9 @@ export interface IHeftTaskHooks { export interface IHeftTaskPlugin extends IHeftPlugin { } +// @public (undocumented) +export type IHeftTaskRecordMetricsHookOptions = IRecordMetricsHookOptions; + // @public export interface IHeftTaskRunHookOptions { // @beta @@ -269,7 +270,7 @@ export interface IMetricsData { totalUptimeMs: number; } -// @internal (undocumented) +// @public (undocumented) export interface _IPerformanceData { // (undocumented) encounteredError?: boolean; @@ -315,6 +316,14 @@ export interface IScopedLogger { readonly terminal: ITerminal; } +// @public (undocumented) +export interface ITaskMetricsData { + encounteredError?: boolean; + phaseName: string; + taskName: string; + taskTotalExecutionMs: number; +} + // @public export interface IWatchedFileState { changed: boolean; @@ -341,6 +350,9 @@ export class _MetricsCollector { recordAsync(command: string, performanceData?: Partial<_IPerformanceData>, parameters?: Record): Promise; // (undocumented) readonly recordMetricsHook: AsyncParallelHook; + recordTaskAsync(taskName: string, phaseName: string, performanceData?: Partial<_IPerformanceData>): Promise; + // (undocumented) + readonly recordTaskMetricsHook: AsyncParallelHook; setStartTime(): void; } diff --git a/rush.json b/rush.json index 3d8f5018a1e..c8808ba0517 100644 --- a/rush.json +++ b/rush.json @@ -791,6 +791,12 @@ "reviewCategory": "tests", "shouldPublish": false }, + { + "packageName": "heft-example-lifecycle-plugin", + "projectFolder": "build-tests/heft-example-lifecycle-plugin", + "reviewCategory": "tests", + "shouldPublish": false + }, { "packageName": "heft-example-plugin-01", "projectFolder": "build-tests/heft-example-plugin-01",