diff --git a/.changeset/popular-seahorses-provide.md b/.changeset/popular-seahorses-provide.md new file mode 100644 index 000000000..7d8e503ed --- /dev/null +++ b/.changeset/popular-seahorses-provide.md @@ -0,0 +1,6 @@ +--- +'reassure': minor +'@callstack/reassure-measure': minor +--- + +feat: add measureAsyncFunction diff --git a/README.md b/README.md index 3bd3381f9..9640a4dbc 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,8 @@ - [`MeasureRendersOptions` type](#measurerendersoptions-type) - [`measureFunction` function](#measurefunction-function) - [`MeasureFunctionOptions` type](#measurefunctionoptions-type) + - [`measureAsyncFunction` function](#measureasyncfunction-function) + - [`MeasureAsyncFunctionOptions` type](#measureasyncfunctionoptions-type) - [Configuration](#configuration) - [Default configuration](#default-configuration) - [`configure` function](#configure-function) @@ -397,11 +399,40 @@ async function measureFunction( interface MeasureFunctionOptions { runs?: number; warmupRuns?: number; + writeFile?: boolean; } ``` - **`runs`**: number of runs per series for the particular test - **`warmupRuns`**: number of additional warmup runs that will be done and discarded before the actual runs. +- **`writeFile`**: (default `true`) should write output to file. + +#### `measureAsyncFunction` function + +Allows you to wrap any **asynchronous** function, measure its execution times and write results to the output file. You can use optional `options` to customize aspects of the testing. Note: the execution count will always be one. + +> **Note**: Measuring performance of asynchronous functions can be tricky. These functions often depend on external conditions like I/O operations, network requests, or storage access, which introduce unpredictable timing variations in your measurements. For stable and meaningful performance metrics, **always ensure all external calls are properly mocked in your test environment to avoid polluting your performance measurements with uncontrollable factors.** + +```ts +async function measureAsyncFunction( + fn: () => Promise, + options?: MeasureAsyncFunctionOptions +): Promise { +``` + +#### `MeasureAsyncFunctionOptions` type + +```ts +interface MeasureAsyncFunctionOptions { + runs?: number; + warmupRuns?: number; + writeFile?: boolean; +} +``` + +- **`runs`**: number of runs per series for the particular test +- **`warmupRuns`**: number of additional warmup runs that will be done and discarded before the actual runs. +- **`writeFile`**: (default `true`) should write output to file. ### Configuration diff --git a/docusaurus/docs/api.md b/docusaurus/docs/api.md index 814de7cdf..939971e71 100644 --- a/docusaurus/docs/api.md +++ b/docusaurus/docs/api.md @@ -99,6 +99,51 @@ interface MeasureFunctionOptions { - **`warmupRuns`**: number of additional warmup runs that will be done and discarded before the actual runs. - **`writeFile`**: (default `true`) should write output to file. +### `measureAsyncFunction` function {#measure-async-function} + +Allows you to wrap any asynchronous function, measure its performance and write results to the output file. You can use optional `options` to customize aspects of the testing. + +:::info + +Measuring performance of asynchronous functions can be tricky. These functions often depend on external conditions like I/O operations, network requests, or storage access, which introduce unpredictable timing variations in your measurements. For stable and meaningful performance metrics, **always ensure all external calls are properly mocked in your test environment to avoid polluting your performance measurements with uncontrollable factors.** + +::: + +```ts +async function measureAsyncFunction( + fn: () => Promise, + options?: MeasureAsyncFunctionOptions, +): Promise { +``` + +#### Example {#measure-async-function-example} + +```ts +// sample.perf-test.tsx +import { measureAsyncFunction } from 'reassure'; +import { fib } from './fib'; + +test('fib 30', async () => { + await measureAsyncFunction(async () => { + return Promise.resolve().then(() => fib(30)); + }); +}); +``` + +### `MeasureAsyncFunctionOptions` type {#measure-async-function-options} + +```ts +interface MeasureAsyncFunctionOptions { + runs?: number; + warmupRuns?: number; + writeFile?: boolean; +} +``` + +- **`runs`**: number of runs per series for the particular test +- **`warmupRuns`**: number of additional warmup runs that will be done and discarded before the actual runs. +- **`writeFile`**: (default `true`) should write output to file. + ## Configuration ### Default configuration diff --git a/packages/compare/src/type-schemas.ts b/packages/compare/src/type-schemas.ts index be5a34a45..56930112c 100644 --- a/packages/compare/src/type-schemas.ts +++ b/packages/compare/src/type-schemas.ts @@ -22,8 +22,8 @@ export const MeasureEntryScheme = z.object({ /** Name of the test scenario. */ name: z.string(), - /** Type of the measured characteristic (render, function execution). */ - type: z.enum(['render', 'function']).default('render'), + /** Type of the measured characteristic (render, function execution, async function execution). */ + type: z.enum(['render', 'function', 'async function']).default('render'), /** Number of times the measurement test was run. */ runs: z.number(), diff --git a/packages/measure/src/__tests__/measure-function.test.tsx b/packages/measure/src/__tests__/measure-function.test.tsx index 3fc3dcf3d..6aa632c44 100644 --- a/packages/measure/src/__tests__/measure-function.test.tsx +++ b/packages/measure/src/__tests__/measure-function.test.tsx @@ -1,5 +1,6 @@ import stripAnsi from 'strip-ansi'; import { measureFunction } from '../measure-function'; +import { measureAsyncFunction } from '../measure-async-function'; import { setHasShownFlagsOutput } from '../output'; // Exponentially slow function @@ -20,6 +21,18 @@ test('measureFunction captures results', async () => { expect(results.counts).toEqual([1]); }); +test('measureAsyncFunction captures results', async () => { + const fn = jest.fn(async () => { + await Promise.resolve(); + return fib(5); + }); + const results = await measureAsyncFunction(fn, { runs: 1, warmupRuns: 0, writeFile: false }); + + expect(fn).toHaveBeenCalledTimes(1); + expect(results.runs).toBe(1); + expect(results.counts).toEqual([1]); +}); + test('measureFunction runs specified number of times', async () => { const fn = jest.fn(() => fib(5)); const results = await measureFunction(fn, { runs: 20, warmupRuns: 0, writeFile: false }); diff --git a/packages/measure/src/index.ts b/packages/measure/src/index.ts index b39aaca76..c0a1899f2 100644 --- a/packages/measure/src/index.ts +++ b/packages/measure/src/index.ts @@ -1,6 +1,8 @@ export { configure, resetToDefaults } from './config'; export { measureRenders, measurePerformance } from './measure-renders'; export { measureFunction } from './measure-function'; +export { measureAsyncFunction } from './measure-async-function'; export type { MeasureRendersOptions } from './measure-renders'; export type { MeasureFunctionOptions } from './measure-function'; +export type { MeasureAsyncFunctionOptions } from './measure-async-function'; export type { MeasureType, MeasureResults } from './types'; diff --git a/packages/measure/src/measure-async-function.tsx b/packages/measure/src/measure-async-function.tsx new file mode 100644 index 000000000..6bb51de54 --- /dev/null +++ b/packages/measure/src/measure-async-function.tsx @@ -0,0 +1,42 @@ +import { config } from './config'; +import type { MeasureResults } from './types'; +import { type RunResult, getCurrentTime, processRunResults } from './measure-helpers'; +import { showFlagsOutputIfNeeded, writeTestStats } from './output'; +import { MeasureFunctionOptions } from './measure-function'; + +export interface MeasureAsyncFunctionOptions extends MeasureFunctionOptions {} + +export async function measureAsyncFunction( + fn: () => Promise, + options?: MeasureAsyncFunctionOptions +): Promise { + const stats = await measureAsyncFunctionInternal(fn, options); + + if (options?.writeFile !== false) { + await writeTestStats(stats, 'async function'); + } + + return stats; +} + +async function measureAsyncFunctionInternal( + fn: () => Promise, + options?: MeasureAsyncFunctionOptions +): Promise { + const runs = options?.runs ?? config.runs; + const warmupRuns = options?.warmupRuns ?? config.warmupRuns; + + showFlagsOutputIfNeeded(); + + const runResults: RunResult[] = []; + for (let i = 0; i < runs + warmupRuns; i += 1) { + const timeStart = getCurrentTime(); + await fn(); + const timeEnd = getCurrentTime(); + + const duration = timeEnd - timeStart; + runResults.push({ duration, count: 1 }); + } + + return processRunResults(runResults, warmupRuns); +} diff --git a/packages/measure/src/measure-function.tsx b/packages/measure/src/measure-function.tsx index 61620fb73..1c9b0bad2 100644 --- a/packages/measure/src/measure-function.tsx +++ b/packages/measure/src/measure-function.tsx @@ -1,7 +1,6 @@ -import { performance } from 'perf_hooks'; import { config } from './config'; import type { MeasureResults } from './types'; -import { type RunResult, processRunResults } from './measure-helpers'; +import { type RunResult, getCurrentTime, processRunResults } from './measure-helpers'; import { showFlagsOutputIfNeeded, writeTestStats } from './output'; export interface MeasureFunctionOptions { @@ -11,7 +10,7 @@ export interface MeasureFunctionOptions { } export async function measureFunction(fn: () => void, options?: MeasureFunctionOptions): Promise { - const stats = await measureFunctionInternal(fn, options); + const stats = measureFunctionInternal(fn, options); if (options?.writeFile !== false) { await writeTestStats(stats, 'function'); @@ -38,7 +37,3 @@ function measureFunctionInternal(fn: () => void, options?: MeasureFunctionOption return processRunResults(runResults, warmupRuns); } - -function getCurrentTime() { - return performance.now(); -} diff --git a/packages/measure/src/measure-helpers.tsx b/packages/measure/src/measure-helpers.tsx index c537c7a60..6096b7a2f 100644 --- a/packages/measure/src/measure-helpers.tsx +++ b/packages/measure/src/measure-helpers.tsx @@ -1,3 +1,4 @@ +import { performance } from 'perf_hooks'; import * as math from 'mathjs'; import type { MeasureResults } from './types'; @@ -6,6 +7,10 @@ export interface RunResult { count: number; } +export function getCurrentTime() { + return performance.now(); +} + export function processRunResults(inputResults: RunResult[], warmupRuns: number): MeasureResults { const warmupResults = inputResults.slice(0, warmupRuns); const results = inputResults.slice(warmupRuns); diff --git a/packages/measure/src/types.ts b/packages/measure/src/types.ts index a01e6e8e0..6711257f3 100644 --- a/packages/measure/src/types.ts +++ b/packages/measure/src/types.ts @@ -1,5 +1,5 @@ /** Type of measured performance characteristic. */ -export type MeasureType = 'render' | 'function'; +export type MeasureType = 'render' | 'function' | 'async function'; /** * Type representing the result of `measure*` functions. diff --git a/packages/reassure/README.md b/packages/reassure/README.md index 3bd3381f9..9640a4dbc 100644 --- a/packages/reassure/README.md +++ b/packages/reassure/README.md @@ -43,6 +43,8 @@ - [`MeasureRendersOptions` type](#measurerendersoptions-type) - [`measureFunction` function](#measurefunction-function) - [`MeasureFunctionOptions` type](#measurefunctionoptions-type) + - [`measureAsyncFunction` function](#measureasyncfunction-function) + - [`MeasureAsyncFunctionOptions` type](#measureasyncfunctionoptions-type) - [Configuration](#configuration) - [Default configuration](#default-configuration) - [`configure` function](#configure-function) @@ -397,11 +399,40 @@ async function measureFunction( interface MeasureFunctionOptions { runs?: number; warmupRuns?: number; + writeFile?: boolean; } ``` - **`runs`**: number of runs per series for the particular test - **`warmupRuns`**: number of additional warmup runs that will be done and discarded before the actual runs. +- **`writeFile`**: (default `true`) should write output to file. + +#### `measureAsyncFunction` function + +Allows you to wrap any **asynchronous** function, measure its execution times and write results to the output file. You can use optional `options` to customize aspects of the testing. Note: the execution count will always be one. + +> **Note**: Measuring performance of asynchronous functions can be tricky. These functions often depend on external conditions like I/O operations, network requests, or storage access, which introduce unpredictable timing variations in your measurements. For stable and meaningful performance metrics, **always ensure all external calls are properly mocked in your test environment to avoid polluting your performance measurements with uncontrollable factors.** + +```ts +async function measureAsyncFunction( + fn: () => Promise, + options?: MeasureAsyncFunctionOptions +): Promise { +``` + +#### `MeasureAsyncFunctionOptions` type + +```ts +interface MeasureAsyncFunctionOptions { + runs?: number; + warmupRuns?: number; + writeFile?: boolean; +} +``` + +- **`runs`**: number of runs per series for the particular test +- **`warmupRuns`**: number of additional warmup runs that will be done and discarded before the actual runs. +- **`writeFile`**: (default `true`) should write output to file. ### Configuration diff --git a/packages/reassure/src/index.ts b/packages/reassure/src/index.ts index e457a29c0..f57b0c375 100644 --- a/packages/reassure/src/index.ts +++ b/packages/reassure/src/index.ts @@ -1,6 +1,7 @@ export { measureRenders, measureFunction, + measureAsyncFunction, configure, resetToDefaults, measurePerformance, @@ -11,6 +12,7 @@ export type { MeasureResults, MeasureRendersOptions, MeasureFunctionOptions, + MeasureAsyncFunctionOptions, MeasureType, } from '@callstack/reassure-measure'; export type { diff --git a/test-apps/native/src/fib.perf.tsx b/test-apps/native/src/fib.perf.tsx index 1dc38255c..d5e22abf4 100644 --- a/test-apps/native/src/fib.perf.tsx +++ b/test-apps/native/src/fib.perf.tsx @@ -1,4 +1,7 @@ -import { measureFunction } from '@callstack/reassure-measure'; +import { + measureFunction, + measureAsyncFunction, +} from '@callstack/reassure-measure'; function fib(n: number): number { if (n <= 1) { @@ -15,11 +18,29 @@ describe('`fib` function', () => { await measureFunction(() => fib(30)); }); + test('fib(30) async', async () => { + await measureAsyncFunction(async () => + Promise.resolve().then(() => fib(30)), + ); + }); + test('fib(31)', async () => { await measureFunction(() => fib(31)); }); + test('fib(31) async', async () => { + await measureAsyncFunction(async () => + Promise.resolve().then(() => fib(31)), + ); + }); + test('fib(32)', async () => { await measureFunction(() => fib(32)); }); + + test('fib(32) async', async () => { + await measureAsyncFunction(async () => + Promise.resolve().then(() => fib(32)), + ); + }); });