Skip to content

Commit

Permalink
refactor: future support for returning multiple results (#464)
Browse files Browse the repository at this point in the history
* refactor: future support for returning multiple results

* chore: add changeset

* chore: adapt tests to API changes
  • Loading branch information
mdjastrzebski committed Feb 22, 2024
1 parent a65ffe0 commit 3cf5660
Show file tree
Hide file tree
Showing 8 changed files with 75 additions and 53 deletions.
5 changes: 5 additions & 0 deletions .changeset/yellow-months-smoke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@callstack/reassure-measure': minor
---

future-proofing: support returning multiple results for a single `measureRenders`/`measureFunction` call.
29 changes: 16 additions & 13 deletions packages/measure/src/__tests__/measure-function.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,34 +14,37 @@ function fib(n: number): number {
test('measureFunction captures results', async () => {
const fn = jest.fn(() => fib(5));
const results = await measureFunction(fn, { runs: 1, warmupRuns: 0, writeFile: false });
const mainResult = results[0];

expect(fn).toHaveBeenCalledTimes(1);
expect(results.runs).toBe(1);
expect(results.counts).toEqual([1]);
expect(mainResult.runs).toBe(1);
expect(mainResult.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 });
const mainResult = results[0];

expect(fn).toHaveBeenCalledTimes(20);
expect(results.runs).toBe(20);
expect(results.durations).toHaveLength(20);
expect(results.counts).toHaveLength(20);
expect(results.meanCount).toBe(1);
expect(results.stdevCount).toBe(0);
expect(mainResult.runs).toBe(20);
expect(mainResult.durations).toHaveLength(20);
expect(mainResult.counts).toHaveLength(20);
expect(mainResult.meanCount).toBe(1);
expect(mainResult.stdevCount).toBe(0);
});

test('measureFunction applies "warmupRuns" option', async () => {
const fn = jest.fn(() => fib(5));
const results = await measureFunction(fn, { runs: 10, warmupRuns: 1, writeFile: false });
const mainResult = results[0];

expect(fn).toHaveBeenCalledTimes(11);
expect(results.runs).toBe(10);
expect(results.durations).toHaveLength(10);
expect(results.counts).toHaveLength(10);
expect(results.meanCount).toBe(1);
expect(results.stdevCount).toBe(0);
expect(mainResult.runs).toBe(10);
expect(mainResult.durations).toHaveLength(10);
expect(mainResult.counts).toHaveLength(10);
expect(mainResult.meanCount).toBe(1);
expect(mainResult.stdevCount).toBe(0);
});

const errorsToIgnore = ['❌ Measure code is running under incorrect Node.js configuration.'];
Expand All @@ -59,7 +62,7 @@ test('measureFunction should log error when running under incorrect node flags',
resetHasShownFlagsOutput();
const results = await measureFunction(jest.fn(), { runs: 1, writeFile: false });

expect(results.runs).toBe(1);
expect(results[0].runs).toBe(1);
const consoleErrorCalls = jest.mocked(realConsole.error).mock.calls;
expect(stripAnsi(consoleErrorCalls[0][0])).toMatchInlineSnapshot(`
"❌ Measure code is running under incorrect Node.js configuration.
Expand Down
41 changes: 23 additions & 18 deletions packages/measure/src/__tests__/measure-renders.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ beforeEach(() => {
test('measureRenders run test given number of times', async () => {
const scenario = jest.fn(() => Promise.resolve(null));
const results = await measureRenders(<View />, { runs: 20, scenario, writeFile: false });
expect(results.runs).toBe(20);
expect(results.durations).toHaveLength(20);
expect(results.counts).toHaveLength(20);
expect(results.meanCount).toBe(1);
expect(results.stdevCount).toBe(0);
const mainResult = results[0];

expect(mainResult.runs).toBe(20);
expect(mainResult.durations).toHaveLength(20);
expect(mainResult.counts).toHaveLength(20);
expect(mainResult.meanCount).toBe(1);
expect(mainResult.stdevCount).toBe(0);

// Test is actually run 21 times = 20 runs + 1 warmup runs
expect(scenario).toHaveBeenCalledTimes(21);
Expand All @@ -31,20 +33,21 @@ test('measureRenders run test given number of times', async () => {
test('measureRenders applies "warmupRuns" option', async () => {
const scenario = jest.fn(() => Promise.resolve(null));
const results = await measureRenders(<View />, { runs: 10, scenario, writeFile: false });
const mainResult = results[0];

expect(scenario).toHaveBeenCalledTimes(11);
expect(results.runs).toBe(10);
expect(results.durations).toHaveLength(10);
expect(results.counts).toHaveLength(10);
expect(results.meanCount).toBe(1);
expect(results.stdevCount).toBe(0);
expect(mainResult.runs).toBe(10);
expect(mainResult.durations).toHaveLength(10);
expect(mainResult.counts).toHaveLength(10);
expect(mainResult.meanCount).toBe(1);
expect(mainResult.stdevCount).toBe(0);
});

test('measureRenders should log error when running under incorrect node flags', async () => {
resetHasShownFlagsOutput();
const results = await measureRenders(<View />, { runs: 1, writeFile: false });

expect(results.runs).toBe(1);
expect(results[0].runs).toBe(1);
const consoleErrorCalls = jest.mocked(realConsole.error).mock.calls;
expect(stripAnsi(consoleErrorCalls[0][0])).toMatchInlineSnapshot(`
"❌ Measure code is running under incorrect Node.js configuration.
Expand All @@ -59,13 +62,15 @@ function IgnoreChildren(_: React.PropsWithChildren<{}>) {

test('measureRenders does not measure wrapper execution', async () => {
const results = await measureRenders(<View />, { wrapper: IgnoreChildren, writeFile: false });
expect(results.runs).toBe(10);
expect(results.durations).toHaveLength(10);
expect(results.counts).toHaveLength(10);
expect(results.meanDuration).toBe(0);
expect(results.meanCount).toBe(0);
expect(results.stdevDuration).toBe(0);
expect(results.stdevCount).toBe(0);
const mainResult = results[0];

expect(mainResult.runs).toBe(10);
expect(mainResult.durations).toHaveLength(10);
expect(mainResult.counts).toHaveLength(10);
expect(mainResult.meanDuration).toBe(0);
expect(mainResult.meanCount).toBe(0);
expect(mainResult.stdevDuration).toBe(0);
expect(mainResult.stdevCount).toBe(0);
});

function Wrapper({ children }: React.PropsWithChildren<{}>) {
Expand Down
17 changes: 10 additions & 7 deletions packages/measure/src/measure-function.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { performance } from 'perf_hooks';
import { config } from './config';
import type { MeasureResults } from './types';
import type { MeasureResult } from './types';
import { type RunResult, processRunResults } from './measure-helpers';
import { showFlagsOutputIfNeeded, writeTestStats } from './output';

Expand All @@ -10,17 +10,18 @@ export interface MeasureFunctionOptions {
writeFile?: boolean;
}

export async function measureFunction(fn: () => void, options?: MeasureFunctionOptions): Promise<MeasureResults> {
const stats = await measureFunctionInternal(fn, options);
export async function measureFunction(fn: () => void, options?: MeasureFunctionOptions): Promise<MeasureResult[]> {
const results = await measureFunctionInternal(fn, options);

if (options?.writeFile !== false) {
await writeTestStats(stats, 'function');
// Currently measureRenders returns only a single result, but in the future it might return multiple ones.
await writeTestStats(results[0], 'function');
}

return stats;
return results;
}

function measureFunctionInternal(fn: () => void, options?: MeasureFunctionOptions): MeasureResults {
function measureFunctionInternal(fn: () => void, options?: MeasureFunctionOptions): MeasureResult[] {
const runs = options?.runs ?? config.runs;
const warmupRuns = options?.warmupRuns ?? config.warmupRuns;

Expand All @@ -36,7 +37,9 @@ function measureFunctionInternal(fn: () => void, options?: MeasureFunctionOption
runResults.push({ duration, count: 1 });
}

return processRunResults(runResults, warmupRuns);
// Currently we return only a single result, but in the future we plan to return multiple ones.
const result = processRunResults(runResults, warmupRuns);
return [result];
}

function getCurrentTime() {
Expand Down
4 changes: 2 additions & 2 deletions packages/measure/src/measure-helpers.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import * as math from 'mathjs';
import type { MeasureResults } from './types';
import type { MeasureResult } from './types';

export interface RunResult {
duration: number;
count: number;
}

export function processRunResults(results: RunResult[], warmupRuns: number): MeasureResults {
export function processRunResults(results: RunResult[], warmupRuns: number): MeasureResult {
results = results.slice(warmupRuns);
results.sort((first, second) => second.duration - first.duration); // duration DESC

Expand Down
22 changes: 14 additions & 8 deletions packages/measure/src/measure-renders.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { config } from './config';
import { RunResult, processRunResults } from './measure-helpers';
import { showFlagsOutputIfNeeded, writeTestStats } from './output';
import { resolveTestingLibrary } from './testing-library';
import type { MeasureResults } from './types';
import type { MeasureResult } from './types';

logger.configure({
verbose: process.env.REASSURE_VERBOSE === 'true' || process.env.REASSURE_VERBOSE === '1',
Expand All @@ -19,14 +19,18 @@ export interface MeasureRendersOptions {
writeFile?: boolean;
}

export async function measureRenders(ui: React.ReactElement, options?: MeasureRendersOptions): Promise<MeasureResults> {
const stats = await measureRendersInternal(ui, options);
export async function measureRenders(
ui: React.ReactElement,
options?: MeasureRendersOptions
): Promise<MeasureResult[]> {
const results = await measureRendersInternal(ui, options);

if (options?.writeFile !== false) {
await writeTestStats(stats, 'render');
// Currently measureRenders returns only a single result, but in the future it might return multiple ones.
await writeTestStats(results[0], 'render');
}

return stats;
return results;
}

/**
Expand All @@ -35,7 +39,7 @@ export async function measureRenders(ui: React.ReactElement, options?: MeasureRe
export async function measurePerformance(
ui: React.ReactElement,
options?: MeasureRendersOptions
): Promise<MeasureResults> {
): Promise<MeasureResult[]> {
logger.warnOnce(
'The `measurePerformance` function has been renamed to `measureRenders`.\n\nThe `measurePerformance` alias is now deprecated and will be removed in future releases.'
);
Expand All @@ -46,7 +50,7 @@ export async function measurePerformance(
async function measureRendersInternal(
ui: React.ReactElement,
options?: MeasureRendersOptions
): Promise<MeasureResults> {
): Promise<MeasureResult[]> {
const runs = options?.runs ?? config.runs;
const scenario = options?.scenario;
const warmupRuns = options?.warmupRuns ?? config.warmupRuns;
Expand Down Expand Up @@ -93,7 +97,9 @@ async function measureRendersInternal(
);
}

return processRunResults(runResults, warmupRuns);
// Currently we return only a single result, but in the future we plan to return multiple ones.
const result = processRunResults(runResults, warmupRuns);
return [result];
}

export function buildUiToRender(
Expand Down
6 changes: 3 additions & 3 deletions packages/measure/src/output.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import * as fs from 'fs/promises';
import * as logger from '@callstack/reassure-logger';
import { config } from './config';
import type { MeasureResults, MeasureType } from './types';
import type { MeasureResult, MeasureType } from './types';

export async function writeTestStats(
stats: MeasureResults,
results: MeasureResult,
type: MeasureType,
outputFilePath: string = config.outputFile
): Promise<void> {
const name = expect.getState().currentTestName;
const line = JSON.stringify({ name, type, ...stats }) + '\n';
const line = JSON.stringify({ name, type, ...results }) + '\n';

try {
await fs.appendFile(outputFilePath, line);
Expand Down
4 changes: 2 additions & 2 deletions packages/measure/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
export type MeasureType = 'render' | 'function';

/**
* Type representing the result of `measure*` functions.
* Result of a single measurement.
*/
export interface MeasureResults {
export interface MeasureResult {
/** Number of times the test subject was run */
runs: number;

Expand Down

0 comments on commit 3cf5660

Please sign in to comment.