Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Updated stats.js to delete counters #183

Merged
merged 8 commits into from

3 participants

@patrickmccoy

Counters are now deleted after being used, not set to zero. This solves
some issues with high load on Graphite and certain graphing functions
which require null instead of 0 counters.

@mrtazz this is a patch for a conversation you had with @holybit a couple days ago on IRC. Please let me know if you want it done any differently, but the patch pretty much mirrors the change in the following gist: https://gist.github.com/3625157

@patrickmccoy patrickmccoy Updated stats.js to delete counters
Counters are now deleted after being used, not set to zero.  This solves
some issues with high load on Graphite and certain graphing functions
which require null instead of 0 counters.
01da23d
@mrtazz
Owner

Yeah the change looks ok, but apparently it breaks the build. Can you update the appropriate tests?

patrickmccoy added some commits
@patrickmccoy patrickmccoy Update stats.js
Changed from delete to undefined, per some info on delete from Mozilla https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Operators/delete

This keeps the array element there, just sets it's value to undefined.
e3f7924
@patrickmccoy patrickmccoy Update test/graphite_tests.js
Updating the test to work with new null count change.
71df1bd
@patrickmccoy patrickmccoy Update stats.js
Added a config option delete_counters with a default: false to control the behavior of the counters metrics to delete.
777ea8c
@mrtazz
Owner

The tests are still failing on this one. Likely because we need a test with the option disabled and one with the option enabled. And then test for the different outcomes.

@jabbaugh

Any thoughts on when this might be merged?

stats.js
((5 lines not shown))
for (key in metrics.counters) {
- metrics.counters[key] = 0;
+ if (conf.deleteCounters) {
+ metrics.counters[key] = undefined;
@mrtazz Owner
mrtazz added a note

I think we should also change this to be delete(counters[key]); as it is in the gist. I don't really see the benefit of storing it as undefined and graphite will record None anyways it it doesn't receive a value afaik. So this is more or less just wasting memory. Or am I missing something here?

I think it is pretty much the same either way, but delete probably uses less memory. In the example below, both elements are undefined in the end, but setting a[2] = undefined stores undefined in the array, whereas delete doesn't, the javascript engine returns undefined when trying to access an element which has been deleted.

> a = ['a', 'b', 'c'];
[ 'a', 'b', 'c' ]
> delete(a[1])
true
> a
[ 'a', , 'c' ]
> a[1]
undefined
> a[2] = undefined
undefined
> a
[ 'a', , undefined ]
> a[2]
undefined

I can change this line to use delete if you think the memory savings will be significant.

@mrtazz Owner
mrtazz added a note

Depends on how you define "pretty much the same". Using delete we use less memory and don't have to loop over keys for which we aren't sending values anyways. So we are also saving some amount of CPU cycles. In the = undefined variant we just set the value to be the same as if the key didn't exist but have to store it and loop over the key. So I'd say let's go with the delete version.

Sounds good, I'll get make the change and add it to the pull by tomorrow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
patrickmccoy added some commits
@patrickmccoy patrickmccoy Changed stats.js to use delete
Changed stats.js to use delete instead of setting the values to
undefined in order to save some memory.
c03f339
@patrickmccoy patrickmccoy Fixed the tests
Fixed the tests that were broken when changing to delete
d0901b8
@patrickmccoy

@mrtazz I have updated the code per our conversation last night to use delete() instead of setting the counters to undefined.

@mrtazz mrtazz merged commit d0901b8 into etsy:master
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Oct 25, 2012
  1. @patrickmccoy

    Updated stats.js to delete counters

    patrickmccoy authored
    Counters are now deleted after being used, not set to zero.  This solves
    some issues with high load on Graphite and certain graphing functions
    which require null instead of 0 counters.
Commits on Oct 30, 2012
  1. @patrickmccoy

    Update stats.js

    patrickmccoy authored
    Changed from delete to undefined, per some info on delete from Mozilla https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Operators/delete
    
    This keeps the array element there, just sets it's value to undefined.
Commits on Oct 31, 2012
  1. @patrickmccoy

    Update test/graphite_tests.js

    patrickmccoy authored
    Updating the test to work with new null count change.
Commits on Nov 1, 2012
  1. @patrickmccoy

    Update stats.js

    patrickmccoy authored
    Added a config option delete_counters with a default: false to control the behavior of the counters metrics to delete.
Commits on Nov 13, 2012
  1. @patrickmccoy

    Tests for Delete Counters Config

    patrickmccoy authored
    Added tests for the delete counters config change.
Commits on Nov 14, 2012
  1. @patrickmccoy
Commits on Nov 26, 2012
  1. @patrickmccoy

    Changed stats.js to use delete

    patrickmccoy authored
    Changed stats.js to use delete instead of setting the values to
    undefined in order to save some memory.
  2. @patrickmccoy

    Fixed the tests

    patrickmccoy authored
    Fixed the tests that were broken when changing to delete
This page is out of date. Refresh to see the latest.
View
1  exampleConfig.js
@@ -34,6 +34,7 @@ Optional Variables:
interval: how often to log frequent keys [ms, default: 0]
percent: percentage of frequent keys to log [%, default: 100]
log: location of log file for frequent keys [default: STDOUT]
+ deleteCounters: when flushing to graphite, send null instead of 0 [default: false]
console:
prettyprint: whether to prettyprint the console backend
View
11 stats.js
@@ -36,6 +36,9 @@ function loadBackend(config, name) {
}
};
+// global for conf
+var conf;
+
// Flush metrics to each backend.
function flushMetrics() {
var time_stamp = Math.round(new Date().getTime() / 1000);
@@ -51,8 +54,13 @@ function flushMetrics() {
// After all listeners, reset the stats
backendEvents.once('flush', function clear_metrics(ts, metrics) {
// Clear the counters
+ conf.deleteCounters = conf.deleteCounters || false;
for (key in metrics.counters) {
- metrics.counters[key] = 0;
+ if (conf.deleteCounters) {
+ delete(metrics.counters[key]);
+ } else {
+ metrics.counters[key] = 0;
+ }
}
// Clear the timers
@@ -81,6 +89,7 @@ var stats = {
var l;
config.configFile(process.argv[2], function (config, oldConfig) {
+ conf = config;
if (! config.debug && debugInt) {
clearInterval(debugInt);
debugInt = false;
View
263 test/graphite_delete_counters_tests.js
@@ -0,0 +1,263 @@
+var fs = require('fs'),
+ net = require('net'),
+ temp = require('temp'),
+ spawn = require('child_process').spawn,
+ util = require('util'),
+ urlparse = require('url').parse,
+ _ = require('underscore'),
+ dgram = require('dgram'),
+ qsparse = require('querystring').parse,
+ http = require('http');
+
+
+var writeconfig = function(text,worker,cb,obj){
+ temp.open({suffix: '-statsdconf.js'}, function(err, info) {
+ if (err) throw err;
+ fs.writeSync(info.fd, text);
+ fs.close(info.fd, function(err) {
+ if (err) throw err;
+ worker(info.path,cb,obj);
+ });
+ });
+}
+
+var array_contents_are_equal = function(first,second){
+ var intlen = _.intersection(first,second).length;
+ var unlen = _.union(first,second).length;
+ return (intlen == unlen) && (intlen == first.length);
+}
+
+var statsd_send = function(data,sock,host,port,cb){
+ send_data = new Buffer(data);
+ sock.send(send_data,0,send_data.length,port,host,function(err,bytes){
+ if (err) {
+ throw err;
+ }
+ cb();
+ });
+}
+
+// keep collecting data until a specified timeout period has elapsed
+// this will let us capture all data chunks so we don't miss one
+var collect_for = function(server,timeout,cb){
+ var received = [];
+ var in_flight = 0;
+ var timed_out = false;
+ var collector = function(req,res){
+ in_flight += 1;
+ var body = '';
+ req.on('data',function(data){ body += data; });
+ req.on('end',function(){
+ received = received.concat(body.split("\n"));
+ in_flight -= 1;
+ if((in_flight < 1) && timed_out){
+ server.removeListener('request',collector);
+ cb(received);
+ }
+ });
+ }
+
+ setTimeout(function (){
+ timed_out = true;
+ if((in_flight < 1)) {
+ server.removeListener('connection',collector);
+ cb(received);
+ }
+ },timeout);
+
+ server.on('connection',collector);
+}
+
+module.exports = {
+ setUp: function (callback) {
+ this.testport = 31337;
+ this.myflush = 200;
+ var configfile = "{graphService: \"graphite\"\n\
+ , batch: 200 \n\
+ , flushInterval: " + this.myflush + " \n\
+ , percentThreshold: 90\n\
+ , port: 8125\n\
+ , dumpMessages: false \n\
+ , debug: false\n\
+ , deleteCounters: true\n\
+ , graphitePort: " + this.testport + "\n\
+ , graphiteHost: \"127.0.0.1\"}";
+
+ this.acceptor = net.createServer();
+ this.acceptor.listen(this.testport);
+ this.sock = dgram.createSocket('udp4');
+
+ this.server_up = true;
+ this.ok_to_die = false;
+ this.exit_callback_callback = process.exit;
+
+ writeconfig(configfile,function(path,cb,obj){
+ obj.path = path;
+ obj.server = spawn('node',['stats.js', path]);
+ obj.exit_callback = function (code) {
+ obj.server_up = false;
+ if(!obj.ok_to_die){
+ console.log('node server unexpectedly quit with code: ' + code);
+ process.exit(1);
+ }
+ else {
+ obj.exit_callback_callback();
+ }
+ };
+ obj.server.on('exit', obj.exit_callback);
+ obj.server.stderr.on('data', function (data) {
+ console.log('stderr: ' + data.toString().replace(/\n$/,''));
+ });
+ /*
+ obj.server.stdout.on('data', function (data) {
+ console.log('stdout: ' + data.toString().replace(/\n$/,''));
+ });
+ */
+ obj.server.stdout.on('data', function (data) {
+ // wait until server is up before we finish setUp
+ if (data.toString().match(/server is up/)) {
+ cb();
+ }
+ });
+
+ },callback,this);
+ },
+ tearDown: function (callback) {
+ this.sock.close();
+ this.acceptor.close();
+ this.ok_to_die = true;
+ if(this.server_up){
+ this.exit_callback_callback = callback;
+ this.server.kill();
+ } else {
+ callback();
+ }
+ },
+
+ send_well_formed_posts: function (test) {
+ test.expect(2);
+
+ // we should integrate a timeout into this
+ this.acceptor.once('connection',function(c){
+ var body = '';
+ c.on('data',function(d){ body += d; });
+ c.on('end',function(){
+ var rows = body.split("\n");
+ var entries = _.map(rows, function(x) {
+ var chunks = x.split(' ');
+ var data = {};
+ data[chunks[0]] = chunks[1];
+ return data;
+ });
+ test.ok(_.include(_.map(entries,function(x) { return _.keys(x)[0] }),'statsd.numStats'),'graphite output includes numStats');
+ test.equal(_.find(entries, function(x) { return _.keys(x)[0] == 'statsd.numStats' })['statsd.numStats'],2);
+ test.done();
+ });
+ });
+ },
+
+ send_malformed_post: function (test) {
+ test.expect(3);
+
+ var testvalue = 1;
+ var me = this;
+ this.acceptor.once('connection',function(c){
+ statsd_send('a_bad_test_value|z',me.sock,'127.0.0.1',8125,function(){
+ collect_for(me.acceptor,me.myflush*2,function(strings){
+ test.ok(strings.length > 0,'should receive some data');
+ var hashes = _.map(strings, function(x) {
+ var chunks = x.split(' ');
+ var data = {};
+ data[chunks[0]] = chunks[1];
+ return data;
+ });
+ var numstat_test = function(post){
+ var mykey = 'statsd.numStats';
+ return _.include(_.keys(post),mykey) && (post[mykey] == 2);
+ };
+ test.ok(_.any(hashes,numstat_test), 'statsd.numStats should be 0');
+
+ var bad_lines_seen_value_test = function(post){
+ var mykey = 'stats_counts.statsd.bad_lines_seen';
+ return _.include(_.keys(post),mykey) && isNaN(post[mykey]);
+ };
+ test.ok(_.any(hashes,bad_lines_seen_value_test), 'stats_counts.statsd.bad_lines_seen should be ' + testvalue);
+
+ test.done();
+ });
+ });
+ });
+ },
+
+ timers_are_valid: function (test) {
+ test.expect(3);
+
+ var testvalue = 100;
+ var me = this;
+ this.acceptor.once('connection',function(c){
+ statsd_send('a_test_value:' + testvalue + '|ms',me.sock,'127.0.0.1',8125,function(){
+ collect_for(me.acceptor,me.myflush*2,function(strings){
+ test.ok(strings.length > 0,'should receive some data');
+ var hashes = _.map(strings, function(x) {
+ var chunks = x.split(' ');
+ var data = {};
+ data[chunks[0]] = chunks[1];
+ return data;
+ });
+ var numstat_test = function(post){
+ var mykey = 'statsd.numStats';
+ return _.include(_.keys(post),mykey) && (post[mykey] == 2);
+ };
+ test.ok(_.any(hashes,numstat_test), 'statsd.numStats should be 1');
+
+ var testtimervalue_test = function(post){
+ var mykey = 'stats.timers.a_test_value.mean_90';
+ return _.include(_.keys(post),mykey) && (post[mykey] == testvalue);
+ };
+ test.ok(_.any(hashes,testtimervalue_test), 'stats.timers.a_test_value.mean should be ' + testvalue);
+
+ test.done();
+ });
+ });
+ });
+ },
+
+ counts_are_valid: function (test) {
+ test.expect(4);
+
+ var testvalue = 100;
+ var me = this;
+ this.acceptor.once('connection',function(c){
+ statsd_send('a_test_value:' + testvalue + '|c',me.sock,'127.0.0.1',8125,function(){
+ collect_for(me.acceptor,me.myflush*2,function(strings){
+ test.ok(strings.length > 0,'should receive some data');
+ var hashes = _.map(strings, function(x) {
+ var chunks = x.split(' ');
+ var data = {};
+ data[chunks[0]] = chunks[1];
+ return data;
+ });
+ var numstat_test = function(post){
+ var mykey = 'statsd.numStats';
+ return _.include(_.keys(post),mykey) && (post[mykey] == 2);
+ };
+ test.ok(_.any(hashes,numstat_test), 'statsd.numStats should be 1');
+
+ var testavgvalue_test = function(post){
+ var mykey = 'stats.a_test_value';
+ return _.include(_.keys(post),mykey) && (post[mykey] == (testvalue/(me.myflush / 1000)));
+ };
+ test.ok(_.any(hashes,testavgvalue_test), 'stats.a_test_value should be ' + (testvalue/(me.myflush / 1000)));
+
+ var testcountvalue_test = function(post){
+ var mykey = 'stats_counts.a_test_value';
+ return _.include(_.keys(post),mykey) && (post[mykey] == testvalue);
+ };
+ test.ok(_.any(hashes,testcountvalue_test), 'stats_counts.a_test_value should be ' + testvalue);
+
+ test.done();
+ });
+ });
+ });
+ }
+}
Something went wrong with that request. Please try again.