diff --git a/performance/README.md b/performance/README.md
index d218ae94c8085200ecb6b755378948db0cb67f00..be276d4570037332bfb121c637f76a53eac4f403 100644
--- a/performance/README.md
+++ b/performance/README.md
@@ -28,7 +28,7 @@ The final bit of assumption is growth rate, what percentage of active
 users in a unit of time are using browserid for the first time.  This
 is interesting as different types of requests (with different costs)
 are made during initial user signup.  We start by assuming a 20/80 split
-of new to returning users.
+of new to returning users per month.
 
 The next bit of guesswork required is to explain the behaviors of these
 sites (RPs) that a user visits.  The average RP will set authentication
diff --git a/performance/lib/add_email.js b/performance/lib/add_email.js
new file mode 100644
index 0000000000000000000000000000000000000000..651bffdacde1c984112be57f5b9da94314047b5f
--- /dev/null
+++ b/performance/lib/add_email.js
@@ -0,0 +1,4 @@
+exports.startFunc = function(cfg, cb) {
+  // XXX: write me
+  setTimeout(function() { cb(); }, 10); 
+};
diff --git a/performance/lib/include_only.js b/performance/lib/include_only.js
new file mode 100644
index 0000000000000000000000000000000000000000..651bffdacde1c984112be57f5b9da94314047b5f
--- /dev/null
+++ b/performance/lib/include_only.js
@@ -0,0 +1,4 @@
+exports.startFunc = function(cfg, cb) {
+  // XXX: write me
+  setTimeout(function() { cb(); }, 10); 
+};
diff --git a/performance/lib/reauth.js b/performance/lib/reauth.js
new file mode 100644
index 0000000000000000000000000000000000000000..651bffdacde1c984112be57f5b9da94314047b5f
--- /dev/null
+++ b/performance/lib/reauth.js
@@ -0,0 +1,4 @@
+exports.startFunc = function(cfg, cb) {
+  // XXX: write me
+  setTimeout(function() { cb(); }, 10); 
+};
diff --git a/performance/lib/reset_pass.js b/performance/lib/reset_pass.js
new file mode 100644
index 0000000000000000000000000000000000000000..651bffdacde1c984112be57f5b9da94314047b5f
--- /dev/null
+++ b/performance/lib/reset_pass.js
@@ -0,0 +1,4 @@
+exports.startFunc = function(cfg, cb) {
+  // XXX: write me
+  setTimeout(function() { cb(); }, 10); 
+};
diff --git a/performance/lib/signin.js b/performance/lib/signin.js
new file mode 100644
index 0000000000000000000000000000000000000000..651bffdacde1c984112be57f5b9da94314047b5f
--- /dev/null
+++ b/performance/lib/signin.js
@@ -0,0 +1,4 @@
+exports.startFunc = function(cfg, cb) {
+  // XXX: write me
+  setTimeout(function() { cb(); }, 10); 
+};
diff --git a/performance/lib/signup.js b/performance/lib/signup.js
new file mode 100644
index 0000000000000000000000000000000000000000..651bffdacde1c984112be57f5b9da94314047b5f
--- /dev/null
+++ b/performance/lib/signup.js
@@ -0,0 +1,4 @@
+exports.startFunc = function(cfg, cb) {
+  // XXX: write me
+  setTimeout(function() { cb(); }, 10); 
+};
diff --git a/performance/run.js b/performance/run.js
index e29d656645897a506afaf4893cad36653b8b8d4d..9e8d4f4d265871c782135fb57b81bf16661606b0 100755
--- a/performance/run.js
+++ b/performance/run.js
@@ -48,7 +48,9 @@ var argv = require('optimist')
 .describe('h', 'display this usage message')
 .alias('m', 'max')
 .describe('m', 'maximum active users to simulate (0 == infinite)')
-.default('m', 100)
+.default('m', 1000)
+.describe('o', 'maximum *outstanding* activities to allow')
+.default('o', 100)
 .alias('s', 'server')
 .describe('s', 'base URL to browserid server')
 .demand('s')
@@ -62,3 +64,114 @@ if (args.h) {
   process.exit(1);
 }
 
+// global configuration
+const configuration = {
+  verifier: argv.v ? argv.v : argv.s + "/verify",
+  browserid: argv.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 = {
+};
+
+// activities
+var activity = { 
+  "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))
+  },
+  "reset_pass": { 
+    // 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 ever 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)
+  }
+};
+
+// 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/" + 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;
+  }
+})();
+
+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!
+    activity[act].startFunc(configuration, function() {
+      console.log(act, "complete");
+    });
+  }
+
+  // XXX: next work to be done is here.  upon each call to poll we must:
+  // 1. update running averages based on activites completed while we
+  //    were sleeping.
+  // 2. 
+  // 3. determine how many activities to start based on throttling,
+  //    current outstanding, and current active users being simulated
+  // 4. start those activities
+  // 5. schedule another poll 1s from the time the last was started
+
+  // XXX: test...
+  for (var i = 0; i < 100; i++) startNewActivity();
+}
+
+poll();