Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Add browser compatibility #7

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

89 changes: 89 additions & 0 deletions src/experiment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { defaultOptions, defaultPublish, Options, PerformanceApi } from ".";

export type ExperimentSyncFunction<TParams extends any[], TResult> = (
...args: TParams
) => TResult;


export interface ExperimentSyncParams<TParams extends any[], TResult> {
name: string;
control: ExperimentSyncFunction<TParams, TResult>;
candidate: ExperimentSyncFunction<TParams, TResult>;
options?: Options<TParams, TResult>;
}

/**
* A factory that creates an experiment function.
*
* @param name - The name of the experiment, typically for use in publish.
* @param control - The legacy function you are trying to replace.
* @param candidate - The new function intended to replace the control.
* @param [options] - Options for the experiment. You will usually want to specify a publish function.
* @returns A function that acts like the control while also running the candidate and publishing results.
*/

export function unconfiguredExperimentSync({
performance
}: {
performance: PerformanceApi
}) {
return function <TParams extends any[], TResult>({
name,
control,
candidate,
options = defaultOptions
}: ExperimentSyncParams<TParams, TResult>): ExperimentSyncFunction<TParams, TResult> {
const publish = options.publish || defaultPublish;

return (...args): TResult => {
let controlResult: TResult | undefined;
let candidateResult: TResult | undefined;
let controlError: any;
let candidateError: any;
let controlTimeMs: number;
let candidateTimeMs: number;
const isEnabled: boolean = !options.enabled || options.enabled(...args);

function publishResults(): void {
if (isEnabled) {
publish({
experimentName: name,
experimentArguments: args,
controlResult,
candidateResult,
controlError,
candidateError,
controlTimeMs,
candidateTimeMs
});
}
}

if (isEnabled) {
try {
// Not using bigint version of hrtime for Node 8 compatibility
const candidateStartTime = performance.now()
candidateResult = candidate(...args);
const candidateStopTime = performance.now()
candidateTimeMs = candidateStopTime - candidateStartTime
} catch (e) {
candidateError = e;
}
}

try {
const controlStartTime = performance.now()
controlResult = control(...args);
const controlStopTime = performance.now()
controlTimeMs = controlStopTime - controlStartTime
} catch (e) {
controlError = e;
publishResults();
throw e;
}

publishResults();
return controlResult;
};
}
}
104 changes: 104 additions & 0 deletions src/experimentAsync.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { defaultOptions, defaultPublish, Options, PerformanceApi } from ".";
import { ExperimentSyncFunction } from "./experiment";

export type ExperimentAsyncFunction<TParams extends any[], TResult> =
ExperimentSyncFunction<TParams, Promise<TResult>>;


export interface ExperimentAsyncParams<TParams extends any[], TResult> {
name: string;
control: ExperimentAsyncFunction<TParams, TResult>;
candidate: ExperimentAsyncFunction<TParams, TResult>;
options?: Options<TParams, TResult>;
}

async function executeAndTime<TParams extends any[], TResult>(
performance: PerformanceApi,
controlOrCandidate: ExperimentAsyncFunction<TParams, TResult>,
args: TParams
): Promise<[TResult, number]> {
// Not using bigint version of hrtime for Node 8 compatibility
const startTime = performance.now()
const result = await controlOrCandidate(...args);
const stopTime = performance.now()
const timeMs = stopTime - startTime
return [result, timeMs];
}

/**
* A factory that creates an asynchronous experiment function.
*
* @param name - The name of the experiment, typically for use in publish.
* @param control - The legacy async function you are trying to replace.
* @param candidate - The new async function intended to replace the control.
* @param [options] - Options for the experiment. You will usually want to specify a publish function.
* @returns An async function that acts like the control while also running the candidate and publishing results.
*/
export function unconfiguredExperimentAsync({
performance
}: {
performance: PerformanceApi
}) {
return function<TParams extends any[], TResult> ({
name,
control,
candidate,
options = defaultOptions
}: ExperimentAsyncParams<TParams, TResult>): ExperimentAsyncFunction<TParams, TResult> {
const publish = options.publish || defaultPublish;

return async (...args): Promise<TResult> => {
let controlResult: TResult | undefined;
let candidateResult: TResult | undefined;
let controlError: any;
let candidateError: any;
let controlTimeMs: number | undefined;
let candidateTimeMs: number | undefined;
const isEnabled: boolean = !options.enabled || options.enabled(...args);

function publishResults(): void {
if (isEnabled) {
publish({
experimentName: name,
experimentArguments: args,
controlResult,
candidateResult,
controlError,
candidateError,
controlTimeMs,
candidateTimeMs
});
}
}

if (isEnabled) {
// Run in parallel
[[candidateResult, candidateTimeMs], [controlResult, controlTimeMs]] =
await Promise.all([
executeAndTime(performance, candidate, args).catch((e) => {
candidateError = e;
return [undefined, undefined];
}),
executeAndTime(performance, control, args).catch((e) => {
controlError = e;
return [undefined, undefined];
})
]);
} else {
controlResult = await control(...args).catch((e) => {
controlError = e;
return undefined;
});
}

publishResults();

if (controlError) {
throw controlError;
}

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return controlResult!;
};
}
}
8 changes: 7 additions & 1 deletion src/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
import {performance} from 'perf_hooks'

(global as any).perf_hooks = {
performance
}

import * as scientist from './index';

describe('experiment', () => {
Expand Down Expand Up @@ -496,7 +502,7 @@ describe('experiment', () => {

beforeEach(() => {
// eslint-disable-next-line @typescript-eslint/no-empty-function
consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => { });
});

afterEach(() => {
Expand Down
Loading