[benchmark] Add Fetch API performance tests.

- Introduces initWorker() and doAction(), and move common code to util.js.
- Changes verifyBlob's doneCallback arguments to match with fetch()'s promises.
- Adds 'Access-Control-Allow-Origin: *' header to XHR/fetch responses because cross-origin fetch() without such headers is not allowed even with --disable-web-security.

R=tyoshino@chromium.org

Review URL: https://codereview.appspot.com/226870043

git-svn-id: https://pywebsocket.googlecode.com/svn/trunk@862 4ff78f4a-9131-11de-b045-6380ec9940d4
diff --git a/src/example/benchmark.html b/src/example/benchmark.html
index 2eb89bf..84bf8c9 100644
--- a/src/example/benchmark.html
+++ b/src/example/benchmark.html
@@ -53,68 +53,34 @@
     // until the sum of sizes reaches this threshold.
     minTotal: getIntFromInput('mintotal'),
     multipliers: getIntArrayFromInput('multipliers'),
-    verifyData: getBoolFromCheckBox('verifydata')
+    verifyData: getBoolFromCheckBox('verifydata'),
+    addToLog: addToLog,
+    addToSummary: addToSummary,
+    measureValue: measureValue,
+    notifyAbort: notifyAbort
   };
 }
 
-var worker = new Worker('benchmark.js');
-worker.onmessage = onMessage;
-
 function onSendBenchmark() {
   var config = getConfig();
-
-  if (getBoolFromCheckBox('worker')) {
-    worker.postMessage({type: 'sendBenchmark', config: config});
-  } else {
-    config.addToLog = addToLog;
-    config.addToSummary = addToSummary;
-    config.measureValue = measureValue;
-    config.notifyAbort = notifyAbort;
-    sendBenchmark(config);
-  }
+  doAction(config, getBoolFromCheckBox('worker'), 'sendBenchmark');
 }
 
 function onReceiveBenchmark() {
   var config = getConfig();
-
-  if (getBoolFromCheckBox('worker')) {
-    worker.postMessage({type: 'receiveBenchmark', config: config});
-  } else {
-    config.addToLog = addToLog;
-    config.addToSummary = addToSummary;
-    config.measureValue = measureValue;
-    config.notifyAbort = notifyAbort;
-    receiveBenchmark(config);
-  }
+  doAction(config, getBoolFromCheckBox('worker'), 'receiveBenchmark');
 }
 
 function onBatchBenchmark() {
   var config = getConfig();
-
-  if (getBoolFromCheckBox('worker')) {
-    worker.postMessage({type: 'batchBenchmark', config: config});
-  } else {
-    config.addToLog = addToLog;
-    config.addToSummary = addToSummary;
-    config.measureValue = measureValue;
-    config.notifyAbort = notifyAbort;
-    batchBenchmark(config);
-  }
+  doAction(config, getBoolFromCheckBox('worker'), 'batchBenchmark');
 }
 
 function onStop() {
   var config = getConfig();
-
-  if (getBoolFromCheckBox('worker')) {
-    worker.postMessage({type: 'stop', config: config});
-  } else {
-    config.addToLog = addToLog;
-    config.addToSummary = addToSummary;
-    config.measureValue = measureValue;
-    config.notifyAbort = notifyAbort;
-    stop(config);
-  }
+  doAction(config, getBoolFromCheckBox('worker'), 'stop');
 }
+
 function init() {
   addressBox = document.getElementById('address');
   logBox = document.getElementById('log');
@@ -132,6 +98,8 @@
   if (!('WebSocket' in window)) {
     addToLog('WebSocket is not available');
   }
+
+  initWorker('WebSocket', '');
 }
 </script>
 </head>
diff --git a/src/example/benchmark.js b/src/example/benchmark.js
index 0d4763b..856435d 100644
--- a/src/example/benchmark.js
+++ b/src/example/benchmark.js
@@ -163,8 +163,6 @@
   return socket;
 }
 
-var tasks = [];
-
 function startBenchmark(config) {
   clearTimeout(timerID);
   destroyAllSockets();
@@ -186,24 +184,6 @@
   }
 }
 
-function runNextTask(config) {
-  var task = tasks.shift();
-  if (task == undefined) {
-    config.addToLog('Finished');
-    destroyAllSockets();
-    return;
-  }
-  timerID = setTimeout(task, 0);
-}
-
-function buildLegendString(config) {
-  var legend = ''
-  if (config.printSize)
-    legend = 'Message size in KiB, Time/message in ms, ';
-  legend += 'Speed in kB/s';
-  return legend;
-}
-
 function getConfigString(config) {
   return '(WebSocket' +
     ', ' + (typeof importScripts !== "undefined" ? 'Worker' : 'Main') +
@@ -215,58 +195,6 @@
     ')';
 }
 
-function addTasks(config, stepFunc) {
-  for (var i = 0;
-      i < config.numWarmUpIterations + config.numIterations; ++i) {
-    var multiplierIndex = 0;
-    for (var size = config.startSize;
-         size <= config.stopThreshold;
-         ++multiplierIndex) {
-      var task = stepFunc.bind(
-          null,
-          size,
-          config,
-          i < config.numWarmUpIterations);
-      tasks.push(task);
-      size *= config.multipliers[
-          multiplierIndex % config.multipliers.length];
-    }
-  }
-}
-
-function addResultReportingTask(config, title) {
-  tasks.push(function(){
-      timerID = null;
-      config.addToSummary(title);
-      reportAverageData(config);
-      clearAverageData();
-      runNextTask(config);
-  });
-}
-
-function sendBenchmark(config) {
-  config.addToLog('Send benchmark');
-  config.addToLog(buildLegendString(config));
-
-  tasks = [];
-  clearAverageData();
-  addTasks(config, sendBenchmarkStep);
-  addResultReportingTask(config, 'Send Benchmark ' + getConfigString(config));
-  startBenchmark(config);
-}
-
-function receiveBenchmark(config) {
-  config.addToLog('Receive benchmark');
-  config.addToLog(buildLegendString(config));
-
-  tasks = [];
-  clearAverageData();
-  addTasks(config, receiveBenchmarkStep);
-  addResultReportingTask(config,
-      'Receive Benchmark ' + getConfigString(config));
-  startBenchmark(config);
-}
-
 function batchBenchmark(config) {
   config.addToLog('Batch benchmark');
   config.addToLog(buildLegendString(config));
@@ -281,25 +209,6 @@
   startBenchmark(config);
 }
 
-function stop(config) {
-  clearTimeout(timerID);
-  timerID = null;
-  config.addToLog('Stopped');
+function cleanup() {
   destroyAllSockets();
 }
-
-onmessage = function (message) {
-  var config = message.data.config;
-  config.addToLog = workerAddToLog;
-  config.addToSummary = workerAddToSummary;
-  config.measureValue = workerMeasureValue;
-  config.notifyAbort = workerNotifyAbort;
-  if (message.data.type === 'sendBenchmark')
-    sendBenchmark(config);
-  else if (message.data.type === 'receiveBenchmark')
-    receiveBenchmark(config);
-  else if (message.data.type === 'batchBenchmark')
-    batchBenchmark(config);
-  else if (message.data.type === 'stop')
-    stop(config);
-};
diff --git a/src/example/fetch_benchmark.html b/src/example/fetch_benchmark.html
new file mode 100644
index 0000000..596d779
--- /dev/null
+++ b/src/example/fetch_benchmark.html
@@ -0,0 +1,165 @@
+<!--
+Copyright 2015 Google Inc. All rights reserved.
+
+Use of this source code is governed by a BSD-style
+license that can be found in the COPYING file or at
+https://developers.google.com/open-source/licenses/bsd
+-->
+<html>
+<head>
+<title>Fetch API benchmark</title>
+<script src="util_main.js"></script>
+<script src="util.js"></script>
+<script src="fetch_benchmark.js"></script>
+<script>
+var addressBox = null;
+
+function getConfig() {
+  return {
+    prefixUrl: addressBox.value,
+    printSize: getBoolFromCheckBox('printsize'),
+    numFetches: getIntFromInput('numFetches'),
+    // Initial size of messages.
+    numIterations: getIntFromInput('numiterations'),
+    numWarmUpIterations: getIntFromInput('numwarmupiterations'),
+    startSize: getIntFromInput('startsize'),
+    // Stops benchmark when the size of message exceeds this threshold.
+    stopThreshold: getIntFromInput('stopthreshold'),
+    // If the size of each message is small, send/receive multiple messages
+    // until the sum of sizes reaches this threshold.
+    // minTotal: getIntFromInput('mintotal'),
+    // minTotal is not yet implemented on Fetch API benchmark
+    multipliers: getIntArrayFromInput('multipliers'),
+    verifyData: getBoolFromCheckBox('verifydata'),
+    addToLog: addToLog,
+    addToSummary: addToSummary,
+    measureValue: measureValue,
+    notifyAbort: notifyAbort
+  };
+}
+
+function onSendBenchmark() {
+  var config = getConfig();
+  config.dataType = getStringFromRadioBox('datatyperadio');
+  doAction(config, getBoolFromCheckBox('worker'), 'sendBenchmark');
+}
+
+function onReceiveBenchmark() {
+  var config = getConfig();
+  config.dataType = getStringFromRadioBox('datatyperadio');
+  doAction(config, getBoolFromCheckBox('worker'), 'receiveBenchmark');
+}
+
+function onBatchBenchmark() {
+  var config = getConfig();
+  doAction(config, getBoolFromCheckBox('worker'), 'batchBenchmark');
+}
+
+function onStop() {
+  var config = getConfig();
+  doAction(config, getBoolFromCheckBox('worker'), 'stop');
+}
+
+function init() {
+  addressBox = document.getElementById('address');
+  logBox = document.getElementById('log');
+
+  summaryBox = document.getElementById('summary');
+
+  // Special address of pywebsocket for XHR/Fetch API benchmark.
+  addressBox.value = '/073be001e10950692ccbf3a2ad21c245';
+
+  addToLog(window.navigator.userAgent.toLowerCase());
+  addToSummary(window.navigator.userAgent.toLowerCase());
+
+  initWorker('fetch', '');
+}
+</script>
+</head>
+<body onload="init()">
+
+<form id="benchmark_form">
+  url prefix <input type="text" id="address" size="40">
+  <input type="button" value="send" onclick="onSendBenchmark()">
+  <input type="button" value="receive" onclick="onReceiveBenchmark()">
+  <input type="button" value="batch" onclick="onBatchBenchmark()">
+  <input type="button" value="stop" onclick="onStop()">
+
+  <br/>
+
+  <input type="checkbox" id="printsize" checked>
+  <label for="printsize">Print size and time per message</label>
+  <input type="checkbox" id="verifydata" checked>
+  <label for="verifydata">Verify data</label>
+  <input type="checkbox" id="worker">
+  <label for="worker">Run on worker</label>
+
+  <br/>
+
+  Parameters:
+
+  <br/>
+
+  <table>
+    <tr>
+      <td>Number of fetch() requests</td>
+      <td><input type="text" id="numFetches" value="1"></td>
+    </tr>
+    <tr>
+      <td>Number of iterations</td>
+      <td><input type="text" id="numiterations" value="1"></td>
+    </tr>
+    <tr>
+      <td>Number of warm-up iterations</td>
+      <td><input type="text" id="numwarmupiterations" value="0"></td>
+    </tr>
+    <tr>
+      <td>Start size</td>
+      <td><input type="text" id="startsize" value="10240"></td>
+    </tr>
+    <tr>
+      <td>Stop threshold</td>
+      <td><input type="text" id="stopthreshold" value="102400000"></td>
+    </tr>
+    <tr>
+      <td>Minimum total</td>
+      <td><input type="text" id="mintotal" value="102400000"></td>
+    </tr>
+    <tr>
+      <td>Multipliers</td>
+      <td><input type="text" id="multipliers" value="5, 2"></td>
+    </tr>
+  </table>
+
+  Set data type
+  <input type="radio"
+         name="datatyperadio"
+         id="datatyperadiotext"
+         value="text"
+         checked><label for="datatyperadiotext">text</label>
+  <input type="radio"
+         name="datatyperadio"
+         id="datatyperadioblob"
+         value="blob"
+         ><label for="datatyperadioblob">blob</label>
+  <input type="radio"
+         name="datatyperadio"
+         id="datatyperadioarraybuffer"
+         value="arraybuffer"
+         ><label for="datatyperadioarraybuffer">arraybuffer</label>
+</form>
+
+<div id="log_div">
+  <textarea
+      id="log" rows="20" style="width: 100%" readonly></textarea>
+</div>
+<div id="summary_div">
+  Summary
+  <textarea
+      id="summary" rows="20" style="width: 100%" readonly></textarea>
+</div>
+
+Note: Effect of RTT and time spent for ArrayBuffer creation in receive benchmarks are not eliminated.
+
+</body>
+</html>
diff --git a/src/example/fetch_benchmark.js b/src/example/fetch_benchmark.js
new file mode 100644
index 0000000..1395219
--- /dev/null
+++ b/src/example/fetch_benchmark.js
@@ -0,0 +1,225 @@
+// Copyright 2015 Google Inc. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the COPYING file or at
+// https://developers.google.com/open-source/licenses/bsd
+
+var isWorker = typeof importScripts !== "undefined";
+
+if (isWorker) {
+  // Running on a worker
+  importScripts('util.js', 'util_worker.js');
+}
+
+// Namespace for holding globals.
+var benchmark = {};
+benchmark.startTimeInMs = 0;
+
+var timerID = null;
+
+function sendBenchmarkStep(size, config, isWarmUp) {
+  timerID = null;
+  benchmark.startTimeInMs = null;
+
+  // Prepare data.
+  var dataArray = [];
+  for (var i = 0; i < config.numFetches; ++i) {
+    var data = null;
+    if (config.dataType == 'arraybuffer' ||
+        config.dataType == 'blob') {
+      data = new ArrayBuffer(size);
+
+      fillArrayBuffer(data, 0x61);
+
+      if (config.dataType == 'blob') {
+        data = new Blob([data]);
+      }
+    } else {
+      data = repeatString('a', size);
+    }
+
+    dataArray.push(data);
+  }
+
+  // Start time measuring.
+  benchmark.startTimeInMs = getTimeStamp();
+
+  // Start fetch.
+  var promises = [];
+  for (var i = 0; i < config.numFetches; ++i) {
+    var data = dataArray[i];
+    var promise = fetch(config.prefixUrl + '_send',
+                        {method: 'POST', body: data})
+        .then(function (response) {
+          if (response.status != 200) {
+            config.addToLog('Failed (status=' + response.status + ')');
+            return Promise.reject();
+          }
+          // Check and warn if proxy is enabled.
+          if (response.headers.get('Via') !== null) {
+            config.addToLog('WARNING: proxy seems enabled.');
+          }
+          if (config.verifyData) {
+            return response.text()
+                .then(function(text) {
+                  if (!verifyAcknowledgement(config, text, size)) {
+                    return Promise.reject();
+                  }
+                });
+          }
+        });
+    promises.push(promise);
+  }
+
+  // Finish and report time measuring.
+  Promise.all(promises)
+      .then(function() {
+        if (benchmark.startTimeInMs == null) {
+          config.addToLog('startTimeInMs not set');
+          return Promise.reject();
+        }
+        calculateAndLogResult(config, size, benchmark.startTimeInMs,
+          size * config.numFetches, isWarmUp);
+        runNextTask(config);
+      })
+      .catch(function(e) {
+        config.addToLog("ERROR: " + e);
+        config.notifyAbort();
+      });
+}
+
+function receiveBenchmarkStep(size, config, isWarmUp) {
+  timerID = null;
+  benchmark.startTimeInMs = null;
+
+  // Start time measuring.
+  benchmark.startTimeInMs = getTimeStamp();
+
+  // Start fetch.
+  var promises = [];
+  for (var i = 0; i < config.numFetches; ++i) {
+    var request;
+    if (config.methodAndCache === 'GET-NOCACHE') {
+      request = new Request(config.prefixUrl + '_receive_getnocache?' + size,
+                            {method: 'GET'});
+    } else if (config.methodAndCache === 'GET-CACHE') {
+      request = new Request(config.prefixUrl + '_receive_getcache?' + size,
+                            {method: 'GET'});
+    } else {
+      request = new Request(config.prefixUrl + '_receive',
+                            {method: 'POST', body: size + ' none'});
+    }
+    var promise = fetch(request)
+        .then(function(response) {
+          if (response.status != 200) {
+            config.addToLog('Failed (status=' + this.status + ')');
+            return Promise.reject();
+          }
+          // Check and warn if proxy is enabled.
+          if (response.headers.get('Via') !== null) {
+            config.addToLog('WARNING: proxy seems enabled.');
+          }
+          if (config.dataType === 'arraybuffer') {
+            return response.arrayBuffer()
+                .then(function(arrayBuffer) {
+                  return [arrayBuffer.byteLength,
+                          (!config.verifyData ||
+                           verifyArrayBuffer(arrayBuffer, 0x61))];
+                });
+          } else if (config.dataType == 'blob') {
+            return response.blob()
+                .then(function(blob) {
+                  return new Promise(function(resolve, reject) {
+                      if (config.verifyData) {
+                        verifyBlob(config, blob, 0x61,
+                            function(receivedSize, verificationResult) {
+                              resolve([receivedSize, verificationResult]);
+                            });
+                      } else {
+                        resolve([blob.size, true]);
+                      }
+                    });
+                });
+          } else {
+            return response.text()
+                .then(function(text) {
+                  return [text.length,
+                          (!config.verifyData ||
+                           text == repeatString('a', text.length))];
+                });
+          }
+        })
+        .then(function(receivedSizeAndVerificationResult) {
+          var receivedSize = receivedSizeAndVerificationResult[0];
+          var verificationResult = receivedSizeAndVerificationResult[1];
+          if (receivedSize !== size) {
+            config.addToLog('Expected ' + size +
+                            'B but received ' + receivedSize + 'B');
+            return Promise.reject();
+          }
+          if (!verificationResult) {
+            config.addToLog('Response verification failed');
+            return Promise.reject();
+          }
+        });
+    promises.push(promise);
+  }
+
+  // Finish and report time measuring.
+  Promise.all(promises)
+      .then(function() {
+        if (benchmark.startTimeInMs == null) {
+          config.addToLog('startTimeInMs not set');
+          return Promise.reject();
+        }
+        calculateAndLogResult(config, size, benchmark.startTimeInMs,
+                              size * config.numFetches, isWarmUp);
+        runNextTask(config);
+      })
+      .catch(function(e) {
+        config.addToLog("ERROR: " + e);
+        config.notifyAbort();
+      });
+}
+
+
+function getConfigString(config) {
+  return '(' + config.dataType +
+    ', verifyData=' + config.verifyData +
+    ', ' + (isWorker ? 'Worker' : 'Main') +
+    ', numFetches=' + config.numFetches +
+    ', numIterations=' + config.numIterations +
+    ', numWarmUpIterations=' + config.numWarmUpIterations +
+    ')';
+}
+
+function startBenchmark(config) {
+  clearTimeout(timerID);
+
+  runNextTask(config);
+}
+
+function batchBenchmark(originalConfig) {
+  originalConfig.addToLog('Batch benchmark');
+
+  tasks = [];
+  clearAverageData();
+
+  var dataTypes = ['text', 'blob', 'arraybuffer'];
+  var stepFuncs = [sendBenchmarkStep, receiveBenchmarkStep];
+  var names = ['Send', 'Receive'];
+  for (var i = 0; i < stepFuncs.length; ++i) {
+    for (var j = 0; j < dataTypes.length; ++j) {
+      var config = cloneConfig(originalConfig);
+      config.dataType = dataTypes[j];
+      addTasks(config, stepFuncs[i]);
+      addResultReportingTask(config,
+          names[i] + ' benchmark ' + getConfigString(config));
+    }
+  }
+
+  startBenchmark(config);
+}
+
+function cleanup() {
+}
diff --git a/src/example/util.js b/src/example/util.js
index de60509..9aa7af6 100644
--- a/src/example/util.js
+++ b/src/example/util.js
@@ -94,6 +94,26 @@
       config.printSize));
 }
 
+function repeatString(str, count) {
+  var data = '';
+  var expChunk = str;
+  var remain = count;
+  while (true) {
+    if (remain % 2) {
+      data += expChunk;
+      remain = (remain - 1) / 2;
+    } else {
+      remain /= 2;
+    }
+
+    if (remain == 0)
+      break;
+
+    expChunk = expChunk + expChunk;
+  }
+  return data;
+}
+
 function fillArrayBuffer(buffer, c) {
   var i;
 
@@ -178,3 +198,124 @@
   }
   return newObj;
 }
+
+var tasks = [];
+
+function runNextTask(config) {
+  var task = tasks.shift();
+  if (task == undefined) {
+    config.addToLog('Finished');
+    cleanup();
+    return;
+  }
+  timerID = setTimeout(task, 0);
+}
+
+function buildLegendString(config) {
+  var legend = ''
+  if (config.printSize)
+    legend = 'Message size in KiB, Time/message in ms, ';
+  legend += 'Speed in kB/s';
+  return legend;
+}
+
+function addTasks(config, stepFunc) {
+  for (var i = 0;
+      i < config.numWarmUpIterations + config.numIterations; ++i) {
+    var multiplierIndex = 0;
+    for (var size = config.startSize;
+         size <= config.stopThreshold;
+         ++multiplierIndex) {
+      var task = stepFunc.bind(
+          null,
+          size,
+          config,
+          i < config.numWarmUpIterations);
+      tasks.push(task);
+      size *= config.multipliers[
+          multiplierIndex % config.multipliers.length];
+    }
+  }
+}
+
+function addResultReportingTask(config, title) {
+  tasks.push(function(){
+      timerID = null;
+      config.addToSummary(title);
+      reportAverageData(config);
+      clearAverageData();
+      runNextTask(config);
+  });
+}
+
+function sendBenchmark(config) {
+  config.addToLog('Send benchmark');
+  config.addToLog(buildLegendString(config));
+
+  tasks = [];
+  clearAverageData();
+  addTasks(config, sendBenchmarkStep);
+  addResultReportingTask(config, 'Send Benchmark ' + getConfigString(config));
+  startBenchmark(config);
+}
+
+function receiveBenchmark(config) {
+  config.addToLog('Receive benchmark');
+  config.addToLog(buildLegendString(config));
+
+  tasks = [];
+  clearAverageData();
+  addTasks(config, receiveBenchmarkStep);
+  addResultReportingTask(config,
+      'Receive Benchmark ' + getConfigString(config));
+  startBenchmark(config);
+}
+
+function stop(config) {
+  clearTimeout(timerID);
+  timerID = null;
+  tasks = [];
+  config.addToLog('Stopped');
+  cleanup();
+}
+
+var worker;
+
+function initWorker(connectionType, origin) {
+  var scriptPath =
+    connectionType === 'WebSocket' ? '/benchmark.js' :
+    connectionType === 'XHR' ? '/xhr_benchmark.js' :
+    '/fetch_benchmark.js'; // connectionType === 'fetch'
+  worker = new Worker(origin + scriptPath);
+}
+
+function doAction(config, isWindowToWorker, action) {
+  if (isWindowToWorker) {
+    worker.onmessage = function(addToLog, addToSummary,
+                                measureValue, notifyAbort, message) {
+      if (message.data.type === 'addToLog')
+        addToLog(message.data.data);
+      else if (message.data.type === 'addToSummary')
+        addToSummary(message.data.data);
+      else if (message.data.type === 'measureValue')
+        measureValue(message.data.data);
+      else if (message.data.type === 'notifyAbort')
+        notifyAbort();
+    }.bind(undefined, config.addToLog, config.addToSummary,
+           config.measureValue, config.notifyAbort);
+    config.addToLog = undefined;
+    config.addToSummary = undefined;
+    config.measureValue = undefined;
+    config.notifyAbort = undefined;
+    worker.postMessage({type: action, config: config});
+  } else {
+    if (action === 'sendBenchmark')
+      sendBenchmark(config);
+    else if (action === 'receiveBenchmark')
+      receiveBenchmark(config);
+    else if (action === 'batchBenchmark')
+      batchBenchmark(config);
+    else if (action === 'stop')
+      stop(config);
+  }
+}
diff --git a/src/example/util_main.js b/src/example/util_main.js
index 29df589..ac0612c 100644
--- a/src/example/util_main.js
+++ b/src/example/util_main.js
@@ -58,14 +58,3 @@
   var strArray = document.getElementById(id).value.split(',');
   return strArray.map(function(str) { return parseInt(str, 10); });
 }
-
-function onMessage(message) {
-  if (message.data.type === 'addToLog')
-    addToLog(message.data.data);
-  else if (message.data.type === 'addToSummary')
-    addToSummary(message.data.data);
-  else if (message.data.type === 'measureValue')
-    measureValue(message.data.data);
-  else if (message.data.type === 'notifyAbort')
-    notifyAbort();
-}
diff --git a/src/example/util_worker.js b/src/example/util_worker.js
index be346e6..98dcf44 100644
--- a/src/example/util_worker.js
+++ b/src/example/util_worker.js
@@ -6,18 +6,15 @@
 
 // Utilities for example applications (for the worker threads only).
 
-function workerAddToLog(text) {
-  postMessage({type: 'addToLog', data: text});
-}
+onmessage = function (message) {
+  var config = message.data.config;
+  config.addToLog = function(text) {
+      postMessage({type: 'addToLog', data: text}); };
+  config.addToSummary = function(text) {
+      postMessage({type: 'addToSummary', data: text}); };
+  config.measureValue = function(value) {
+      postMessage({type: 'measureValue', data: value}); };
+  config.notifyAbort = function() { postMessage({type: 'notifyAbort'}); };
 
-function workerAddToSummary(text) {
-  postMessage({type: 'addToSummary', data: text});
-}
-
-function workerMeasureValue(value) {
-  postMessage({type: 'measureValue', data: value});
-}
-
-function workerNotifyAbort() {
-  postMessage({type: 'notifyAbort'});
-}
+  doAction(config, false, message.data.type);
+};
diff --git a/src/example/xhr_benchmark.html b/src/example/xhr_benchmark.html
index 096aea2..a99919e 100644
--- a/src/example/xhr_benchmark.html
+++ b/src/example/xhr_benchmark.html
@@ -56,69 +56,34 @@
     // minTotal is not yet implemented on XHR benchmark
     multipliers: getIntArrayFromInput('multipliers'),
     verifyData: getBoolFromCheckBox('verifydata'),
-    methodAndCache: getStringFromRadioBox('methodandcache')
+    methodAndCache: getStringFromRadioBox('methodandcache'),
+    addToLog: addToLog,
+    addToSummary: addToSummary,
+    measureValue: measureValue,
+    notifyAbort: notifyAbort
   };
 }
 
-var worker = new Worker('xhr_benchmark.js');
-worker.onmessage = onMessage;
-
 function onSendBenchmark() {
   var config = getConfig();
   config.dataType = getStringFromRadioBox('datatyperadio');
-
-  if (getBoolFromCheckBox('worker')) {
-    worker.postMessage({type: 'sendBenchmark', config: config});
-  } else {
-    config.addToLog = addToLog;
-    config.addToSummary = addToSummary;
-    config.measureValue = measureValue;
-    config.notifyAbort = notifyAbort;
-    sendBenchmark(config);
-  }
+  doAction(config, getBoolFromCheckBox('worker'), 'sendBenchmark');
 }
 
 function onReceiveBenchmark() {
   var config = getConfig();
   config.dataType = getStringFromRadioBox('datatyperadio');
-
-  if (getBoolFromCheckBox('worker')) {
-    worker.postMessage({type: 'receiveBenchmark', config: config});
-  } else {
-    config.addToLog = addToLog;
-    config.addToSummary = addToSummary;
-    config.measureValue = measureValue;
-    config.notifyAbort = notifyAbort;
-    receiveBenchmark(config);
-  }
+  doAction(config, getBoolFromCheckBox('worker'), 'receiveBenchmark');
 }
 
 function onBatchBenchmark() {
   var config = getConfig();
-
-  if (getBoolFromCheckBox('worker')) {
-    worker.postMessage({type: 'batchBenchmark', config: config});
-  } else {
-    config.addToLog = addToLog;
-    config.addToSummary = addToSummary;
-    config.measureValue = measureValue;
-    config.notifyAbort = notifyAbort;
-    batchBenchmark(config);
-  }
+  doAction(config, getBoolFromCheckBox('worker'), 'batchBenchmark');
 }
 
 function onStop() {
   var config = getConfig();
-
-  if (getBoolFromCheckBox('worker')) {
-    worker.postMessage({type: 'stop', config: config});
-  } else {
-    config.addToLog = addToLog;
-    config.addToSummary = addToSummary;
-    config.measureValue = measureValue;
-    config.notifyAbort = notifyAbort;
-    stop(config);
-  }
+  doAction(config, getBoolFromCheckBox('worker'), 'stop');
 }
 
 function init() {
@@ -132,6 +97,8 @@
 
   addToLog(window.navigator.userAgent.toLowerCase());
   addToSummary(window.navigator.userAgent.toLowerCase());
+
+  initWorker('XHR', '');
 }
 </script>
 </head>
diff --git a/src/example/xhr_benchmark.js b/src/example/xhr_benchmark.js
index ea2168e..14a97e8 100644
--- a/src/example/xhr_benchmark.js
+++ b/src/example/xhr_benchmark.js
@@ -33,26 +33,6 @@
   // gc() might be needed for Chrome/Blob
 }
 
-function repeatString(str, count) {
-  var data = '';
-  var expChunk = str;
-  var remain = count;
-  while (true) {
-    if (remain % 2) {
-      data += expChunk;
-      remain = (remain - 1) / 2;
-    } else {
-      remain /= 2;
-    }
-
-    if (remain == 0)
-      break;
-
-    expChunk = expChunk + expChunk;
-  }
-  return data;
-}
-
 function sendBenchmarkStep(size, config, isWarmUp) {
   timerID = null;
 
@@ -276,83 +256,6 @@
   runNextTask(config);
 }
 
-// TODO(hiroshige): the following code is the same as benchmark.html
-// and some of them should be merged into e.g. util.js
-
-var tasks = [];
-
-function runNextTask(config) {
-  var task = tasks.shift();
-  if (task == undefined) {
-    config.addToLog('Finished');
-    destroyAllXHRs();
-    return;
-  }
-  timerID = setTimeout(task, 0);
-}
-
-function buildLegendString(config) {
-  var legend = ''
-  if (config.printSize)
-    legend = 'Message size in KiB, Time/message in ms, ';
-  legend += 'Speed in kB/s';
-  return legend;
-}
-
-function addTasks(config, stepFunc) {
-  for (var i = 0;
-      i < config.numWarmUpIterations + config.numIterations; ++i) {
-    var multiplierIndex = 0;
-    for (var size = config.startSize;
-         size <= config.stopThreshold;
-         ++multiplierIndex) {
-      var task = stepFunc.bind(
-          null,
-          size,
-          config,
-          i < config.numWarmUpIterations);
-      tasks.push(task);
-      size *= config.multipliers[
-          multiplierIndex % config.multipliers.length];
-    }
-  }
-}
-
-function addResultReportingTask(config, title) {
-  tasks.push(function(){
-      timerID = null;
-      config.addToSummary(title);
-      reportAverageData(config);
-      clearAverageData();
-      runNextTask(config);
-  });
-}
-
-// --------------------------------
-
-function sendBenchmark(config) {
-  config.addToLog('Send benchmark');
-  config.addToLog(buildLegendString(config));
-
-  tasks = [];
-  clearAverageData();
-  addTasks(config, sendBenchmarkStep);
-  addResultReportingTask(config, 'Send Benchmark ' + getConfigString(config));
-  startBenchmark(config);
-}
-
-function receiveBenchmark(config) {
-  config.addToLog('Receive benchmark');
-  config.addToLog(buildLegendString(config));
-
-  tasks = [];
-  clearAverageData();
-  addTasks(config, receiveBenchmarkStep);
-  addResultReportingTask(config,
-      'Receive Benchmark ' + getConfigString(config));
-  startBenchmark(config);
-}
-
 function batchBenchmark(originalConfig) {
   originalConfig.addToLog('Batch benchmark');
 
@@ -385,26 +288,5 @@
   startBenchmark(config);
 }
 
-
-function stop(config) {
-  destroyAllXHRs();
-  clearTimeout(timerID);
-  timerID = null;
-  config.addToLog('Stopped');
+function cleanup() {
 }
-
-onmessage = function (message) {
-  var config = message.data.config;
-  config.addToLog = workerAddToLog;
-  config.addToSummary = workerAddToSummary;
-  config.measureValue = workerMeasureValue;
-  config.notifyAbort = workerNotifyAbort;
-  if (message.data.type === 'sendBenchmark')
-    sendBenchmark(config);
-  else if (message.data.type === 'receiveBenchmark')
-    receiveBenchmark(config);
-  else if (message.data.type === 'batchBenchmark')
-    batchBenchmark(config);
-  else if (message.data.type === 'stop')
-    stop(config);
-};
diff --git a/src/mod_pywebsocket/xhr_benchmark_handler.py b/src/mod_pywebsocket/xhr_benchmark_handler.py
index aa2e84a..c62f154 100644
--- a/src/mod_pywebsocket/xhr_benchmark_handler.py
+++ b/src/mod_pywebsocket/xhr_benchmark_handler.py
@@ -42,6 +42,7 @@
         response_body = '%d' % content_length
         self.wfile.write(
             'HTTP/1.1 200 OK\r\n'
+            'Access-Control-Allow-Origin: *\r\n'
             'Content-Type: text/html\r\n'
             'Content-Length: %d\r\n'
             '\r\n%s' % (len(response_body), response_body))
@@ -83,6 +84,7 @@
         # Write a header
         response_header = (
             'HTTP/1.1 200 OK\r\n'
+            'Access-Control-Allow-Origin: *\r\n'
             'Content-Type: application/octet-stream\r\n')
         if enable_cache:
             response_header += 'Cache-Control: private, max-age=10\r\n'