Skip to content
Snippets Groups Projects
load_gen 11.4 KiB
Newer Older
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/* This file is the main run file for the browserid load generation
 * tool, which is capable of analysing the maximum active users that
 * a browserid deployment can support */

const winston = require('winston');

// option processing with optimist
var argv = require('optimist')
.usage('Apply load to a BrowserID server.\nUsage: $0', [ "foo" ])
.alias('a', 'activities')
.describe('a', 'only run a subset of activities, specified as a CSV list')
.alias('h', 'help')
.describe('h', 'display this usage message')
.alias('l', 'list')
.describe('l', 'list available activities and exit')
.alias('m', 'max')
.describe('m', 'maximum active users to simulate (0 == infinite)')
.alias('o', 'omit-static')
.describe('o', 'when enabled, only dynamic WSAPI calls will be simulated, not static resource requests')
.default('o', false)
.describe('s', 'base URL to browserid server')
  return (argv.h || typeof argv.s === 'string' || argv.l) != undefined;
.describe('v', 'base URL to verifier service (default is browserid server + \'/verify\')')
.alias('u', 'user-range')
.describe('u', 'rather than creating users, assume a range of users exist #@loadtest.domain (with password "THE PASSWORD")');
// global configuration
const configuration = {
  verifier: args.v ? args.v : args.s + "/verify",
  browserid: args.s
};

// last time we updated stats and added work if necc.
var lastPoll = new Date();

// average active users simulated over the last second, 5s, and 60s
var averages = [
  0.0,
  0.0,
  0.0
];

// activities complete since the last poll
var completed = {
};

// how many activies does an active user undertake per second
const activitiesPerUserPerSecond = (40.0 / ( 24 * 60 * 60 ));
  "signup": {
    // a %20 montly growth rate means there's a 20% probability of
    // the monthly activity generated by an active user being a
    // new user signup
    probability: (1.0 / (40 * 28 * .2))
  },
    // users forget their password once every 4 weeks
    probability: (1.0 / (40 * 28.0))
  },
  "add_email": {
    // users add a new email address once every 2 weeks
    probability: (1.0 / (40 * 14.0))
  },
  "reauth": {
    // users must re-authenticate to browser id once a week
    // (once every two weeks per device)
    probability: (1.0 / (40 * 7.0))
  },
  "signin": {
    // users sign in using existing authentication material
    // 8 times a day (once every six hours per device)
    probability: (8 / 40.0)
  },
  "include_only": {
    // most of the time, users are already authenticated to their
    // RPs, so the hit on our servers is simply resource (include.js)
    // inclusion.  The strict probability is 100% - sum of above
    // probabilities.  We round to 31 / 40.
    probability: (31 / 40.0)
  },
  "change_pass": {
    // users change their passwords once every two months
    probability: (1.0 / (40 * 56))
if (args.l) {
  console.log("available activities:", Object.keys(activity).join(", "));
  process.exit(0);
}

if (args.h) {
  argv.showHelp();
  process.exit(1);
}

var activitiesToRun = Object.keys(activity);

// handle modification of activities to run (-o or -a)
if (args.a) {
  if (typeof args.a !== 'string') {
    process.stdout.write("invalid argument: " + args.a.toString() + "\n\n");
    argv.showHelp();
    process.exit(1);
  }
  activitiesToRun = args.a.split(',');
  activitiesToRun.forEach(function(act) {
    if (!activity.hasOwnProperty(act)) {
      process.stdout.write("invalid activity: " + act + "\n\n");
      process.exit(1);
    }
  });
} else if (args.o) {
  activitiesToRun.splice(activitiesToRun.indexOf('include_only'), 1);
}

// outstanding incomplete activites
var outstanding = { };

Object.keys(activity).forEach(function(act) {
  outstanding[act] = 0;
});

function numOutstanding() {
  var n = 0;
  Object.keys(outstanding).forEach(function(act) {
    n += outstanding[act];
  });
  return n;
}

// now attach "start functions" to the activity map by including
// the implementation of each activity
Object.keys(activity).forEach(function(k) {
  activity[k].startFunc = require("../lib/load_gen/activities/" + k).startFunc;
});

// probs is a 2d array mapping normalized probabilities from 0-1 to
// activities, used when determining what activity to perform next
var probs = [];
Object.keys(activity).forEach(function(k) {
  var sum = 0;
  if (probs.length) sum = probs[probs.length - 1][0];
  sum += activity[k].probability;
  probs.push([sum, k]);
});

// and normalize probs into 0..1
(function() {
  var max = probs[probs.length - 1][0];
  for (var i = 0; i < probs.length; i++) {
    probs[i][0] /= max;
  }
})();

// a global count of how many poll iterations have been completed
var iterations = 0;

// output a textual summary of how many activites per second are
// associated with the given number of active users
function outputActiveUserSummary(activeUsers) {
  console.log("with", activeUsers, "active users there will be:");
  for (var i = 0; i < probs.length; i++) {
    var p = probs[i][0];
    if (i !== 0) p -= probs[i-1][0];
    var n = p * activeUsers * activitiesPerUserPerSecond;
    console.log(" ", n.toFixed(2), probs[i][1], "activites per second");
  }
}

function poll() {
  function startNewActivity() {
    // what type of activity is this?
    var n = Math.random();
    var act = undefined;
    for (var i = 0; i < probs.length; i++) {
      if (n <= probs[i][0]) {
        act = probs[i][1];
        break;
      }
    }
    // start the activity! (if it is enabled)
    if (activitiesToRun.indexOf(act) !== -1) {
      activity[act].startFunc(configuration, function(err) {
        if (undefined === completed[act]) completed[act] = [ 0, 0, 0 ];
        if (err) {
          if (typeof err != 'string') err = err.toString();
          if (err.indexOf('server is too busy') != -1) {
            completed[act][2]++;
          } else {
            completed[act][1]++;
      if (undefined === completed[act]) completed[act] = [ 0, 0, 0 ];
  function updateAverages(elapsed) {
    if (!iterations) return;

    var numActCompleted = 0;
    Object.keys(completed).forEach(function(k) {
      numActCompleted += completed[k][0];
      numErrors += completed[k][1];
    completed = { };
    var avgUsersThisPeriod = (numActCompleted / activitiesPerUserPerSecond) * (elapsed / 1000);

    // the 1s average is a goldfish.
    averages[0] = avgUsersThisPeriod;

    // for 5s and 60s averages, a little special logic to handle cases
    // where we don't have enough history to dampen based on past performance
    var i = 5 > iterations ? iterations * 1.0 : 5.0;
    averages[1] = ((i-1) * averages[1] + avgUsersThisPeriod) / i;
    var i = 60 > iterations ? iterations * 1.0 : 60.0;
    averages[2] = ((i-1) * averages[2] + avgUsersThisPeriod) / i;
  }

  function outputAverages() {
    var actSumString = numOutstanding() + " R, " + numStarted + " S";
    var actNums = [];
    Object.keys(outstanding).forEach(function(act) {
      actNums.push(outstanding[act] + act.substr(0,1) + act.substr(-1,1));
    });
    actSumString += " (" + actNums.join(' ') + ")";

    console.log("\t", averages[0].toFixed(2),
                "\t", averages[1].toFixed(2),
                "\t", numErrors ? "(" + numErrors + " ERRORS!)" : "",
                "\t", num503s ? " (" + num503s + " 503s)" : "");
  }

  // ** how much time has elapsed since the last poll?
  var elapsed;
  {
    var now = new Date();
    elapsed = now - lastPoll;
    lastPoll = now;
  }

  // ** update running averages **
  updateAverages(elapsed);

  // ** determine how many activities to start **

  // how many active users would we like to simulate
  var targetActive = args.m;
  // if we're not throttled, then we'll trying 150% as many as
  // we're simulating right now.  If we're not simulating at least
  // 10000 active users, that shall be our lower bound
  if (!targetActive) {
    if (averages[0] > 10000) targetActive = averages[0] * 1.5;
    else targetActive = 10000;
  }

  // now how many new activities do we want to start?
  var newAct = activitiesPerUserPerSecond * targetActive;

  // scale based on how much time has elapsed since the last poll
  // on every iteration except the first
  if (iterations) newAct *= (elapsed / 1000);

  // probabilistic rounding
  {
    var add = (newAct % 1.0) < Math.random() ? 0 : 1;
    newAct = Math.floor(newAct) + add;
  }

  // ** start activities **

  // start the new activites until they're all started, or until we've
  // got twice as many outstanding as would be required by the target we
  // want to hit (which means the server can't keep up).
  while (newAct >= 1.0 && numOutstanding() < (activitiesPerUserPerSecond * targetActive * 2)) {
    numStarted++;
    startNewActivity();
    newAct--;
  }

  // ** schedule another wake up
  var wakeUpIn = 1000 - (new Date() - lastPoll);
  setTimeout(poll, wakeUpIn);

  // display averages
  outputAverages();

  iterations++;
// always start out by creating a bunch of users
var NUM_INITIAL_USERS = 100;
// if an explicit target was specified, let's output what that means
// in understandable terms
if (args.m) outputActiveUserSummary(args.m);

const userdb = require("../lib/load_gen/user_db.js");
const lg_crypto = require("../lib/load_gen/crypto.js");

lg_crypto.init(function(err) {
  if (err) {
    process.stderr.write('error initializing crypto module: ' + err);
  if (args.u) {
    // parse args.u
    var start, end;
    try {
      var r = args.u.split('/');
      if (r.length != 2) throw "expected format ##/##";
      start = parseInt(r[0], 10);
      end = parseInt(r[1], 10);
      if (start >= end) throw "first number must be smaller than the second";
    } catch(e) {
      console.log("your -u argument is poorly formated: " + e.toString());
      process.exit(1);
    }

    // now create all them users!
    console.log("Runing with", (end - start + 1), "pre-created users (XXX@loadtest.domain)");
    for (var i = start; i < end; i++) {
      userdb.addNewUser(userdb.getNewUser(i + "@loadtest.domain", "THE PASSWORD"));
    }
    console.log("users created!  applying load...");
    poll();
  } else {
    console.log("To start, let's create " + NUM_INITIAL_USERS + " users via the API.  One moment please...");

    var createUser = require("../lib/load_gen/activities/signup.js").startFunc;
    var created = 0;
    for (var i = 0; i < NUM_INITIAL_USERS; i++) {
      createUser(configuration, function(err) {
        if (err) {
          console.log("failed to create initial users! tragedy!  run away!:", err);
          process.exit(1);
        }
        process.stdout.write(".");
        if (++created == NUM_INITIAL_USERS) {
          process.stdout.write("\n\n");
          console.log("Average active users simulated over the last 1s/5s/60s:");
          poll();
        }
      });
    }