todosapp: refactor benchmark setup, extract createHierarchy helper

Change-Id: I76624212448276a7f30142eb7a5d730abba7315a
diff --git a/browser/benchmark.js b/browser/benchmark.js
index 9fe0187..52a73a2 100644
--- a/browser/benchmark.js
+++ b/browser/benchmark.js
@@ -7,34 +7,13 @@
 var nosql = syncbase.nosql;
 
 var util = require('./util');
-
-exports.logFn = logFn;
-exports.runBenchmark = runBenchmark;
+var wt = util.wt;
 
 var LOG_EVERYTHING = false;
 
-function logStart(name) {
-  console.log(name + ' start');
-  return Date.now();
-}
-
-function logStop(name, start, err) {
-  var dt = Date.now() - start;
-  console.log(name + (err ? ' FAILED after ' : ' took ') + dt + 'ms');
-  if (err) console.error(err);
-}
-
-function logFn(name, cb) {
-  var start = logStart(name);
-  return function(err) {
-    logStop(name, start, err);
-    cb.apply(null, arguments);
-  };
-}
-
 // Does n parallel puts with a common prefix, then returns the prefix.
 function doPuts(ctx, tb, n, cb) {
-  cb = logFn('doPuts', cb);
+  cb = util.logFn('doPuts', cb);
   var prefix = util.timestamp() + '.';
   async.times(100, function(n, cb) {
     // TODO(sadovsky): Remove this once we loosen Syncbase's naming rules.
@@ -51,9 +30,9 @@
   });
 }
 
-// Scans (and logs) all records with the given prefix.
+// Scans all records with the given prefix.
 function doScan(ctx, tb, prefix, cb) {
-  cb = logFn('doScan(' + prefix + ')', cb);
+  cb = util.logFn('doScan(' + prefix + ')', cb);
   var bytes = 0, streamErr = null;
   tb.scan(ctx, nosql.rowrange.prefix(prefix), function(err) {
     err = err || streamErr;
@@ -68,12 +47,18 @@
   });
 }
 
-// Assumes table 'tb' exists.
-function runBenchmark(ctx, db, cb) {
-  cb = logFn('runBenchmark', cb);
-  var tb = db.table('tb');
-  doPuts(ctx, tb, 100, function(err, prefix) {
+// Creates a fresh hierarchy, then runs doPuts followed by doScan.
+exports.runBenchmark = function(rt, name, cb) {
+  cb = util.logFn('runBenchmark', cb);
+  var ctx = rt.getContext();
+  var s = syncbase.newService(name);
+  var appName = 'bm.' + util.uid();
+  util.createHierarchy(ctx, s, appName, 'db', 'tb', function(err, db) {
     if (err) return cb(err);
-    doScan(ctx, tb, prefix, cb);
+    var tb = db.table('tb');
+    doPuts(wt(ctx), tb, 100, function(err, prefix) {
+      if (err) return cb(err);
+      doScan(wt(ctx), tb, prefix, cb);
+    });
   });
-}
+};
diff --git a/browser/collection_dispatcher.js b/browser/collection_dispatcher.js
index 1986dc2..79c3717 100644
--- a/browser/collection_dispatcher.js
+++ b/browser/collection_dispatcher.js
@@ -67,6 +67,7 @@
   cb = cb || noop;
   return function(err) {
     cb.apply(null, arguments);
-    if (!err) that.emit('change');
+    if (err) return;
+    that.emit('change');
   };
 };
diff --git a/browser/defaults.js b/browser/defaults.js
index 2dceddb..6a876f3 100644
--- a/browser/defaults.js
+++ b/browser/defaults.js
@@ -5,10 +5,10 @@
 var vanadium = require('vanadium');
 var verror = vanadium.verror;
 
-var bm = require('./benchmark');
 var CollectionDispatcher = require('./collection_dispatcher');
 var MemCollection = require('./mem_collection');
 var SyncbaseDispatcher = require('./syncbase_dispatcher');
+var util = require('./util');
 
 // Copied from meteor/todos/server/bootstrap.js.
 var data = [
@@ -49,7 +49,7 @@
 ];
 
 function initData(disp, cb) {
-  cb = bm.logFn('initData', cb);
+  cb = util.logFn('initData', cb);
   var timestamp = Date.now();
   async.each(data, function(list, cb) {
     disp.addList({name: list.name}, function(err, listId) {
@@ -73,52 +73,28 @@
   });
 }
 
-// Returns a new Vanadium context object with a timeout.
-function wt(ctx, timeout) {
-  return ctx.withTimeout(timeout || 5000);
-}
-
-exports.initSyncbaseDispatcher = function(rt, name, benchmark, cb) {
-  cb = bm.logFn('initSyncbaseDispatcher', cb);
+exports.initSyncbaseDispatcher = function(rt, name, cb) {
+  cb = util.logFn('initSyncbaseDispatcher', cb);
   var ctx = rt.getContext();
-  var service = syncbase.newService(name);
-  var app = service.app('todos'), db = app.noSqlDatabase('db');
-  var disp = new SyncbaseDispatcher(rt, db);
-  app.create(wt(ctx), {}, function(err) {
-    if (err) {
-      if (err instanceof verror.ExistError) {
-        if (benchmark) {
-          return bm.runBenchmark(ctx, db, cb);
-        }
-        console.log('app exists; assuming database has been initialized');
-        return cb(null, disp);
-      }
+  var s = syncbase.newService(name);
+  util.createHierarchy(ctx, s, 'todos', 'db', 'tb', function(err, db) {
+    if (err && !(err instanceof verror.ExistError)) {
       return cb(err);
     }
-    console.log('app did not exist; initializing database');
-    db.create(wt(ctx), {}, function(err) {
+    var disp = new SyncbaseDispatcher(rt, db);
+    if (err) {  // verror.ExistError
+      console.log('skipping initData');
+      return cb(null, disp);
+    }
+    initData(disp, function(err) {
       if (err) return cb(err);
-      db.createTable(wt(ctx), 'tb', {}, function(err) {
-        if (err) return cb(err);
-        if (benchmark) {
-          // TODO(sadovsky): Restructure things so that we still call initData
-          // even if on the first page load we ran the benchmark. Maybe do this
-          // by having the benchmark use a completely different app and/or
-          // database.
-          return bm.runBenchmark(ctx, db, cb);
-        }
-        console.log('hierarchy created; writing rows');
-        initData(disp, function(err) {
-          if (err) return cb(err);
-          cb(null, disp);
-        });
-      });
+      cb(null, disp);
     });
   });
 };
 
 exports.initCollectionDispatcher = function(cb) {
-  cb = bm.logFn('initCollectionDispatcher', cb);
+  cb = util.logFn('initCollectionDispatcher', cb);
   var lists = new MemCollection('lists'), todos = new MemCollection('todos');
   var disp = new CollectionDispatcher(lists, todos);
   initData(disp, function(err) {
diff --git a/browser/dom_log.js b/browser/dom_log.js
index fa38ac3..e7beb8b 100644
--- a/browser/dom_log.js
+++ b/browser/dom_log.js
@@ -31,7 +31,7 @@
   window.onerror = function(errorMsg, url, lineNumber) {
     var args = [util.timestamp(), errorMsg];
     append('msg error', args.join(' '));
-    // Show log if it's not already visible, so that the developer sees the
+    // Show the log if it's not already visible, so that the developer sees the
     // error.
     logEl.classList.add('visible');
   };
diff --git a/browser/index.js b/browser/index.js
index dd5d88d..5b9b82c 100644
--- a/browser/index.js
+++ b/browser/index.js
@@ -13,7 +13,8 @@
 var bm = require('./benchmark');
 var defaults = require('./defaults');
 var domLog = require('./dom_log');
-var h = require('./util').h;
+var util = require('./util');
+var h = util.h;
 
 ////////////////////////////////////////
 // Constants
@@ -37,7 +38,7 @@
 function noop() {}
 
 function initVanadium(cb) {
-  cb = bm.logFn('initVanadium', cb);
+  cb = util.logFn('initVanadium', cb);
   var vanadiumConfig = {
     logLevel: vanadium.vlog.levels.INFO,
     namespaceRoots: u.query.mounttable ? [u.query.mounttable] : undefined,
@@ -46,7 +47,7 @@
   vanadium.init(vanadiumConfig, cb);
 }
 
-function initDispatcher(dispType, syncbaseName, benchmark, cb) {
+function initDispatcher(dispType, syncbaseName, cb) {
   var clientCb = cb;
   cb = function(err, resDisp) {
     if (err) return clientCb(err);
@@ -54,13 +55,12 @@
     clientCb();
   };
   if (dispType === DISP_TYPE_COLLECTION) {
-    console.assert(!benchmark);
     defaults.initCollectionDispatcher(cb);
   } else if (dispType === DISP_TYPE_SYNCBASE) {
     initVanadium(function(err, rt) {
       if (err) return cb(err);
       userEmail = blessingToEmail(rt.accountName);
-      defaults.initSyncbaseDispatcher(rt, syncbaseName, benchmark, cb);
+      defaults.initSyncbaseDispatcher(rt, syncbaseName, cb);
     });
   } else {
     process.nextTick(function() {
@@ -401,12 +401,12 @@
           if (shared) {
             // TODO(sadovsky): Let the user add members to an existing SG once
             // Syncbase supports it.
-            console.error('Cannot add members to existing SyncGroup.');
+            alert('Cannot add members to an existing SyncGroup.');
             return;
           }
           // TODO(sadovsky): Better input validation.
           if (!value.includes('@') || !value.includes('.')) {
-            console.error('Invalid email address.');
+            alert('Invalid email address.');
             return;
           }
           disp.createSyncGroup(list._id, [
@@ -531,18 +531,22 @@
   },
   componentWillMount: function() {
     var that = this, props = this.props;
-    var dt = props.dispType, sn = props.syncbaseName, bm = props.benchmark;
+    if (props.benchmark) {
+      initVanadium(function(err, rt) {
+        if (err) throw err;
+        bm.runBenchmark(rt, props.syncbaseName, function(err) {
+          if (err) throw err;
+        });
+      });
+      return;
+    }
     function done() {
       that.setState({dispInitialized: true});
     }
-    initDispatcher(dt, sn, bm, function(err) {
+    initDispatcher(props.dispType, props.syncbaseName, function(err) {
       if (err) throw err;
-      if (bm) {
-        console.log('benchmark done');
-        return;
-      }
       if (props.joinListId) {
-        console.assert(dt === DISP_TYPE_SYNCBASE);
+        console.assert(props.dispType === DISP_TYPE_SYNCBASE);
         disp.joinSyncGroup(props.joinListId, function(err) {
           // Note, joinSyncGroup is a noop (no error) if the caller is already a
           // member, which is the desired behavior here.
@@ -623,7 +627,7 @@
     // TODO(sadovsky): Only read (and only redraw) what's needed based on what
     // changed.
     disp.on('change', function() {
-      var doneCb = bm.logFn('onChange', function(err) {
+      var doneCb = util.logFn('onChange', function(err) {
         if (err) throw err;
       });
       updateLists(function(err) {
diff --git a/browser/syncbase_dispatcher.js b/browser/syncbase_dispatcher.js
index 89ac9b3..8895f8e 100644
--- a/browser/syncbase_dispatcher.js
+++ b/browser/syncbase_dispatcher.js
@@ -20,14 +20,14 @@
 var _ = require('lodash');
 var async = require('async');
 var inherits = require('inherits');
-var randomBytes = require('randombytes');
 var syncbase = require('syncbase');
 var nosql = syncbase.nosql;
 var vanadium = require('vanadium');
 var vtrace = vanadium.vtrace;
 
-var bm = require('./benchmark');
 var Dispatcher = require('./dispatcher');
+var util = require('./util');
+var wn = util.wn, wt = util.wt;
 
 inherits(SyncbaseDispatcher, Dispatcher);
 module.exports = SyncbaseDispatcher;
@@ -61,17 +61,12 @@
   return args.join(SEP);
 }
 
-function uuid(len) {
-  len = len || 16;
-  return randomBytes(Math.ceil(len / 2)).toString('hex').substr(0, len);
-}
-
 function newListKey() {
-  return uuid();
+  return util.uid();
 }
 
 function newTodoKey(listId) {
-  return join(listId, 'todos', uuid());
+  return join(listId, 'todos', util.uid());
 }
 
 function tagKey(todoId, tag) {
@@ -95,19 +90,6 @@
   return JSON.parse(x);
 }
 
-////////////////////////////////////////
-// Vanadium helpers
-
-// Returns a new Vanadium context object with a timeout.
-function wt(ctx, timeout) {
-  return ctx.withTimeout(timeout || 5000);
-}
-
-// Returns a new Vanadium context object with the given name.
-function wn(ctx, name) {
-  return vtrace.withNewSpan(ctx, name);
-}
-
 var SILENT = new vanadium.context.ContextKey();
 
 // Defines a SyncbaseDispatcher method. If the first argument to fn is not a
@@ -131,7 +113,7 @@
       // to JSON, drop square brackets.
       var cb = args[args.length - 1];
       var argsStr = JSON.stringify(args.slice(1, -1)).slice(1, -1);
-      args[args.length - 1] = bm.logFn(name + '(' + argsStr + ')', cb);
+      args[args.length - 1] = util.logFn(name + '(' + argsStr + ')', cb);
     }
     return fn.apply(this, args);
   };
@@ -396,7 +378,7 @@
 // Random number, used to implement watch. Each client writes to their own watch
 // key to signify that they've written new data, and each client periodically
 // polls all watch keys to see if anything has changed.
-var clientId = uuid();
+var clientId = util.uid();
 function watchPrefix(listId) {
   return join(listId, 'watch');
 }
diff --git a/browser/util.js b/browser/util.js
index 0584383..1234937 100644
--- a/browser/util.js
+++ b/browser/util.js
@@ -2,7 +2,9 @@
 
 var _ = require('lodash');
 var moment = require('moment');
+var randomBytes = require('randombytes');
 var React = require('react');
+var vtrace = require('vanadium').vtrace;
 
 exports.h = function(selector, props, children) {
   if (_.isPlainObject(props)) {
@@ -27,3 +29,62 @@
   t = t || Date.now();
   return moment(t).format('HH:mm:ss.SSS');
 };
+
+// Returns a unique string identifier of the given length.
+exports.uid = function(len) {
+  len = len || 16;
+  return randomBytes(Math.ceil(len / 2)).toString('hex').substr(0, len);
+};
+
+function logStart(name) {
+  console.log(name + ' start');
+  return Date.now();
+}
+
+function logStop(name, start, err) {
+  var dt = Date.now() - start;
+  console.log(name + (err ? ' FAILED after ' : ' took ') + dt + 'ms');
+  if (err) console.error(err);
+}
+
+// Wraps the given callback to log start time, stop time, and delta time of a
+// function invocation.
+function logFn(name, cb) {
+  var start = logStart(name);
+  return function(err) {
+    logStop(name, start, err);
+    cb.apply(null, arguments);
+  };
+}
+exports.logFn = logFn;
+
+// Returns a new Vanadium context object with the given name.
+exports.wn = function(ctx, name) {
+  return vtrace.withNewSpan(ctx, name);
+};
+
+// Returns a new Vanadium context object with a timeout.
+function wt(ctx, timeout) {
+  return ctx.withTimeout(timeout || 5000);
+}
+exports.wt = wt;
+
+// Creates <app>/<database>/<table> hierarchy in Syncbase.
+// Note, for errors we still return the db handle since some errors (e.g.
+// verror.ExistError) are non-fatal.
+exports.createHierarchy = function(ctx, service, appName, dbName, tbName, cb) {
+  var app = service.app(appName), db = app.noSqlDatabase(dbName);
+  var appLog = 'create app "' + appName + '"';
+  app.create(wt(ctx), {}, logFn(appLog, function(err) {
+    if (err) return cb(err, db);
+    var dbLog = 'create database "' + dbName + '"';
+    db.create(wt(ctx), {}, logFn(dbLog, function(err) {
+      if (err) return cb(err, db);
+      var tbLog = 'create table "' + tbName + '"';
+      db.createTable(wt(ctx), tbName, {}, logFn(tbLog, function(err) {
+        if (err) return cb(err, db);
+        cb(null, db);
+      }));
+    }));
+  }));
+};