Socket
Socket
Sign inDemoInstall

cronometro

Package Overview
Dependencies
Maintainers
1
Versions
32
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

cronometro - npm Package Compare versions

Comparing version 0.3.0 to 0.4.0

lib/runner.js

7

CHANGELOG.md

@@ -0,1 +1,8 @@

### 2020-04-22 / 0.4.0
- chore: Allow greater test timeouts.
- feat: Removed useless debug infrastructure.
- feat: Reimplemented warmup mode.
- feat: Use worker_threads for execution. Tested everything.
### 2020-03-05 / 0.3.0

@@ -2,0 +9,0 @@

190

lib/index.js
'use strict';
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
function __export(m) {
for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p];
}
Object.defineProperty(exports, "__esModule", { value: true });
// @ts-ignore
const native_hdr_histogram_1 = __importDefault(require("native-hdr-histogram"));
const path_1 = require("path");
const worker_threads_1 = require("worker_threads");
const models_1 = require("./models");
const print_1 = require("./print");
const debug = (process.env.NODE_DEBUG || '').includes('cronometro');
function schedule(operation) {
process.nextTick(operation);
}
function runIteration(context) {
function trackResults(error) {
// Handle error
if (error) {
context.results[context.current.name] = { success: false, error, mean: 0 };
schedule(() => processQueue(context));
return;
}
const { histogram } = context.current;
if (histogram.record(Number(process.hrtime.bigint() - start))) {
context.current.records++;
}
if (context.errorThreshold > 0) {
const completedPercentage = Math.floor((1 - context.current.remaining / context.iterations) * 10000);
// Check if abort the test earlier. It is checked every 5% after 10% of the iterations
if (completedPercentage > 1000 && completedPercentage % 500 === 0) {
const standardErrorPercentage = histogram.stddev() / Math.sqrt(context.current.records) / histogram.mean();
if (standardErrorPercentage < context.errorThreshold) {
context.current.remaining = 0;
}
}
}
if (context.current.remaining === 0) {
context.results[context.current.name] = {
success: true,
size: context.current.records,
min: histogram.min(),
max: histogram.min(),
mean: histogram.mean(),
stddev: histogram.stddev(),
percentiles: histogram
.percentiles()
.reduce((accu, { percentile, value }) => {
accu[percentile] = value;
return accu;
}, {}),
standardError: histogram.stddev() / Math.sqrt(context.current.records)
};
schedule(() => processQueue(context));
return;
}
context.current.remaining--;
schedule(() => runIteration(context));
__export(require("./models"));
function scheduleNextTest(context) {
// We still have work to do
if (context.current < context.tests.length) {
return process.nextTick(() => run(context));
}
if (debug) {
console.debug(`cronometro: Executing test ${context.current.name}, ${context.current.remaining} iterations to go`);
if (context.print) {
const { colors, compare, compareMode } = {
colors: true,
compare: false,
compareMode: 'base',
...(context.print === true ? {} : context.print)
};
print_1.printResults(context.results, colors, compare, compareMode);
}
const start = process.hrtime.bigint();
try {
// Execute the function and get the response time - Handle also promises
const callResult = context.current.test(trackResults);
if (callResult && typeof callResult.then === 'function') {
callResult.then(() => trackResults(null), trackResults);
context.callback(null, context.results);
}
function run(context) {
const name = context.tests[context.current][0];
const worker = new worker_threads_1.Worker(path_1.join(__dirname, '../lib/runner.js'), {
workerData: {
path: process.argv[1],
index: context.current,
iterations: context.iterations,
warmup: context.warmup,
errorThreshold: context.errorThreshold
}
else if (context.current.test.length === 0) {
trackResults(null);
}
}
catch (error) {
context.results[context.current.name] = { success: false, error };
schedule(() => processQueue(context));
}
});
worker.on('error', (error) => {
context.results[name] = {
success: false,
error,
size: 0,
min: 0,
max: 0,
mean: 0,
stddev: 0,
percentiles: {},
standardError: 0
};
context.current++;
scheduleNextTest(context);
});
worker.on('message', (result) => {
context.results[name] = result;
context.current++;
scheduleNextTest(context);
});
}
function processQueue(context) {
// Get the next test to run
const next = context.queue.shift();
if (!next) {
return context.callback(null, context.results);
function cronometro(tests, options, cb) {
/* istanbul ignore next */
if (!worker_threads_1.isMainThread) {
worker_threads_1.workerData.tests = Object.entries(tests);
return;
}
const testContext = context;
testContext.current = {
name: next[0],
test: next[1],
remaining: context.iterations - 1,
records: 0,
histogram: new native_hdr_histogram_1.default(1, 1e9, 5)
};
schedule(() => runIteration(testContext));
}
function cronometro(tests, options, callback) {
let promise;

@@ -97,5 +69,6 @@ let promiseResolve;

if (typeof options === 'function') {
callback = options;
cb = options;
options = {};
}
let callback = cb;
if (!callback) {

@@ -114,9 +87,3 @@ promise = new Promise((resolve, reject) => {

// Parse and validate options
const { iterations, errorThreshold, print, warmup } = {
iterations: 1e4,
warmup: true,
errorThreshold: 1,
print: true,
...options
};
const { iterations, errorThreshold, print, warmup } = { ...models_1.defaultOptions, ...options };
// tslint:disable-next-line strict-type-predicates

@@ -132,39 +99,14 @@ if (typeof iterations !== 'number' || iterations < 1) {

}
// Process all tests
// Prepare the test
const context = {
queue: Object.entries(tests),
results: {},
warmup,
iterations,
errorThreshold: errorThreshold / 100,
callback(error, results) {
if (error) {
callback(error);
return;
}
if (print) {
const { colors, compare, compareMode } = {
colors: true,
compare: false,
compareMode: 'base',
...(print === true ? {} : print)
};
print_1.printResults(results, colors, compare, compareMode);
}
callback(null, results);
}
};
const boot = {
queue: warmup ? context.queue.slice(0) : [],
print,
tests: Object.entries(tests),
results: {},
iterations,
errorThreshold: errorThreshold / 100,
callback(error) {
if (error) {
callback(error);
return;
}
schedule(() => processQueue(context));
}
current: 0,
callback
};
schedule(() => processQueue(boot));
process.nextTick(() => run(context));
return promise;

@@ -171,0 +113,0 @@ }

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.defaultOptions = {
iterations: 1e4,
warmup: true,
errorThreshold: 1,
print: true
};
exports.percentiles = [0.001, 0.01, 0.1, 1, 2.5, 10, 25, 50, 75, 90, 97.5, 99, 99.9, 99.99, 99.999];

@@ -6,2 +6,7 @@ "use strict";

const styles = ['red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white', 'gray'];
let currentLogger = console.log;
function setLogger(logger) {
currentLogger = logger;
}
exports.setLogger = setLogger;
function printResults(results, colors, compare, mode) {

@@ -14,6 +19,14 @@ const styler = colors ? acquerello_1.colorize : acquerello_1.clean;

const entries = Object.entries(results)
.sort((a, b) => b[1].mean - a[1].mean)
.sort((a, b) => (!a[1].success ? -1 : b[1].mean - a[1].mean))
.map(([name, result]) => {
if (!result.success) {
return { name, error: result.error, throughput: '', standardError: '', relative: '', compared: '' };
return {
name,
size: 0,
error: result.error,
throughput: '',
standardError: '',
relative: '',
compared: ''
};
}

@@ -36,3 +49,3 @@ const { size, mean, standardError } = result;

name,
size: size,
size,
error: null,

@@ -57,2 +70,3 @@ throughput: (1e9 / mean).toFixed(2),

}
return row;
}

@@ -77,5 +91,5 @@ const { name, size, throughput, standardError, relative } = entry;

});
const compareHeader = `Difference with ${mode === 'base' ? compared : 'previous'}`;
const compareHeader = `Difference with ${mode === 'base' ? 'slowest' : 'previous'}`;
rows.unshift([
styler('{{bold white}}Test{{-}}'),
styler('{{bold white}}Slower tests{{-}}'),
styler('{{bold white}}Samples{{-}}'),

@@ -95,3 +109,3 @@ styler('{{bold white}}Result{{-}}'),

}
console.log(table_1.table(rows, {
currentLogger(table_1.table(rows, {
columns: {

@@ -98,0 +112,0 @@ 0: {

{
"name": "cronometro",
"version": "0.3.0",
"version": "0.4.0",
"description": "Simple benchmarking suite powered by HDR histograms.",

@@ -28,5 +28,5 @@ "homepage": "https://sw.cowtech.it/cronometro",

"scripts": {
"lint": "tslint --project tsconfig.test.json -t stylish src/*.ts test/*.ts",
"ci": "yarn lint && tap --no-color --reporter=spec --coverage-report=json --coverage-report=text --branches 90 --functions 90 --lines 90 --statements 90 test/*.spec.ts",
"test": "tap --reporter=spec --coverage-report=html --coverage-report=text --no-browser test/*.spec.ts",
"lint": "eslint src/*.ts test/*.ts",
"ci": "yarn lint && tap --timeout=60 --no-color --reporter=spec --coverage-report=json --coverage-report=text --branches 90 --functions 90 --lines 90 --statements 90 test/*.spec.ts",
"test": "tap --timeout=60 --reporter=spec --coverage-report=html --coverage-report=text --no-browser test/*.spec.ts",
"prebuild": "rm -rf lib/* types/* && yarn lint",

@@ -39,17 +39,23 @@ "build": "tsc -p .",

"acquerello": "^0.1.2",
"native-hdr-histogram": "^0.7.0",
"hdr-histogram-js": "^1.2.0",
"table": "^5.4.6"
},
"devDependencies": {
"@cowtech/tslint-config": "^5.13.0",
"@types/node": "^13.5.1",
"@types/table": "^4.0.7",
"prettier": "^1.19.1",
"tap": "^14.10.6",
"tslint": "^5.20.0",
"@cowtech/eslint-config": "^6.8.5",
"@types/node": "^13.13.1",
"@types/sinon": "^9.0.0",
"@types/table": "^5.0.0",
"prettier": "^2.0.4",
"proxyquire": "^2.1.3",
"sinon": "^9.0.2",
"tap": "^14.10.7",
"typescript": "^3.7.5"
},
"peerDependencies": {
"ts-node": "^8.9.0",
"typescript": "^3.8.3"
},
"engines": {
"node": ">= 12.0.0"
"node": ">=12.15.0"
}
}

@@ -12,2 +12,32 @@ # cronometro

## Requirements
Cronometro uses [worker_threads](https://nodejs.org/dist/latest-v12.x/docs/api/worker_threads.html) to run tests in a isolated V8 enviroments to offer the most accurate benchmark. This imposes the restrictions described in the subsections below.
### Supported Node versions
Only Node 12.x and above are supported.
### Script invocation
The main script which invokes cronometro must be executable without command line arguments, as it is how it will be called within a Worker Thread.
If you need to configure the script at runtime, use environment variables and optionally configuration files.
### TypeScript
cronometro uses [ts-node](https://www.npmjs.com/package/ts-node) to compile TypeScript files on the fly.
ts-node and TypeScript are not installed automatically by cronometro (as they are listed in `peerDependencies`) so you need to do it manually.
To pass the `tsconfig.json` project file to use, use the `TS_NODE_PROJECT` environment variable.
### API use
If you use cronometro as an API and manipulate its return value, consider that the exact same code its executed in both the main thread and in worker threads.
Inside worker threads, the cronometro function invocation will return no value and no callbacks are invoked.
You can use `isMainThread` from Worker Threads API to check in which environment the script is running.
## Usage

@@ -59,6 +89,6 @@

const results = cronometro({
test1: function() {
test1: function () {
// Do something
},
test2: function() {
test2: function () {
// Do something else

@@ -88,9 +118,14 @@ }

const pattern = /[123]/g
const replacements: { [key: string]: string } = { 1: 'a', 2: 'b', 3: 'c' }
const subject = '123123123123123123123123123123123123123123123123'
const results = cronometro(
{
test1: function() {
// Do something
single() {
subject.replace(pattern, (m) => replacements[m])
},
test2: function() {
// Do something else
multiple() {
subject.replace(/1/g, 'a').replace(/2/g, 'b').replace(/3/g, 'c')
}

@@ -113,61 +148,53 @@ },

{
"test1": {
"single": {
"success": true,
"size": 100001,
"min": 4732,
"max": 4732,
"mean": 7010.153258467415,
"stddev": 66157.57904031666,
"size": 5,
"min": 29785,
"max": 41506,
"mean": 32894.2,
"stddev": 4407.019555209621,
"percentiles": {
"0": 4732,
"50": 5216,
"75": 6077,
"100": 20469632,
"87.5": 6988,
"93.75": 13986,
"96.875": 20314,
"98.4375": 25338,
"99.21875": 27127,
"99.609375": 32019,
"99.8046875": 39667,
"99.90234375": 57527,
"99.951171875": 81029,
"99.9755859375": 237553,
"99.98779296875": 700244,
"99.993896484375": 996392,
"99.9969482421875": 1133848,
"99.99847412109375": 2129600,
"99.99923706054688": 20469632
"1": 29785,
"10": 29785,
"25": 29861,
"50": 30942,
"75": 32377,
"90": 41506,
"99": 41506,
"0.001": 29785,
"0.01": 29785,
"0.1": 29785,
"2.5": 29785,
"97.5": 41506,
"99.9": 41506,
"99.99": 41506,
"99.999": 41506
},
"standardError": 209.20758821469119
"standardError": 1970.87906072392
},
"test2": {
"multiple": {
"success": true,
"size": 100001,
"min": 1376,
"max": 1376,
"mean": 3810.3853361466386,
"stddev": 50388.5658604418,
"size": 5,
"min": 21881,
"max": 33368,
"mean": 27646.4,
"stddev": 4826.189494829228,
"percentiles": {
"0": 1376,
"50": 1478,
"75": 1587,
"100": 3069392,
"87.5": 1732,
"93.75": 1811,
"96.875": 2037,
"98.4375": 15448,
"99.21875": 20835,
"99.609375": 23230,
"99.8046875": 42270,
"99.90234375": 1129616,
"99.951171875": 1333168,
"99.9755859375": 1429376,
"99.98779296875": 1483120,
"99.993896484375": 1743192,
"99.9969482421875": 2598512,
"99.99847412109375": 2910800,
"99.99923706054688": 3069392
"1": 21881,
"10": 21881,
"25": 23142,
"50": 26770,
"75": 33071,
"90": 33368,
"99": 33368,
"0.001": 21881,
"0.01": 21881,
"0.1": 21881,
"2.5": 21881,
"97.5": 33368,
"99.9": 33368,
"99.99": 33368,
"99.999": 33368
},
"standardError": 159.34183944119272
"standardError": 2158.337556546705
}

@@ -174,0 +201,0 @@ }

import { Callback, Options, Results, Tests } from './models';
export * from './models';
export declare function cronometro(tests: Tests, options: Options | Callback, callback?: Callback): Promise<Results> | undefined;
export declare function cronometro(tests: Tests): Promise<Results> | void;
export declare function cronometro(tests: Tests, options: Partial<Options>): Promise<Results>;
export declare function cronometro(tests: Tests, options: Partial<Options>, cb: Callback): undefined;
export declare function cronometro(tests: Tests, options: Callback): void;

@@ -0,1 +1,2 @@

import { AbstractHistogram } from 'hdr-histogram-js';
export interface PrintOptions {

@@ -8,2 +9,3 @@ colors?: boolean;

iterations: number;
errorThreshold: number;
print: boolean | PrintOptions;

@@ -16,13 +18,5 @@ warmup: boolean;

export declare type Test = StaticTest | AsyncTest | PromiseTest;
export declare type Callback = (err?: Error | null, result?: Results) => any;
export interface Histogram {
record(value: number): boolean;
min(): number;
max(): number;
mean(): number;
stddev(): number;
percentiles(): Array<{
percentile: number;
value: number;
}>;
export declare type Callback = ((err: Error | null) => any) | ((err: null, results: Results) => any);
export interface Percentiles {
[key: string]: number;
}

@@ -32,11 +26,9 @@ export interface Result {

error?: Error;
size?: number;
min?: number;
max?: number;
mean?: number;
stddev?: number;
standardError?: number;
percentiles?: {
[key: string]: number;
};
size: number;
min: number;
max: number;
mean: number;
stddev: number;
standardError: number;
percentiles: Percentiles;
}

@@ -50,16 +42,37 @@ export interface Tests {

export interface Context {
warmup: boolean;
iterations: number;
errorThreshold: number;
print: boolean | PrintOptions;
tests: Array<[string, Test]>;
results: Results;
current: number;
callback: Callback;
queue: Array<[string, Test]>;
results: Results;
}
export interface WorkerContext {
path: string;
tests: Array<[string, Test]>;
index: number;
iterations: number;
warmup: boolean;
errorThreshold: number;
}
export interface TestContext extends Context {
current: {
name: string;
test: Test;
remaining: number;
records: number;
histogram: Histogram;
};
export interface TestContext {
name: string;
test: Test;
errorThreshold: number;
total: number;
executed: number;
histogram: AbstractHistogram;
start: bigint;
handler(error?: Error | null): void;
notifier(value: any): void;
callback(result: Result): void;
}
export declare const defaultOptions: {
iterations: number;
warmup: boolean;
errorThreshold: number;
print: boolean;
};
export declare const percentiles: number[];
import { Results } from './models';
export declare function setLogger(logger: (message: string, ...params: Array<any>) => void): void;
export declare function printResults(results: Results, colors: boolean, compare: boolean, mode: 'base' | 'previous'): void;
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc