New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

@contrast/perf

Package Overview
Dependencies
Maintainers
0
Versions
6
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@contrast/perf - npm Package Compare versions

Comparing version 1.2.0 to 1.2.1

lib/writer.js

211

lib/index.js

@@ -23,2 +23,12 @@ /*

// each instance map has metadata associated with the map itself.
// this is the stats type
const kStatsType = Symbol('stats-type');
const kSimple = 'simple';
const kStreaming = 'streaming';
// this is how many times the map has been updated
const kUpdateCount = Symbol('update-count');
const kReportedCount = Symbol('reported-count');
class Perf {

@@ -31,9 +41,15 @@ constructor(prefix, options = {}) {

if (options.streaming) {
this.timings[kStatsType] = kStreaming;
this.record = this.recordWithStats;
this.getStats = (precision) => Perf.getStreamingStats(this.timings, precision);
} else {
this.timings[kStatsType] = kSimple;
this.record = this.recordSimple;
this.getStats = (precision) => Perf.getStats(this.timings, precision);
}
this.timings[kUpdateCount] = 0n;
this.timings[kReportedCount] = 0n;
this.writer = null;
// for testing. i don't like this, but it's the only way to test the

@@ -52,7 +68,30 @@ // record functions and resulting data.

// findings across all instances of Perf are kept here. this seems better
// than keeping them in a global.
// interval setting
static interval_ms = +process.env.CSI_PERF_INTERVAL || 60_000;
// findings across all instances of Perf are kept here. each instance has it's own
// map of timings stored here.
//
// this seems better than keeping them in a global.
static all = new Map();
// place for intermediate marks.
static marks = new Map();
// our interval timer
static interval = null;
static intervalCounter = 0;
static symbols = {
kStatsType,
kSimple,
kStreaming,
kUpdateCount,
kReportedCount,
};
// global counters
static requestCount = 0;
/**

@@ -86,7 +125,88 @@ * This exists primarily for testing. It collects the timings from an instance

/**
* This is used to collect the stats from all instances and replace them with
* empty maps. It is invoked on the 'listening' event to separate startup
* stats from runtime stats.
*
* N.B. For a given markPrefix, this will only work once for streaming stats.
* If it is called multiple times with the same markPrefix, the marked
* streaming stats will not be updated because they cannot be aggregated with
* the new streaming stats.
*
* So, If the server only listens once, this works fine with both streaming and
* non-streaming stats, but if the server calls listen multiple times, only
* non-streaming stats will be updated for the markPrefix.
*/
static mark(markPrefix) {
let markEntries = Perf.marks.get(markPrefix);
if (!markEntries) {
markEntries = new Map();
Perf.marks.set(markPrefix, markEntries);
}
// there is a mark. the difference is that streaming instances are only
// captured the first time a mark is set. if the mark is set again, the
// streaming stats are reset, but the already captured mark is not
// overwritten.
//
// why is this? the thinking is that the window between multiple
// listening events is short enough that it's not worth the complexity
// to capture multiple versions of a mark. if it is, we can start
// capturing versions. yuck.
// iterate through the existing "all" entries and merge them into the mark
// if they are not streaming.
for (const [prefix, map] of Perf.all.entries()) {
if (map.size === 0) {
continue;
}
// if this "all" entry doesn't exist in the mark, copy it.
if (!markEntries.has(prefix)) {
const copiedMap = new Map(map);
copiedMap[kStatsType] = map[kStatsType];
copiedMap[kUpdateCount] = map[kUpdateCount];
copiedMap[kReportedCount] = map[kReportedCount];
markEntries.set(prefix, copiedMap);
map.clear();
continue;
}
// this "all" entry exists in the mark. don't merge streaming stats, but
// merge simple stats. clear both. the point of clearing is to remove
// the impact of startup code from request-handling.
if (map[kStatsType] === kSimple) {
for (const [k, v] of map.entries()) {
let existing = markEntries.get(prefix).get(k);
// it's possible that a new stat appeared.
if (!existing) {
existing = new Map([[k, [0, 0n]]]);
markEntries.set(prefix, existing);
}
existing[0] += v[0];
existing[1] += v[1];
}
} else if (map[kStatsType] === kStreaming) {
// should data for the second mark replace the first?
continue;
// they cannot be merged, so just copy the map.
// const copiedMap = new Map(map);
// copiedMap[kStatsType] = map[kStatsType];
// copiedMap[kUpdateCount] = map[kUpdateCount];
// copiedMap[kReportedCount] = map[kReportedCount];
// markEntries.set(prefix, copiedMap);
} else {
throw new Error(`invalid stats type: ${map[kStatsType]}`);
}
map.clear();
}
}
/**
* This is a bit of a misnomer; it converts nanoseconds to microseconds and
* calculates the mean. It returns a new map with the same keys and an object
* with n, totalMicros, and mean. There are not individual observations so
* standard deviation is not calculated. See `record()` - it's possible to
* calculate mean and stddev on the fly if it's important.
* standard deviation is not calculated.
*

@@ -110,2 +230,14 @@ * @param {Map} map - the map to calculate stats for

/**
* This is also a bit of a misnomer; it just formats the streaming stats
* and returns them in a new map. They've already been calculated by the
* RunningStats class.
*
* The logic is all part of the RunningStats class.
*
* @param {Map} map - the map to calculate stats for
* @param {number=2} precision - the number of decimal places to round to
* @returns {Map} - a new map with the same keys and the object returned by
* getStats().
*/
static getStreamingStats(map, precision = 2) {

@@ -121,3 +253,58 @@ const stats = new Map();

static getStatsType(map) {
return map[kStatsType];
}
/**
* Sets up the interval timer to log perf data to a file.
*/
static setInterval(options = {}) {
if (!Perf.isEnabled) {
return;
}
const {
name = 'agent-perf.jsonl',
interval = Perf.interval_ms || 60_000,
precision = 2,
} = options;
if (!this.writer) {
const Writer = require('./writer');
this.writer = new Writer(name);
}
Perf.interval = setInterval(() => {
Perf.intervalCounter += 1;
// if mark entries are present, log them for the first 5 intervals.
if (Perf.intervalCounter < 5 && Perf.marks.size !== 0) {
// log stats from the "listening" mark (only type of mark now).
for (const [markPrefix, markEntries] of Perf.marks.entries()) {
this.writer.writeAllMap(markEntries, markPrefix, precision);
}
// if requests have started, clear the marks.
if (Perf.requestCount > 0) {
Perf.marks.clear();
}
}
// nothing to log. it's possible to miss some timer-driven activity, so
// log every 5 intervals anyway.
if (Perf.intervalCounter % 5 !== 0) {
return;
}
// writeAllMap() is async, but we can't await because we're in setInterval
// callback. the key here is that the interval need to be long enough
// that any async operations will complete before the next interval.
// given that the interval is 60 seconds (and can't be user configured),
// this should be fine.
this.writer.writeAllMap(Perf.all, 'not-mark', precision);
}, interval);
this.interval.unref();
}
/**
* This wraps init functions and the install function if present on the

@@ -219,3 +406,3 @@ * result init() returns. It is designed to be used be agentify.

return pino;
};
}

@@ -299,2 +486,4 @@ /**

this.timings[kUpdateCount] += 1n;
return deltaT;

@@ -319,2 +508,4 @@ }

this.timings[kUpdateCount] += 1n;
return deltaT;

@@ -328,2 +519,5 @@ }

*
* Not currently used because the "excluded" time can be derived from two raw
* times.
*
* @param {string} tag - the tag to record

@@ -335,3 +529,3 @@ * @param {bigint} start - the start time

recordExcluding(tag, start, excludeT) {
let deltaT = this.hrtime.bigint() - start - excludeT;
const deltaT = this.hrtime.bigint() - start - excludeT;
let existing = this.timings.get(tag);

@@ -354,2 +548,6 @@ if (!existing) {

* https://nestedsoftware.com/2018/03/27/calculating-standard-deviation-on-streaming-data-253l.23919.html
*
* Note: RunningStats keeps values in microseconds so that overflow is less
* likely. It is different than the way simpleRecord() works, which only
* converts to microseconds when stats are requested.
*/

@@ -380,2 +578,3 @@ class RunningStats {

newValue = Number(newValue);
const meanDifferential = (newValue - this.mean) / this.n;

@@ -382,0 +581,0 @@

@@ -7,3 +7,2 @@ 'use strict';

describe('perf', function () {

@@ -232,3 +231,3 @@ describe('Perf class', function () {

afterEach(function() {
console.log(Perf.all);
//console.log(Perf.all);
});

@@ -312,2 +311,130 @@

});
describe('test prototype', function() {
const instances = [];
const values = {
'instance-0': [10, 20, 30],
'instance-1': [100, 200, 300],
'instance-2': [1000, 2000, 3000],
'streaming': [10000, 20000, 30000],
};
let firstMark;
before(async function() {
Perf.all.clear();
const prefixes = Object.keys(values);
for (let i = 0; i < 4; i++) {
const prefix = prefixes[i];
const options = {
streaming: prefix === 'streaming',
hrtime: () => BigInt(values[prefix].shift())
};
instances.push(new Perf(prefix, options));
for (let j = 0; j < 3; j++) {
instances[i].record('test', 0n);
}
}
});
it('dumps some stuff', function() {
// make a bunch of perf instances
const i0 = instances[0].timings;
const i1 = instances[1].timings;
const i2 = instances[2].timings;
const i3 = instances[3].timings;
expect(i0).deep.equal(new Map([['test', [3, 60n]]]));
expect(i1).deep.equal(new Map([['test', [3, 600n]]]));
expect(i2).deep.equal(new Map([['test', [3, 6000n]]]));
// maybe Perf should expose RunningStats?
expect(i3).instanceOf(Map);
expect(i3.size).equal(1);
const i3stats = i3.get('test');
expect(i3stats).property('n', 3);
expect(i3stats).property('totalMicros', 60n); // it's been converted to microseconds.
Perf.mark('listening');
expect(Perf.marks.get('listening')).instanceOf(Map);
const mark0 = Perf.marks.get('listening').get('instance-0');
const mark1 = Perf.marks.get('listening').get('instance-1');
const mark2 = Perf.marks.get('listening').get('instance-2');
const mark3 = Perf.marks.get('listening').get('streaming');
expect(mark0).deep.equal(new Map([['test', [3, 60n]]]));
expect(mark1).deep.equal(new Map([['test', [3, 600n]]]));
expect(mark2).deep.equal(new Map([['test', [3, 6000n]]]));
const s3 = mark3.get('test');
expect(s3).property('n', 3);
expect(s3).property('totalMicros', 60n);
/* eslint-disable */
expect(i0).instanceOf(Map).property('size', 0);
expect(i1).instanceOf(Map).property('size', 0);
expect(i2).instanceOf(Map).property('size', 0);
expect(i3).instanceOf(Map).property('size', 0);
/* eslint-enable */
});
it('gets the same answer for the streaming stats', async function() {
// duplicate the existing listening stats
firstMark = new Map(Perf.marks.get('listening'));
const firstMarkMaps = {};
for (const [prefix, data] of firstMark) {
firstMarkMaps[prefix] = new Map(data);
}
// add two more perf measurements
const values = {
'instance-0': [40, 50],
'instance-1': [400, 500],
'instance-2': [4000, 5000],
'streaming': [40000, 50000],
};
const prefixes = Object.keys(values);
for (let i = 0; i < 4; i++) {
const prefix = prefixes[i];
instances[i].hrtime = { bigint: () => BigInt(values[prefix].shift()) };
for (let j = 0; j < 2; j++) {
instances[i].record('test', 0n);
}
}
expect(Perf.marks.get('listening')).instanceOf(Map);
const all0 = Perf.all.get('instance-0');
const all1 = Perf.all.get('instance-1');
const all2 = Perf.all.get('instance-2');
const all3 = Perf.all.get('streaming');
expect(all0).deep.equal(new Map([['test', [2, 90n]]]));
expect(all1).deep.equal(new Map([['test', [2, 900n]]]));
expect(all2).deep.equal(new Map([['test', [2, 9000n]]]));
let s3 = all3.get('test');
expect(s3).property('n', 2);
expect(s3).property('totalMicros', 90n);
// ask for another mark of the same prefix. it should merge non-streaming
// and *not* replace streaming stats.
Perf.mark('listening');
/* eslint-disable */
expect(Perf.marks.get('listening')).instanceOf(Map);
const mark0 = Perf.marks.get('listening').get('instance-0');
const mark1 = Perf.marks.get('listening').get('instance-1');
const mark2 = Perf.marks.get('listening').get('instance-2');
const mark3 = Perf.marks.get('listening').get('streaming');
/* eslint-enable */
// simple stats are aggregated
expect(mark0).deep.equal(new Map([['test', [5, 150n]]]));
expect(mark1).deep.equal(new Map([['test', [5, 1500n]]]));
expect(mark2).deep.equal(new Map([['test', [5, 15000n]]]));
// streaming stats don't change
s3 = mark3.get('test');
expect(s3).property('n', 3);
expect(s3).property('totalMicros', 60n);
});
});
});

12

lib/tsconfig.json
{
"extends": "@tsconfig/node16/tsconfig.json",
"compilerOptions": {

@@ -15,7 +16,6 @@ // Tells TypeScript to read JS files, as

// next to the .js files
//"outDir": "dist",
"outDir": "../types",
// go to js file when using IDE functions like
// "Go to Definition" in VSCode
"declarationMap": true,
"outDir": "../types",
"declarationMap": true
},

@@ -27,7 +27,3 @@ "references": [

],
"exclude": [
"../types",
"**/*.spec.*",
"**/*.test.*"
]
"exclude": ["../types", "**/*.spec.*", "**/*.test.*"]
}
{
"name": "@contrast/perf",
"version": "1.2.0",
"version": "1.2.1",
"description": "Performance measurement",

@@ -21,4 +21,4 @@ "license": "SEE LICENSE IN LICENSE",

"dependencies": {
"@contrast/logger": "1.12.0"
"sonic-boom": "^4.1.0"
}
}
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