Skip to content

jhuckaby/pixl-perf

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

12 Commits
 
 
 
 
 
 
 
 

Repository files navigation

Overview

This module provides an easy way to track high resolution performance metrics in your app. Basically, you wrap your function calls or async operations you want to measure with begin() and end() calls, provide IDs for each metric, and the library provides a summary report whenever you want one. You can also increment arbitrary counters, and include that data in the report as well.

Usage

Use npm to install the module:

npm install pixl-perf

Then use require() to load it in your code:

const Perf = require('pixl-perf');

To use the module, instantiate an object, and start tracking. The first thing you should do is call begin() without any arguments, which starts the overall tracking system. Similar, the last thing you should do is call end() also with no arguments. These allow the system to track a "total time elapsed". Example:

let perf = new Perf();
perf.begin(); // start overall tracking

// do stuff here

perf.end(); // end all tracking

To track individual metrics, simply pass an identifier key to begin() and end(). Here is an example for tracking something synchronous:

let perf = new Perf();
perf.begin(); // start overall tracking

perf.begin('json_parse');
let obj = JSON.parse("{ ...some long JSON document here... }");
perf.end('json_parse');

perf.end(); // end all tracking

You can overlap and nest multiple metrics inside each other. For example, you could have an overall db metric for database operations, but also an inner db_query for the actual DB query time.

let perf = new Perf();
perf.begin(); // start overall tracking

perf.begin('db');
// connect to db here

	perf.begin('db_query');
	// run db query here
	perf.end('db_query');

// disconnect from db here
perf.end('db');

perf.end(); // end all tracking

For tracking asynchronous operations, a little extra care is needed. We can't simply call end() with a plain key, because multiple Node "threads" may be running the same operation at the same time. So in this case, we can use the return value of begin() as a promise. begin() always returns a special unique tracker object, which has its own end() method on it.

let perf = new Perf();
perf.begin(); // start overall tracking

let tracker = perf.begin('something'); // begin measuring 'something'
setTimeout( function() {
	// one second later...
	tracker.end(); // done with something
	
	perf.end(); // end all tracking
	console.log("Perf Metrics: ", perf.metrics());
}, 1000 );

This way, if your app happens to call the same code multiple times simultaneously, each one will have its own unique tracker object, and not clobber each other. When end() is called on the tracker object, it merges the results back into the main instance it was spawned from.

As you can see, the above example also introduces the metrics() method, for fetching a summary of all our measurements. This would output something like the following:

Perf Metrics:  { scale: 1000,
  perf: { total: 1005.205, something: 1004.982 },
  counters: {} }

The metrics() method returns all the performance metrics recorded up to the current point. It will not include metrics still in progress (i.e. those not ended). It consists of the following components:

The scale is the current scale at which time is measured, compared to 1.0 seconds (see Scale and Precision below). In the above example and the default, time is measured in milliseconds.

The perf object contains keys for every measured metric (something for this example) with the values set to the total elapsed time. The library will also add a total key, containing the total overall measured time.

The counters object is a special container for "counters", which can measure any arbitrary number instead of elapsed time. They are explained below in Counters.

Note that all measurements are cumulative, so you can call begin() and end() multiple times with the same keys, and it'll simply add to those metrics. It is up to you how often you want to call metrics() or the related output methods, and possibly reset() to reset the tracker and start over.

See Output Formats for other available output formats, which may be more suitable for your application.

Counters

In addition to tracking elapsed time, you can also have the library track any arbitrary number, called a "counter". These are accessible by the count() method, and accept any number, integer or float, positive or negative. This can do things such as increment line counters for processing a file, or count the number of DB queries or other actions that took place. These counters are included in summary reports, and can be used to calculate averages, or just displayed as is. Example:

let perf = new Perf();
perf.begin(); // start overall tracking

// increment some counters
perf.count('lines');
perf.count('db_queries', 2);
perf.count('something', 0.0001);

perf.end(); // end all tracking
console.log("Perf Metrics: ", perf.metrics());

As you can see, you can call count() without a number argument (which defaults to 1), or pass it any integer or float. The above example would output:

Perf Metrics:  { scale: 1000,
  perf: { total: 0.297 },
  counters: { lines: 1, db_queries: 2, something: 0.0001 } }

Scale and Precision

By default, the library tracks all metrics using milliseconds, and allows up to 3 digits after the decimal point. You can customize both of these things, by calling setScale() which sets the time scale, and/or setPrecision() which controls the precision (the number of digits after the decimal). Examples:

let perf = new Perf();

perf.setScale( 1000000000 ); // nanoseconds
perf.setScale( 1000000 ); // microseconds
perf.setScale( 1000 ); // milliseconds
perf.setScale( 1 ); // seconds

perf.setPrecision( 1 ); // integers only
perf.setPrecision( 10 ); // 1 digit after the decimal
perf.setPrecision( 100 ); // 2 digits after the decimal
perf.setPrecision( 1000 ); // 3 digits after the decimal

So for example, if you wanted to track time in nanoseconds, but only use integers, set the two accordingly:

perf.setScale( 1000000000 ); // nanoseconds
perf.setPrecision( 1 ); // integers only

Or, if you wanted to track time in seconds, but have floating point precision up to 6 digits after the decimal, call:

perf.setScale( 1 ); // seconds
perf.setPrecision( 1000000 ); // 6 digits after the decimal

Another way to think about time scale is that you're basically telling the library how to represent "one second". So, with a value of '1' passed to setScale(), one second will appear as 1.0 (or thereabouts, give or take some decimal points), whereas a value of 1000 will be reported as 1000.0 or the like.

The current time scale is always returned in the response from metrics() and related output methods (useful for analysis and reporting tools).

Output Formats

Previously you've seen the output of the metrics() method, which returns an object containing your performance metrics. The library also provides two other output formats, json() and summarize().

Calling json() will simply return a JSON serialized string of the metrics object. This is merely a convenience, as you could just as easily call JSON.stringify() yourself, but whatever, it's there if you want it. Example:

{"scale":1,"perf":{"total":1.006985,"something":1.006581},"counters":{"lines":1,"db_queries":2,"something":0.0001}}

Calling summarize() will return a flattened summarization of the metrics in psuedo-query-string format. This is useful for some logs or analysis / reporting systems which don't support native JSON. Example:

scale=1&total=1.00728&something=1.006581&c_lines=1&c_db_queries=2&c_something=0.0001

As you can see, all the information is in the summary, but it is represented with ampersand-delimited key/value pairs similar to a URL query string. Counters are always at the end, and prefixed with c_ to avoid key collisions.

The summarize() method also accepts an optional prefix string, which will precede every time-based key. This way you can have them all prefixed like the counters are. Example:

perf.summarize( "t_" );

Output:

scale=1&total=1.00728&t_something=1.006581&t_other=0.02&c_lines=1&c_db_queries=2&c_something=0.0001

Resetting

You can call reset() to reset the performance tracker to start a new session. Don't forget to call begin() again, without a key, to start tracking the total time. It will preserve your scale and precision settings.

Importing

You can import performance metrics from another pixl-perf object by calling the import() method. This will merge in all the metrics and counters from the specified object, adding new keys or appending elapsed time to existing keys. If the two classes have different scale settings, the values are converted as they are imported. Example:

perf.import( other_perf );

Note that this only imports individual named metrics, and not the total time.

You can optionally prefix all the keys of the imported metrics, so they don't collide with your own keys. Example:

perf.import( other_perf, 'other_' );

This would prefix all of the other_perf keys with the string other_.

Importing works with both pixl-perf class instances, as well as generic objects returned from metrics().

Advanced

If you need low-level access to the performance tracking internals, here are a few ways to do that:

First, if you need access to the internal object used to track all the time based metrics (both in progress and completed), call get(). This will return an object with keys for each metric, with the value being a sub-object containing start and possibly end timestamps (these are from process.hrtime()), and also an elapsed key if the metric has been "ended". The elapsed stores the cumulative elapsed time in the selected time scale, but note that the precision has not yet been applied (so it may have more decimals than you want).

Second, if you need access to the raw counters object, call getCounters(). This will return an object with keys for each counter, and the values will simply be the current amount on the counter.

Finally, if you need to simply fetch the current elapsed time for one single metric, call elapsed() and pass in the key of the metric you wish to retrieve. Pass true as the second argument to massage the value into the current precision setting (i.e. trim excess decimals if required). Note that the metric must be "ended" for this to work.

Total Key

To change the name of the total key, set the totalKey property to whatever string you want. It defaults to total. Example:

let perf = new Perf();
perf.totalKey = 't';

Special MinMax Mode

This optional mode allows you to accumulate data about multiple performance measurements over time, and then fetch a summary. Basically, when enabled, it tracks the minimum, average, maximum, total and counts for each named metric. All this accumulated data is then made available via a summary API.

This mode is useful when your application measures the same metric multiple times. Then, instead of logging performance for each measurement, you can log a summary of all the min/avg/max metrics over a larger time period (last second, last minute, etc.).

To enable the feature, set the minMax property to true, then begin taking multiple measurements. Example use:

let perf = new Perf();
perf.minMax = true;

for (let idx = 0; idx < 100; idx++) {
	perf.begin('load_data');
		// do something loady
	perf.end('load_data');

	perf.begin('save_data');
		// do something savey
	perf.end('save_data');
}

When you are done collecting measurements, call the getMinMaxMetrics() method to fetch a summary of all the minimums, averages, maximums, totals and counts for all your named metrics. Example:

let metrics = perf.getMinMaxMetrics();

This returns an object which contains a property for each of your metrics, and the values are sub-objects containing min, max, total, count and avg properties. Example:

{
	"load_data": {
		"min": 0.132,
		"max": 8.828,
		"total": 319.99,
		"count": 100,
		"avg": 1.285
	},
	"save_data": {
		"min": 0.784,
		"max": 7.367,
		"total": 198.952,
		"count": 100,
		"avg": 1.591
	}
}

The values are all converted to the correct scale and precision for your pixl-perf object.

Note that the total metric is ignored in this mode. This is as designed, because total is only ever measured once, and this mode is more for collecting multiple measurements and fetching metrics for them over time.

However, what you can do is use two types of pixl-perf objects, one to collect basic metrics (including a total) and another to accumulate them with minMax. You can import metrics from one into the other using the import() method. Example:

let a = new Perf();
a.begin();
	// do something
a.end();

let b = new Perf();
b.begin();
	// do something
b.end();

let perf = new Perf();
perf.minMax = true;
perf.totalKey = 'unused';

// import from a and b
perf.import( a );
perf.import( b );

let metrics = perf.getMinMaxMetrics();

This would accumulate the metrics from a and b into the perf object and include their totals. The trick here is setting the totalKey to "unused" (really any string other than the default total). This allows import() to import the totals from a and b, which it normally wouldn't do.

License

The MIT License

Copyright (c) 2015 - 2018 Joseph Huckaby.

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

About

A simple, high precision performance tracking system for Node.JS apps.

Resources

Stars

Watchers

Forks

Packages

No packages published