changeset 10:0a694cb579ec

map-reduce now supports 'chunking', i.e. processing some number of documents and then giving the system a chance to report progress to the user, cancel the calculation, giving the UI a chance to breathe, etc, before resuming the calculation.
author Atul Varma <varmaa@toolness.com>
date Fri, 10 Apr 2009 12:05:40 -0700
parents 8842af55f7fe
children 89c6105c2d1e
files browser-couch.js tests.js
diffstat 2 files changed, 66 insertions(+), 19 deletions(-) [+]
line wrap: on
line diff
--- a/browser-couch.js	Fri Apr 10 11:32:23 2009 -0700
+++ b/browser-couch.js	Fri Apr 10 12:05:40 2009 -0700
@@ -138,15 +138,20 @@
     this.view = function DB_view(options) {
       if (!options.map)
         throw new Error('map function not provided');
+      if (!options.finished)
+        throw new Error('finished callback not provided');
 
       BrowserCouch._mapReduce(options.map,
                               options.reduce,
                               documents,
-                              options.callback);
+                              options.progress,
+                              options.finished,
+                              options.chunkSize);
     };
   },
 
-  _mapReduce: function BC__mapReduce(map, reduce, documents, cb) {
+  _mapReduce: function BC__mapReduce(map, reduce, documents, progress,
+                                     finished, chunkSize) {
     var mapResult = {};
 
     function emit(key, value) {
@@ -158,28 +163,61 @@
       mapResult[key].push(value);
     }
 
-    for (var i = 0; i < documents.length; i++)
-      map(documents[i], emit);
+    // Maximum number of documents to process before giving the UI a chance
+    // to breathe.
+    var DEFAULT_CHUNK_SIZE = 1000;
+
+    // If no progress callback is given, we'll automatically give the
+    // UI a chance to breathe for this many milliseconds before continuing
+    // processing.
+    var DEFAULT_UI_BREATHE_TIME = 50;
+
+    if (!chunkSize)
+      chunkSize = DEFAULT_CHUNK_SIZE;
+    var i = 0;
 
-    if (reduce) {
-      var keys = [];
-      var values = [];
+    function continueMap() {
+      var iAtStart = i;
 
-      for (key in mapResult) {
-        keys.push(key);
-        values.push(mapResult[key]);
+      do {
+        map(documents[i], emit);
+        i++;
+      } while (i - iAtStart < chunkSize &&
+               i < documents.length)
+
+      if (i == documents.length)
+        doReduce();
+      else {
+        if (progress)
+          progress(i / documents.length, continueMap);
+        else
+          window.setTimeout(continueMap, DEFAULT_UI_BREATHE_TIME);
       }
+    }
 
-      cb(reduce(keys, values));
-    } else {
-      var result = [];
+    continueMap();
+
+    function doReduce() {
+      if (reduce) {
+        var keys = [];
+        var values = [];
 
-      for (key in mapResult) {
-        var values = mapResult[key];
-        for (i = 0; i < values.length; i++)
-          result.push([key, values[i]]);
+        for (key in mapResult) {
+          keys.push(key);
+          values.push(mapResult[key]);
+        }
+
+        finished(reduce(keys, values));
+      } else {
+        var result = [];
+
+        for (key in mapResult) {
+          var values = mapResult[key];
+          for (var i = 0; i < values.length; i++)
+            result.push([key, values[i]]);
+        }
+        finished(result);
       }
-      cb(result);
     }
   }
 };
--- a/tests.js	Fri Apr 10 11:32:23 2009 -0700
+++ b/tests.js	Fri Apr 10 12:05:40 2009 -0700
@@ -47,12 +47,14 @@
     BrowserCouch.get(
       "blarg",
       function(db) {
+        var progressCalled = false;
         db.put(
           [{id: "monkey",
             content: "hello there dude"},
            {id: "chunky",
             content: "hello there dogen"}],
           function() {
+            var timesProgressCalled = 0;
             db.view(
               {map: function(doc, emit) {
                  var words = doc.content.split(" ");
@@ -65,7 +67,14 @@
                    totals[keys[i]] = values[i].length;
                  return totals;
                },
-               callback: function(result) {
+               chunkSize: 1,
+               progress: function(percentDone, resume) {
+                 self.assertEqual(percentDone, 0.5);
+                 progressCalled = true;
+                 resume();
+               },
+               finished: function(result) {
+                 self.assertEqual(progressCalled, true);
                  self.assertEqual(result.hello, 2);
                  self.assertEqual(result.there, 2);
                  self.assertEqual(result.dude, 1);