todosapp: dom logging mechanism

Change-Id: I748a2be01a81dc15a20eeff5d53191c8cb6ea399
diff --git a/browser/benchmark.js b/browser/benchmark.js
index f1be681..9e264a0 100644
--- a/browser/benchmark.js
+++ b/browser/benchmark.js
@@ -3,30 +3,23 @@
 'use strict';
 
 var async = require('async');
-var moment = require('moment');
 var syncbase = require('syncbase');
 var nosql = syncbase.nosql;
 
+var util = require('./util');
+
 exports.logLatency = logLatency;
 exports.runBenchmark = runBenchmark;
 
 var LOG_EVERYTHING = false;
 
-// Returns a string timestamp, useful for logging.
-function timestamp(t) {
-  t = t || Date.now();
-  return moment(t).format('HH:mm:ss.SSS');
-}
-
 function logStart(name) {
-  var t = Date.now();
-  console.log(timestamp(t) + ' ' + name + ' start');
-  return t;
+  util.log(name + ' start');
+  return Date.now();
 }
 
 function logStop(name, start) {
-  var t = Date.now();
-  console.log(timestamp(t) + ' ' + name + ' took ' + (t - start) + 'ms');
+  util.log(name + ' took ' + (Date.now() - start) + 'ms');
 }
 
 function logLatency(name, cb) {
@@ -47,15 +40,15 @@
 // which should create the VC.)
 function doPuts(ctx, tb, n, cb) {
   cb = logLatency('doPuts', cb);
-  var prefix = timestamp() + '.';
+  var prefix = util.timestamp() + '.';
   async.times(100, function(n, cb) {
     // TODO(sadovsky): Remove this once we loosen Syncbase's naming rules.
     prefix = prefix.replace(/:/g, '.');
     var key = prefix + n;
     var value = '';
-    if (LOG_EVERYTHING) console.log('put: ' + key);
+    if (LOG_EVERYTHING) util.log('put: ' + key);
     tb.put(ctx, key, value, function(err) {
-      if (LOG_EVERYTHING) console.log('put done: ' + key);
+      if (LOG_EVERYTHING) util.log('put done: ' + key);
       cb(err);
     });
   }, function(err) {
@@ -70,11 +63,11 @@
   tb.scan(ctx, nosql.rowrange.prefix(prefix), function(err) {
     err = err || streamErr;
     if (err) return cb(err);
-    console.log('scanned ' + bytes + ' bytes');
+    util.log('scanned ' + bytes + ' bytes');
     cb();
   }).on('data', function(row) {
     bytes += row.key.length + row.value.length;
-    if (LOG_EVERYTHING) console.log('scan: ' + JSON.stringify(row));
+    if (LOG_EVERYTHING) util.log('scan: ' + JSON.stringify(row));
   }).on('error', function(err) {
     streamErr = streamErr || err.error;
   });
diff --git a/browser/defaults.js b/browser/defaults.js
index e9dfea4..bae85e3 100644
--- a/browser/defaults.js
+++ b/browser/defaults.js
@@ -7,6 +7,7 @@
 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 = [
@@ -90,10 +91,10 @@
       if (benchmark) {
         return bm.runBenchmark(ctx, db, cb);
       }
-      console.log('app exists; assuming everything has been initialized');
+      util.log('app exists; assuming everything has been initialized');
       return cb(null, disp);
     }
-    console.log('app does not exist; initializing everything');
+    util.log('app does not exist; initializing everything');
     app.create(wt(ctx), {}, function(err) {
       if (err) return cb(err);
       db.create(wt(ctx), {}, function(err) {
@@ -103,7 +104,7 @@
           if (benchmark) {
             return bm.runBenchmark(ctx, db, cb);
           }
-          console.log('hierarchy created; writing rows');
+          util.log('hierarchy created; writing rows');
           initData(disp, function(err) {
             if (err) return cb(err);
             cb(null, disp);
diff --git a/browser/index.js b/browser/index.js
index 17eb9a1..00e3eff 100644
--- a/browser/index.js
+++ b/browser/index.js
@@ -12,7 +12,8 @@
 var vanadium = require('vanadium');
 
 var defaults = require('./defaults');
-var h = require('./util').h;
+var util = require('./util');
+var h = util.h;
 
 ////////////////////////////////////////
 // Constants
@@ -422,8 +423,8 @@
   },
   componentWillUpdate: function(nextProps, nextState) {
     if (false) {
-      console.log(this.props, nextProps);
-      console.log(this.state, nextState);
+      util.log(this.props, nextProps);
+      util.log(this.state, nextState);
     }
   },
   componentDidUpdate: function() {
@@ -487,12 +488,21 @@
 ////////////////////////////////////////
 // Initialization
 
+var logEl = document.querySelector('#log');
+util.addLogger(function() {
+  var msgEl = document.createElement('div');
+  msgEl.className = 'msg';
+  msgEl.innerText = Array.prototype.slice.call(arguments).join(' ');
+  logEl.appendChild(msgEl);
+});
+util.log('starting app');
+
 var u = url.parse(window.location.href, true);
 
 var rc;  // React component
 function render(props) {
   console.assert(!rc);
-  rc = React.render(Page(props), document.getElementById('page'));
+  rc = React.render(Page(props), document.querySelector('#page'));
 }
 
 function initDispatcher(dispType, syncbaseName, benchmark, cb) {
diff --git a/browser/syncbase_dispatcher.js b/browser/syncbase_dispatcher.js
index e7cc62c..1c64ccc 100644
--- a/browser/syncbase_dispatcher.js
+++ b/browser/syncbase_dispatcher.js
@@ -31,6 +31,7 @@
 
 var bm = require('./benchmark');
 var Dispatcher = require('./dispatcher');
+var util = require('./util');
 
 inherits(SyncbaseDispatcher, Dispatcher);
 module.exports = SyncbaseDispatcher;
@@ -248,7 +249,7 @@
 };
 
 SyncbaseDispatcher.prototype.logTraceRecords = function() {
-  console.log(vtrace.formatTraces(this.getTraceRecords()));
+  util.log(vtrace.formatTraces(this.getTraceRecords()));
 };
 
 // TODO(sadovsky): Watch for changes on Syncbase itself so that we can detect
diff --git a/browser/util.js b/browser/util.js
index 290e6b6..999ca99 100644
--- a/browser/util.js
+++ b/browser/util.js
@@ -1,6 +1,7 @@
 'use strict';
 
 var _ = require('lodash');
+var moment = require('moment');
 var React = require('react');
 
 exports.h = function(selector, props, children) {
@@ -11,12 +12,31 @@
     props = {};
   }
   var parts = selector.split('.');
-  var x = parts[0].split('#'), type = x[0], id = x[1];
+  var x = parts[0].split('#'), tagName = x[0], id = x[1];
   var className = parts.slice(1).join(' ');
-  console.assert(type);
+  console.assert(tagName);
   props = _.assign({}, props, {
     id: id || undefined,
     className: className || undefined
   });
-  return React.createElement(type, props, children);
+  return React.createElement(tagName, props, children);
+};
+
+// Returns a string timestamp, useful for logging.
+var timestamp = exports.timestamp = function(t) {
+  t = t || Date.now();
+  return moment(t).format('HH:mm:ss.SSS');
+};
+
+var LOGGERS = [console.log.bind(console)];
+
+exports.addLogger = function(logger) {
+  LOGGERS.push(logger);
+};
+
+exports.log = function() {
+  var args = [timestamp()].concat(Array.prototype.slice.call(arguments));
+  _.forEach(LOGGERS, function(logger) {
+    logger.apply(null, args);
+  });
 };
diff --git a/public/extras.css b/public/extras.css
index ac5a4f1..2edb974 100644
--- a/public/extras.css
+++ b/public/extras.css
@@ -1,3 +1,9 @@
+*,
+:before,
+:after {
+  box-sizing: border-box;
+}
+
 .disp-type {
   position: fixed;
   top: 0;
@@ -16,3 +22,33 @@
 .disp-type.syncbase {
   background-color: #673ab7;
 }
+
+#log {
+  position: fixed;
+  right: 0;
+  bottom: 0;
+  width: 20px;
+  height: 20px;
+  background-color: #e91e63;
+  font: 400 14px/1.4 monospace;
+  overflow-x: hidden;
+  overflow-y: scroll;
+  white-space: pre-wrap;
+  word-wrap: break-word;
+}
+
+#log .msg {
+  display: none;
+}
+
+#log:hover {
+  width: 100%;
+  height: 300px;
+  padding: 16px;
+  background-color: #fff;
+  border-top: 1px solid #000;
+}
+
+#log:hover .msg {
+  display: block;
+}
diff --git a/public/index.html b/public/index.html
index 599dcfa..9cf329c 100644
--- a/public/index.html
+++ b/public/index.html
@@ -10,6 +10,7 @@
   </head>
   <body>
     <div id="page"></div>
+    <div id="log"></div>
     <script src="/third_party/async.min.js"></script>
     <script src="/third_party/lodash.min.js"></script>
     <script src="/third_party/moment.min.js"></script>