Skip to content

Commit b4d94c3

Browse files
committed
Merge branch 'performance-metrics'
2 parents e0f52d7 + aca91fb commit b4d94c3

File tree

5 files changed

+107
-14
lines changed

5 files changed

+107
-14
lines changed

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,10 @@ The `control` and the `candidate` will be run in parallel (that is, concurrently
114114

115115
If your functions use callbacks, look at wrapping them with [util.promisify](https://nodejs.org/api/util.html#util_util_promisify_original).
116116

117+
### Timing / profiling
118+
119+
Published results now include timings for both the control and the candidate. Timings are in milliseconds (ms). Note that other queued tasks could affect asynchronous timings, at least in theory.
120+
117121
## FAQ
118122

119123
Q. Why would I use this library?
@@ -159,10 +163,6 @@ Q. Why doesn't Tzientist randomize the order in which the control and the candid
159163

160164
A. Because those functions should not have side effects.
161165

162-
## To do
163-
164-
- Timer support.
165-
166166
## Why
167167

168168
GitHub's [Scientist](https://github.com/github/scientist) Ruby library is a brilliant concept. Unfortunately the Node.js alternatives aren't very TypeScript-friendly.
@@ -183,7 +183,7 @@ Feature parity with Scientist is _not_ a goal.
183183
### Technology stack
184184

185185
- TypeScript v3.8
186-
- Node v12 (should work on v8.17.0 or higher)
186+
- Node v12 (should work on v8.17.0 or higher, but tests require v12)
187187
- npm v6 (I like yarn, but not everyone does)
188188
- [Prettier](https://prettier.io/)
189189
- ESLint

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "tzientist",
3-
"version": "2.1.1",
3+
"version": "2.2.0",
44
"description": "Scientist-like library for Node.js in TypeScript",
55
"main": "lib/index.js",
66
"typings": "lib/index.d.ts",

src/index.spec.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ describe('experiment', () => {
5454
expect(results.candidateResult).toBe(3);
5555
expect(results.controlError).toBeUndefined();
5656
expect(results.candidateError).toBeUndefined();
57+
expect(results.controlTimeMs).toBeDefined();
58+
expect(results.controlTimeMs).toBeGreaterThan(0);
59+
expect(results.candidateTimeMs).toBeDefined();
60+
expect(results.candidateTimeMs).toBeGreaterThan(0);
5761
});
5862
});
5963

@@ -101,6 +105,10 @@ describe('experiment', () => {
101105
expect(results.candidateResult).toBe('C');
102106
expect(results.controlError).toBeUndefined();
103107
expect(results.candidateError).toBeUndefined();
108+
expect(results.controlTimeMs).toBeDefined();
109+
expect(results.controlTimeMs).toBeGreaterThan(0);
110+
expect(results.candidateTimeMs).toBeDefined();
111+
expect(results.candidateTimeMs).toBeGreaterThan(0);
104112
});
105113
});
106114

@@ -149,6 +157,9 @@ describe('experiment', () => {
149157
expect(results.controlError).toBeUndefined();
150158
expect(results.candidateError).toBeDefined();
151159
expect(results.candidateError.message).toBe("Candy I can't let you go");
160+
expect(results.controlTimeMs).toBeDefined();
161+
expect(results.controlTimeMs).toBeGreaterThan(0);
162+
expect(results.candidateTimeMs).toBeUndefined();
152163
});
153164
});
154165

@@ -199,6 +210,9 @@ describe('experiment', () => {
199210
expect(results.controlError).toBeDefined();
200211
expect(results.controlError.message).toBe('Kaos!');
201212
expect(results.candidateError).toBeUndefined();
213+
expect(results.controlTimeMs).toBeUndefined();
214+
expect(results.candidateTimeMs).toBeDefined();
215+
expect(results.candidateTimeMs).toBeGreaterThan(0);
202216
});
203217
});
204218

@@ -250,6 +264,8 @@ describe('experiment', () => {
250264
expect(results.controlError.message).toBe('Kaos!');
251265
expect(results.candidateError).toBeDefined();
252266
expect(results.candidateError.message).toBe("Candy I can't let you go");
267+
expect(results.controlTimeMs).toBeUndefined();
268+
expect(results.candidateTimeMs).toBeUndefined();
253269
});
254270
});
255271

@@ -627,6 +643,10 @@ describe('experimentAsync', () => {
627643
expect(results.candidateResult).toBe(3);
628644
expect(results.controlError).toBeUndefined();
629645
expect(results.candidateError).toBeUndefined();
646+
expect(results.controlTimeMs).toBeDefined();
647+
expect(results.controlTimeMs).toBeGreaterThan(0);
648+
expect(results.candidateTimeMs).toBeDefined();
649+
expect(results.candidateTimeMs).toBeGreaterThan(0);
630650
});
631651
});
632652

@@ -685,6 +705,10 @@ describe('experimentAsync', () => {
685705
expect(results.candidateResult).toBe('C');
686706
expect(results.controlError).toBeUndefined();
687707
expect(results.candidateError).toBeUndefined();
708+
expect(results.controlTimeMs).toBeDefined();
709+
expect(results.controlTimeMs).toBeGreaterThan(0);
710+
expect(results.candidateTimeMs).toBeDefined();
711+
expect(results.candidateTimeMs).toBeGreaterThan(0);
688712
});
689713
});
690714

@@ -743,6 +767,9 @@ describe('experimentAsync', () => {
743767
expect(results.controlError).toBeUndefined();
744768
expect(results.candidateError).toBeDefined();
745769
expect(results.candidateError.message).toBe("Candy I can't let you go");
770+
expect(results.controlTimeMs).toBeDefined();
771+
expect(results.controlTimeMs).toBeGreaterThan(0);
772+
expect(results.candidateTimeMs).toBeUndefined();
746773
});
747774
});
748775

@@ -803,6 +830,9 @@ describe('experimentAsync', () => {
803830
expect(results.controlError).toBeDefined();
804831
expect(results.controlError.message).toBe('Kaos!');
805832
expect(results.candidateError).toBeUndefined();
833+
expect(results.controlTimeMs).toBeUndefined();
834+
expect(results.candidateTimeMs).toBeDefined();
835+
expect(results.candidateTimeMs).toBeGreaterThan(0);
806836
});
807837
});
808838

@@ -863,6 +893,8 @@ describe('experimentAsync', () => {
863893
expect(results.controlError.message).toBe('Kaos!');
864894
expect(results.candidateError).toBeDefined();
865895
expect(results.candidateError.message).toBe("Candy I can't let you go");
896+
expect(results.controlTimeMs).toBeUndefined();
897+
expect(results.candidateTimeMs).toBeUndefined();
866898
});
867899
});
868900

@@ -1051,5 +1083,30 @@ describe('experimentAsync', () => {
10511083

10521084
expect(elapsedMs).toBeLessThan(msPerFunction + allowedOverhead);
10531085
});
1086+
1087+
it('should publish individual timings', async () => {
1088+
const allowedVarianceMs = 125;
1089+
const minMs = msPerFunction - allowedVarianceMs;
1090+
const maxMs = msPerFunction + allowedVarianceMs;
1091+
const experiment = scientist.experimentAsync({
1092+
name: 'async parallel2',
1093+
control: ctrl,
1094+
candidate: candi,
1095+
options: {
1096+
publish: publishMock
1097+
}
1098+
});
1099+
1100+
await experiment();
1101+
1102+
expect(publishMock.mock.calls.length).toBe(1);
1103+
const results = publishMock.mock.calls[0][0];
1104+
expect(results.controlTimeMs).toBeDefined();
1105+
expect(results.controlTimeMs).toBeGreaterThan(minMs);
1106+
expect(results.controlTimeMs).toBeLessThan(maxMs);
1107+
expect(results.candidateTimeMs).toBeDefined();
1108+
expect(results.candidateTimeMs).toBeGreaterThan(minMs);
1109+
expect(results.candidateTimeMs).toBeLessThan(maxMs);
1110+
});
10541111
});
10551112
});

src/index.ts

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,22 @@ export interface Results<TParams extends any[], TResult> {
1414
candidateResult?: TResult;
1515
controlError?: any;
1616
candidateError?: any;
17+
controlTimeMs?: number;
18+
candidateTimeMs?: number;
1719
}
1820

1921
export interface Options<TParams extends any[], TResult> {
2022
publish?: (results: Results<TParams, TResult>) => void;
2123
enabled?: (...args: TParams) => boolean;
2224
}
2325

26+
function hrtimeToMs(hrtime: [number, number]): number {
27+
const MS_PER_SEC = 1000;
28+
const NS_PER_MS = 1e6;
29+
const [seconds, nanoseconds] = hrtime;
30+
return seconds * MS_PER_SEC + nanoseconds / NS_PER_MS;
31+
}
32+
2433
function defaultPublish<TParams extends any[], TResult>(
2534
results: Results<TParams, TResult>
2635
): void {
@@ -64,6 +73,8 @@ export function experiment<TParams extends any[], TResult>({
6473
let candidateResult: TResult | undefined;
6574
let controlError: any;
6675
let candidateError: any;
76+
let controlTimeMs: number;
77+
let candidateTimeMs: number;
6778
const isEnabled: boolean = !options.enabled || options.enabled(...args);
6879

6980
function publishResults(): void {
@@ -74,21 +85,28 @@ export function experiment<TParams extends any[], TResult>({
7485
controlResult,
7586
candidateResult,
7687
controlError,
77-
candidateError
88+
candidateError,
89+
controlTimeMs,
90+
candidateTimeMs
7891
});
7992
}
8093
}
8194

8295
if (isEnabled) {
8396
try {
97+
// Not using bigint version of hrtime for Node 8 compatibility
98+
const candidateStartTime = process.hrtime();
8499
candidateResult = candidate(...args);
100+
candidateTimeMs = hrtimeToMs(process.hrtime(candidateStartTime));
85101
} catch (e) {
86102
candidateError = e;
87103
}
88104
}
89105

90106
try {
107+
const controlStartTime = process.hrtime();
91108
controlResult = control(...args);
109+
controlTimeMs = hrtimeToMs(process.hrtime(controlStartTime));
92110
} catch (e) {
93111
controlError = e;
94112
publishResults();
@@ -100,6 +118,17 @@ export function experiment<TParams extends any[], TResult>({
100118
};
101119
}
102120

121+
async function executeAndTime<TParams extends any[], TResult>(
122+
controlOrCandidate: ExperimentAsyncFunction<TParams, TResult>,
123+
args: TParams
124+
): Promise<[TResult, number]> {
125+
// Not using bigint version of hrtime for Node 8 compatibility
126+
const startTime = process.hrtime();
127+
const result = await controlOrCandidate(...args);
128+
const timeMs = hrtimeToMs(process.hrtime(startTime));
129+
return [result, timeMs];
130+
}
131+
103132
/**
104133
* A factory that creates an asynchronous experiment function.
105134
*
@@ -127,6 +156,8 @@ export function experimentAsync<TParams extends any[], TResult>({
127156
let candidateResult: TResult | undefined;
128157
let controlError: any;
129158
let candidateError: any;
159+
let controlTimeMs: number | undefined;
160+
let candidateTimeMs: number | undefined;
130161
const isEnabled: boolean = !options.enabled || options.enabled(...args);
131162

132163
function publishResults(): void {
@@ -137,21 +168,26 @@ export function experimentAsync<TParams extends any[], TResult>({
137168
controlResult,
138169
candidateResult,
139170
controlError,
140-
candidateError
171+
candidateError,
172+
controlTimeMs,
173+
candidateTimeMs
141174
});
142175
}
143176
}
144177

145178
if (isEnabled) {
146179
// Run in parallel
147-
[candidateResult, controlResult] = await Promise.all([
148-
candidate(...args).catch((e) => {
180+
[
181+
[candidateResult, candidateTimeMs],
182+
[controlResult, controlTimeMs]
183+
] = await Promise.all([
184+
executeAndTime(candidate, args).catch((e) => {
149185
candidateError = e;
150-
return undefined;
186+
return [undefined, undefined];
151187
}),
152-
control(...args).catch((e) => {
188+
executeAndTime(control, args).catch((e) => {
153189
controlError = e;
154-
return undefined;
190+
return [undefined, undefined];
155191
})
156192
]);
157193
} else {

0 commit comments

Comments
 (0)