Bring CLI speedtest into line with the features on the HTML Sepedtest webpage

- implemented exponential moving average
- variable end condition basedon moving averages as per the Web page
- added a Task abstraction for running tests and other tasks
- added progress updates as an option
- full override options for all speedtest parameters

Change-Id: I1a4429993a7b4de9247e2654eca2c13ac5804769
diff --git a/speedtest/Makefile b/speedtest/Makefile
index b3b625c..af5ea27 100644
--- a/speedtest/Makefile
+++ b/speedtest/Makefile
@@ -4,7 +4,8 @@
 BINDIR=$(PREFIX)/bin
 DEBUG?=-g
 WARNINGS=-Wall -Werror -Wno-unused-result -Wno-unused-but-set-variable
-CXXFLAGS=$(DEBUG) $(WARNINGS) -O3 -DNDEBUG -std=c++11 $(EXTRACFLAGS)
+CXXFLAGS=$(DEBUG) $(WARNINGS) -DNDEBUG -std=c++11 $(EXTRACFLAGS)
+#CXXFLAGS=$(DEBUG) $(WARNINGS) -O3 -DNDEBUG -std=c++11 $(EXTRACFLAGS)
 LDFLAGS=$(DEBUG) $(EXTRALDFLAGS)
 
 GTEST_DIR=googletest
@@ -15,16 +16,18 @@
 TOBJS=curl_env.o url.o errors.o request.o utils.o
 OBJS=config.o \
      curl_env.o \
-     download_test.o \
+     download_task.o \
      errors.o \
-     generic_test.o \
+     http_task.o \
      options.o \
-     ping_test.o \
+     ping_task.o \
      request.o \
-     runner.o \
      speedtest.o \
-     transfer_test.o \
-     upload_test.o \
+     task.o \
+     timed_runner.o \
+     transfer_runner.o \
+     transfer_task.o \
+     upload_task.o \
      url.o \
      utils.o
 
@@ -33,27 +36,30 @@
 config.o: config.cc config.h url.h
 errors.o: errors.cc errors.h
 curl_env.o: curl_env.cc curl_env.h errors.h request.h
-download_test.o: download_test.cc download_test.h generic_test.h transfer_test.h utils.h
-generic_test.o: generic_test.cc generic_test.h request.h utils.h
+download_task.o: download_task.cc download_task.h transfer_task.h utils.h
+http_task.o: http_task.cc http_task.h
 options.o: options.cc options.h url.h
-ping_test.o: ping_test.cc ping_test.h generic_test.h request.h url.h utils.h
+ping_task.o: ping_task.cc ping_task.h http_task.h request.h url.h utils.h
 request.o: request.cc request.h url.h
-runner.o: runner.cc runner.h generic_test.h transfer_test.h
 speedtest.o: speedtest.cc \
              speedtest.h \
              config.h \
              curl_env.h \
-             download_test.h \
-             generic_test.h \
+             download_task.h \
              options.h \
-             ping_test.h \
+             ping_task.h \
              request.h \
-             runner.h \
-             upload_test.h \
+             task.h \
+             timed_runner.h \
+             transfer_runner.h \
+             upload_task.h \
              url.h
 speedtest_main.o: speedtest_main.cc options.h speedtest.h
-transfer_test.o: transfer_test.cc transfer_test.h generic_test.h
-upload_test.o: upload_test.cc upload_test.h generic_test.h transfer_test.h utils.h
+task.o: task.cc task.h utils.h
+timed_runner.o: timed_runner.cc timed_runner.h task.h
+transfer_runner.o: transfer_runner.cc transfer_runner.h transfer_task.h utils.h
+transfer_task.o: transfer_task.cc transfer_task.h http_task.h
+upload_task.o: upload_task.cc upload_task.h transfer_task.h utils.h
 utils.o: utils.cc options.h
 url.o: url.cc url.h utils.h
 
diff --git a/speedtest/config.cc b/speedtest/config.cc
index d12b4e5..aede959 100644
--- a/speedtest/config.cc
+++ b/speedtest/config.cc
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015 Google Inc. All rights reserved.
+ * Copyright 2016 Google Inc. All rights reserved.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -35,7 +35,7 @@
 
   config->download_size = root["downloadSize"].asInt();
   config->upload_size = root["uploadSize"].asInt();
-  config->interval_size = root["intervalSize"].asInt();
+  config->interval_millis = root["intervalSize"].asInt();
   config->location_name = root["locationName"].asString();
   config->min_transfer_intervals = root["minTransferIntervals"].asInt();
   config->max_transfer_intervals = root["maxTransferIntervals"].asInt();
@@ -79,7 +79,7 @@
 void PrintConfig(std::ostream &out, const Config &config) {
   out << "Download size: " << config.download_size << " bytes\n"
       << "Upload size: " << config.upload_size << " bytes\n"
-      << "Interval size: " << config.interval_size << " ms\n"
+      << "Interval size: " << config.interval_millis << " ms\n"
       << "Location name: " << config.location_name << "\n"
       << "Min transfer intervals: " << config.min_transfer_intervals << "\n"
       << "Max transfer intervals: " << config.max_transfer_intervals << "\n"
diff --git a/speedtest/config.h b/speedtest/config.h
index 01b5483..2988484 100644
--- a/speedtest/config.h
+++ b/speedtest/config.h
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015 Google Inc. All rights reserved.
+ * Copyright 2016 Google Inc. All rights reserved.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -27,7 +27,7 @@
 struct Config {
   int download_size = 0;
   int upload_size = 0;
-  int interval_size = 0;
+  int interval_millis = 0;
   std::string location_name;
   int min_transfer_intervals = 0;
   int max_transfer_intervals = 0;
diff --git a/speedtest/config_test.cc b/speedtest/config_test.cc
index 681e0fc..5924921 100644
--- a/speedtest/config_test.cc
+++ b/speedtest/config_test.cc
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015 Google Inc. All rights reserved.
+ * Copyright 2016 Google Inc. All rights reserved.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -86,7 +86,7 @@
   EXPECT_EQ(20000000, config.upload_size);
   EXPECT_EQ(20, config.num_downloads);
   EXPECT_EQ(15, config.num_uploads);
-  EXPECT_EQ(200, config.interval_size);
+  EXPECT_EQ(200, config.interval_millis);
   EXPECT_EQ("Kansas City", config.location_name);
   EXPECT_EQ(10, config.min_transfer_intervals);
   EXPECT_EQ(25, config.max_transfer_intervals);
diff --git a/speedtest/curl_env.cc b/speedtest/curl_env.cc
index 4a49b99..eed370f 100644
--- a/speedtest/curl_env.cc
+++ b/speedtest/curl_env.cc
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015 Google Inc. All rights reserved.
+ * Copyright 2016 Google Inc. All rights reserved.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/speedtest/curl_env.h b/speedtest/curl_env.h
index 85a34ba..6a70f28 100644
--- a/speedtest/curl_env.h
+++ b/speedtest/curl_env.h
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015 Google Inc. All rights reserved.
+ * Copyright 2016 Google Inc. All rights reserved.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/speedtest/download_test.cc b/speedtest/download_task.cc
similarity index 79%
rename from speedtest/download_test.cc
rename to speedtest/download_task.cc
index f23d97a..a643725 100644
--- a/speedtest/download_test.cc
+++ b/speedtest/download_task.cc
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015 Google Inc. All rights reserved.
+ * Copyright 2016 Google Inc. All rights reserved.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,25 +14,24 @@
  * limitations under the License.
  */
 
-#include "download_test.h"
+#include "download_task.h"
 
 #include <algorithm>
 #include <cassert>
 #include <iostream>
 #include <thread>
-#include "generic_test.h"
 #include "utils.h"
 
 namespace speedtest {
 
-DownloadTest::DownloadTest(const Options &options)
-    : TransferTest(options_),
+DownloadTask::DownloadTask(const Options &options)
+    : TransferTask(options_),
       options_(options) {
   assert(options_.num_transfers > 0);
   assert(options_.download_size > 0);
 }
 
-void DownloadTest::RunInternal() {
+void DownloadTask::RunInternal() {
   ResetCounters();
   threads_.clear();
   if (options_.verbose) {
@@ -46,15 +45,15 @@
   }
 }
 
-void DownloadTest::StopInternal() {
+void DownloadTask::StopInternal() {
   std::for_each(threads_.begin(), threads_.end(), [](std::thread &t) {
     t.join();
   });
 }
 
-void DownloadTest::RunDownload(int id) {
-  GenericTest::RequestPtr download = options_.request_factory(id);
-  while (GetStatus() == TestStatus::RUNNING) {
+void DownloadTask::RunDownload(int id) {
+  http::Request::Ptr download = options_.request_factory(id);
+  while (GetStatus() == TaskStatus::RUNNING) {
     long downloaded = 0;
     download->set_param("i", to_string(id));
     download->set_param("size", to_string(options_.download_size));
@@ -67,7 +66,7 @@
         TransferBytes(dlnow - downloaded);
         downloaded = dlnow;
       }
-      return GetStatus() != TestStatus::RUNNING;
+      return GetStatus() != TaskStatus::RUNNING;
     });
     StartRequest();
     download->Get();
diff --git a/speedtest/download_test.h b/speedtest/download_task.h
similarity index 68%
rename from speedtest/download_test.h
rename to speedtest/download_task.h
index f8a249f..2b65478 100644
--- a/speedtest/download_test.h
+++ b/speedtest/download_task.h
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015 Google Inc. All rights reserved.
+ * Copyright 2016 Google Inc. All rights reserved.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,22 +14,22 @@
  * limitations under the License.
  */
 
-#ifndef SPEEDTEST_DOWNLOAD_TEST_H
-#define SPEEDTEST_DOWNLOAD_TEST_H
+#ifndef SPEEDTEST_DOWNLOAD_TASK_H
+#define SPEEDTEST_DOWNLOAD_TASK_H
 
 #include <thread>
 #include <vector>
-#include "transfer_test.h"
+#include "transfer_task.h"
 
 namespace speedtest {
 
-class DownloadTest : public TransferTest {
+class DownloadTask : public TransferTask {
  public:
-  struct Options : TransferTest::Options {
+  struct Options : TransferTask::Options {
     int download_size = 0;
   };
 
-  explicit DownloadTest(const Options &options);
+  explicit DownloadTask(const Options &options);
 
  protected:
   void RunInternal() override;
@@ -42,10 +42,10 @@
   std::vector<std::thread> threads_;
 
   // disallowed
-  DownloadTest(const DownloadTest &) = delete;
-  void operator=(const DownloadTest &) = delete;
+  DownloadTask(const DownloadTask &) = delete;
+  void operator=(const DownloadTask &) = delete;
 };
 
 }  // namespace speedtest
 
-#endif  // SPEEDTEST_DOWNLOAD_TEST_H
+#endif  // SPEEDTEST_DOWNLOAD_TASK_H
diff --git a/speedtest/errors.cc b/speedtest/errors.cc
index 05f382b..43c94f8 100644
--- a/speedtest/errors.cc
+++ b/speedtest/errors.cc
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015 Google Inc. All rights reserved.
+ * Copyright 2016 Google Inc. All rights reserved.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/speedtest/errors.h b/speedtest/errors.h
index 334689d..4c70003 100644
--- a/speedtest/errors.h
+++ b/speedtest/errors.h
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015 Google Inc. All rights reserved.
+ * Copyright 2016 Google Inc. All rights reserved.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/speedtest/generic_test.cc b/speedtest/generic_test.cc
deleted file mode 100644
index be012ba..0000000
--- a/speedtest/generic_test.cc
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * Copyright 2015 Google Inc. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include "generic_test.h"
-
-#include <cassert>
-#include "utils.h"
-
-namespace speedtest {
-
-const char *AsString(TestStatus status) {
-  switch (status) {
-    case TestStatus::NOT_STARTED: return "NOT_STARTED";
-    case TestStatus::RUNNING: return "RUNNING";
-    case TestStatus::STOPPING: return "STOPPING";
-    case TestStatus::STOPPED: return "STOPPED";
-  }
-  std::exit(1);
-}
-
-GenericTest::GenericTest(const Options &options)
-    : status_(TestStatus::NOT_STARTED) {
-  assert(options.request_factory);
-}
-
-void GenericTest::Run() {
-  {
-    std::lock_guard <std::mutex> lock(mutex_);
-    if (status_ != TestStatus::NOT_STARTED &&
-        status_ != TestStatus::STOPPED) {
-      return;
-    }
-    status_ = TestStatus::RUNNING;
-    start_time_ = SystemTimeMicros();
-  }
-  RunInternal();
-}
-
-void GenericTest::Stop() {
-  {
-    std::lock_guard <std::mutex> lock(mutex_);
-    if (status_ != TestStatus::RUNNING) {
-      return;
-    }
-    status_ = TestStatus::STOPPING;
-  }
-  StopInternal();
-  std::lock_guard <std::mutex> lock(mutex_);
-  status_ = TestStatus::STOPPED;
-  end_time_ = SystemTimeMicros();
-}
-
-TestStatus GenericTest::GetStatus() const {
-  std::lock_guard <std::mutex> lock(mutex_);
-  return status_;
-}
-
-long GenericTest::GetRunningTime() const {
-  std::lock_guard <std::mutex> lock(mutex_);
-  switch (status_) {
-    case TestStatus::NOT_STARTED:
-      break;
-    case TestStatus::RUNNING:
-    case TestStatus::STOPPING:
-      return SystemTimeMicros() - start_time_;
-    case TestStatus::STOPPED:
-      return end_time_ - start_time_;
-  }
-  return 0;
-}
-
-void GenericTest::WaitForEnd() {
-  std::unique_lock<std::mutex> lock(mutex_);
-  condition_.wait(lock, [this]{
-    return status_ == TestStatus::STOPPED;
-  });
-}
-
-}  // namespace speedtest
diff --git a/speedtest/runner.h b/speedtest/http_task.cc
similarity index 74%
copy from speedtest/runner.h
copy to speedtest/http_task.cc
index 4d677fc..1275aa4 100644
--- a/speedtest/runner.h
+++ b/speedtest/http_task.cc
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015 Google Inc. All rights reserved.
+ * Copyright 2016 Google Inc. All rights reserved.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,15 +14,11 @@
  * limitations under the License.
  */
 
-#ifndef SPEEDTEST_RUNNER_H
-#define SPEEDTEST_RUNNER_H
-
-#include "generic_test.h"
+#include "http_task.h"
 
 namespace speedtest {
 
-void TimedRun(GenericTest *test, long millis);
+HttpTask::HttpTask(const Options &options): Task(options) {
+}
 
 }  // namespace speedtest
-
-#endif  // SPEEDTEST_RUNNER_H
diff --git a/speedtest/http_task.h b/speedtest/http_task.h
new file mode 100644
index 0000000..a54e4ba
--- /dev/null
+++ b/speedtest/http_task.h
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2016 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SPEEDTEST_HTTP_TASK_H
+#define SPEEDTEST_HTTP_TASK_H
+
+#include "task.h"
+
+#include "request.h"
+
+namespace speedtest {
+
+class HttpTask : public Task {
+ public:
+  struct Options : Task::Options {
+    bool verbose = false;
+    std::function<http::Request::Ptr(int)> request_factory;
+  };
+
+  explicit HttpTask(const Options &options);
+
+ private:
+  // disallowed
+  HttpTask(const Task &) = delete;
+  void operator=(const HttpTask &) = delete;
+};
+
+}  // namespace speedtest
+
+#endif  // SPEEDTEST_HTTP_TASK_H
diff --git a/speedtest/options.cc b/speedtest/options.cc
index db7f3ae..133b857 100644
--- a/speedtest/options.cc
+++ b/speedtest/options.cc
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015 Google Inc. All rights reserved.
+ * Copyright 2016 Google Inc. All rights reserved.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -29,10 +29,19 @@
 
 namespace {
 
-bool ParseLong(const char *s, char **endptr, long *size) {
-  assert(s != nullptr);
-  assert(size != nullptr);
-  *size = strtol(s, endptr, 10);
+bool ParseLong(const char *s, char **endptr, long *number) {
+  assert(s);
+  assert(endptr);
+  assert(number != nullptr);
+  *number = strtol(s, endptr, 10);
+  return !**endptr;
+}
+
+bool ParseDouble(const char *s, char **endptr, double *number) {
+  assert(s);
+  assert(endptr);
+  assert(number != nullptr);
+  *number = strtod(s, endptr);
   return !**endptr;
 }
 
@@ -58,29 +67,47 @@
   return true;
 }
 
-const int kOptMinTransferTime = 1000;
-const int kOptMaxTransferTime = 1001;
-const int kOptPingRuntime = 1002;
-const int kOptPingTimeout = 1003;
-const int kOptDisableDnsCache = 1004;
-const int kOptMaxConnections = 1005;
+const int kOptDisableDnsCache = 1000;
+const int kOptMaxConnections = 1001;
+const int kOptExponentialMovingAverage = 1002;
+
+const int kOptMinTransferTime = 1100;
+const int kOptMaxTransferTime = 1101;
+const int kOptMinTransferIntervals = 1102;
+const int kOptMaxTransferIntervals = 1103;
+const int kOptMaxTransferVariance = 1104;
+const int kOptIntervalMillis = 1105;
+const int kOptPingRuntime = 1106;
+const int kOptPingTimeout = 1107;
+
 const char *kShortOpts = "hvg:a:d:s:t:u:p:";
+
 struct option kLongOpts[] = {
     {"help", no_argument, nullptr, 'h'},
     {"verbose", no_argument, nullptr, 'v'},
     {"global_host", required_argument, nullptr, 'g'},
     {"user_agent", required_argument, nullptr, 'a'},
+    {"disable_dns_cache", no_argument, nullptr, kOptDisableDnsCache},
+    {"max_connections", required_argument, nullptr, kOptMaxConnections},
+    {"progress_millis", required_argument, nullptr, 'p'},
+    {"exponential_moving_average", no_argument, nullptr,
+        kOptExponentialMovingAverage},
+
     {"num_downloads", required_argument, nullptr, 'd'},
     {"download_size", required_argument, nullptr, 's'},
     {"num_uploads", required_argument, nullptr, 'u'},
     {"upload_size", required_argument, nullptr, 't'},
-    {"progress", required_argument, nullptr, 'p'},
-    {"min_transfer_time", required_argument, nullptr, kOptMinTransferTime},
-    {"max_transfer_time", required_argument, nullptr, kOptMaxTransferTime},
+    {"min_transfer_runtime", required_argument, nullptr, kOptMinTransferTime},
+    {"max_transfer_runtime", required_argument, nullptr, kOptMaxTransferTime},
+    {"min_transfer_intervals", required_argument, nullptr,
+        kOptMinTransferIntervals},
+    {"max_transfer_intervals", required_argument, nullptr,
+        kOptMaxTransferIntervals},
+    {"max_transfer_variance", required_argument, nullptr,
+        kOptMaxTransferVariance},
+    {"interval_millis", required_argument, nullptr, kOptIntervalMillis},
     {"ping_runtime", required_argument, nullptr, kOptPingRuntime},
     {"ping_timeout", required_argument, nullptr, kOptPingTimeout},
-    {"disable_dns_cache", no_argument, nullptr, kOptDisableDnsCache},
-    {"max_connections", required_argument, nullptr, kOptMaxConnections},
     {nullptr, 0, nullptr, 0},
 };
 const int kMaxNumber = 1000;
@@ -95,23 +122,28 @@
 will be used without pinging.
 
 Usage: speedtest [options] [host ...]
- -h, --help                This help text
- -v, --verbose             Verbose output
- -g, --global_host URL     Global host URL
- -a, --user_agent AGENT    User agent string for HTTP requests
- --disable_dns_cache       Disable global DNS cache
- --max_connections NUM     Maximum number of parallel connections
+ -h, --help                    This help text
+ -v, --verbose                 Verbose output
+ -g, --global_host URL         Global host URL
+ -a, --user_agent AGENT        User agent string for HTTP requests
+ -p, --progress_millis NUM     Delay in milliseconds between updates
+ --disable_dns_cache           Disable global DNS cache
+ --max_connections NUM         Maximum number of parallel connections
+ --exponential_moving_average  Use exponential instead of simple moving average
 
 These options override the speedtest config parameters:
- -d, --num_downloads NUM   Number of simultaneous downloads
- -p, --progress TIME       Progress intervals in milliseconds
- -s, --download_size SIZE  Download size in bytes
- -t, --upload_size SIZE    Upload size in bytes
- -u, --num_uploads NUM     Number of simultaneous uploads
- --min_transfer_time TIME  Minimum transfer time in milliseconds
- --max_transfer_time TIME  Maximum transfer time in milliseconds
- --ping_time TIME          Ping time in milliseconds
- --ping_timeout TIME       Ping timeout in milliseconds;
+ -d, --num_downloads NUM       Number of simultaneous downloads
+ -s, --download_size SIZE      Download size in bytes
+ -t, --upload_size SIZE        Upload size in bytes
+ -u, --num_uploads NUM         Number of simultaneous uploads
+ --min_transfer_runtime TIME   Minimum transfer time in milliseconds
+ --max_transfer_runtime TIME   Maximum transfer time in milliseconds
+ --min_transfer_intervals NUM  Short moving average intervals
+ --max_transfer_intervals NUM  Long moving average intervals
+ --max_transfer_variance NUM   Max difference between moving averages
+ --interval_millis TIME        Interval size in milliseconds
+ --ping_runtime TIME           Ping runtime in milliseconds
+ --ping_timeout TIME           Ping timeout in milliseconds
 )USAGE";
 
 }  // namespace
@@ -122,17 +154,25 @@
   options->verbose = false;
   options->global_host = http::Url(kDefaultHost);
   options->global = false;
+  options->user_agent = "";
+  options->progress_millis = 0;
   options->disable_dns_cache = false;
   options->max_connections = 0;
+  options->exponential_moving_average = false;
+
   options->num_downloads = 0;
   options->download_size = 0;
   options->num_uploads = 0;
   options->upload_size = 0;
-  options->progress_millis = 0;
-  options->min_transfer_time = 0;
-  options->max_transfer_time = 0;
+  options->min_transfer_runtime = 0;
+  options->max_transfer_runtime = 0;
+  options->min_transfer_intervals = 0;
+  options->max_transfer_intervals = 0;
+  options->max_transfer_variance = 0.0;
+  options->interval_millis = 0;
   options->ping_runtime = 0;
   options->ping_timeout = 0;
+
   options->hosts.clear();
 
   if (!options->global_host.ok()) {
@@ -221,6 +261,27 @@
       case 'v':
         options->verbose = true;
         break;
+      case kOptDisableDnsCache:
+        options->disable_dns_cache = true;
+        break;
+      case kOptMaxConnections: {
+        long max_connections;
+        char *endptr;
+        if (!ParseLong(optarg, &endptr, &max_connections)) {
+          std::cerr << "Could not parse max connections '" << optarg << "'\n";
+          return false;
+        }
+        if (max_connections < 0) {
+          std::cerr << "Max connections must be nonnegative, got "
+          << optarg << "'\n";
+          return false;
+        }
+        options->max_connections = static_cast<int>(max_connections);
+        break;
+      }
+      case kOptExponentialMovingAverage:
+        options->exponential_moving_average = true;
+        break;
       case kOptMinTransferTime: {
         long transfer_time;
         char *endptr;
@@ -230,11 +291,11 @@
           return false;
         }
         if (transfer_time < 0) {
-          std::cerr << "Minimum transfer time must be nonnegative, got "
+          std::cerr << "Minimum transfer runtime must be nonnegative, got "
                     << optarg << "'\n";
           return false;
         }
-        options->min_transfer_time = static_cast<int>(transfer_time);
+        options->min_transfer_runtime = static_cast<int>(transfer_time);
         break;
       }
       case kOptMaxTransferTime: {
@@ -246,11 +307,72 @@
           return false;
         }
         if (transfer_time < 0) {
-          std::cerr << "Maximum transfer must be nonnegative, got "
+          std::cerr << "Maximum transfer runtime must be nonnegative, got "
                     << optarg << "'\n";
           return false;
         }
-        options->max_transfer_time = static_cast<int>(transfer_time);
+        options->max_transfer_runtime = static_cast<int>(transfer_time);
+        break;
+      }
+      case kOptMinTransferIntervals: {
+        long intervals;
+        char *endptr;
+        if (!ParseLong(optarg, &endptr, &intervals)) {
+          std::cerr << "Could not parse minimum transfer intervals '"
+                    << optarg << "'\n";
+          return false;
+        }
+        if (intervals < 0) {
+          std::cerr << "Minimum transfer intervals must be nonnegative, got "
+                    << optarg << "'\n";
+          return false;
+        }
+        options->min_transfer_intervals = static_cast<int>(intervals);
+        break;
+      }
+      case kOptMaxTransferIntervals: {
+        long intervals;
+        char *endptr;
+        if (!ParseLong(optarg, &endptr, &intervals)) {
+          std::cerr << "Could not parse maximum transfer intervals '"
+                    << optarg << "'\n";
+          return false;
+        }
+        if (intervals < 0) {
+          std::cerr << "Maximum transfer intervals must be nonnegative, got "
+                    << optarg << "'\n";
+          return false;
+        }
+        options->max_transfer_intervals = static_cast<int>(intervals);
+        break;
+      }
+      case kOptMaxTransferVariance: {
+        double variance;
+        char *endptr;
+        if (!ParseDouble(optarg, &endptr, &variance)) {
+          std::cerr << "Could not parse variance '" << optarg << "'\n";
+          return false;
+        }
+        if (variance < 0) {
+          std::cerr << "Variances must be nonnegative, got " << optarg << "'\n";
+          return false;
+        }
+        options->max_transfer_variance = variance;
+        break;
+      }
+      case kOptIntervalMillis: {
+        long interval_millis;
+        char *endptr;
+        if (!ParseLong(optarg, &endptr, &interval_millis)) {
+          std::cerr << "Could not parse interval time '" << optarg << "'\n";
+          return false;
+        }
+        if (interval_millis < 0) {
+          std::cerr << "Interval time must be nonnegative, got "
+                    << optarg << "'\n";
+          return false;
+        }
+        options->interval_millis = static_cast<int>(interval_millis);
         break;
       }
       case kOptPingRuntime: {
@@ -283,24 +405,6 @@
         options->ping_timeout = static_cast<int>(ping_timeout);
         break;
       }
-      case kOptDisableDnsCache:
-        options->disable_dns_cache = true;
-        break;
-      case kOptMaxConnections: {
-        long max_connections;
-        char *endptr;
-        if (!ParseLong(optarg, &endptr, &max_connections)) {
-          std::cerr << "Could not parse max connections '" << optarg << "'\n";
-          return false;
-        }
-        if (max_connections < 0) {
-          std::cerr << "Max connections must be nonnegative, got "
-                    << optarg << "'\n";
-          return false;
-        }
-        options->max_connections = static_cast<int>(max_connections);
-        break;
-      }
       default:
         return false;
     }
@@ -338,18 +442,24 @@
       << "Global host: " << options.global_host.url() << "\n"
       << "Global: " << (options.global ? "true" : "false") << "\n"
       << "User agent: " << options.user_agent << "\n"
+      << "Progress interval: " << options.progress_millis << " ms\n"
       << "Disable DNS cache: "
       << (options.disable_dns_cache ? "true" : "false") << "\n"
       << "Max connections: " << options.max_connections << "\n"
+      << "Exponential moving average: "
+      << (options.exponential_moving_average ? "true" : "false") << "\n"
       << "Number of downloads: " << options.num_downloads << "\n"
       << "Download size: " << options.download_size << " bytes\n"
       << "Number of uploads: " << options.num_uploads << "\n"
       << "Upload size: " << options.upload_size << " bytes\n"
-      << "Min transfer time: " << options.min_transfer_time << " ms\n"
-      << "Max transfer time: " << options.max_transfer_time << " ms\n"
+      << "Min transfer runtime: " << options.min_transfer_runtime << " ms\n"
+      << "Max transfer runtime: " << options.max_transfer_runtime << " ms\n"
+      << "Min transfer intervals: " << options.min_transfer_intervals << "\n"
+      << "Max transfer intervals: " << options.max_transfer_intervals << "\n"
+      << "Max transfer variance: " << options.max_transfer_variance << "\n"
+      << "Interval size: " << options.interval_millis << " ms\n"
       << "Ping runtime: " << options.ping_runtime << " ms\n"
       << "Ping timeout: " << options.ping_timeout << " ms\n"
-      << "Progress interval: " << options.progress_millis << " ms\n"
       << "Hosts:\n";
   for (const http::Url &host : options.hosts) {
     out << "  " << host.url() << "\n";
diff --git a/speedtest/options.h b/speedtest/options.h
index 0413798..9028f70 100644
--- a/speedtest/options.h
+++ b/speedtest/options.h
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015 Google Inc. All rights reserved.
+ * Copyright 2016 Google Inc. All rights reserved.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -27,24 +27,29 @@
 extern const char* kDefaultHost;
 
 struct Options {
-  bool verbose;
-  bool usage;
+  bool usage = false;
+  bool verbose = false;
   http::Url global_host;
-  bool global;
+  bool global = false;
   std::string user_agent;
-  bool disable_dns_cache;
-  int max_connections;
+  bool disable_dns_cache = false;
+  int max_connections = 0;
+  int progress_millis = 0;
+  bool exponential_moving_average = false;
 
   // A value of 0 means use the speedtest config parameters
-  int num_downloads;
-  long download_size;
-  int num_uploads;
-  long upload_size;
-  int progress_millis;
-  int min_transfer_time;
-  int max_transfer_time;
-  int ping_runtime;
-  int ping_timeout;
+  int num_downloads = 0;
+  long download_size = 0;
+  int num_uploads = 0;
+  long upload_size = 0;
+  int min_transfer_runtime = 0;
+  int max_transfer_runtime = 0;
+  int min_transfer_intervals = 0;
+  int max_transfer_intervals = 0;
+  double max_transfer_variance = 0.0;
+  int interval_millis = 0;
+  int ping_runtime = 0;
+  int ping_timeout = 0;
 
   std::vector<http::Url> hosts;
 };
diff --git a/speedtest/options_test.cc b/speedtest/options_test.cc
index 113f730..601984a 100644
--- a/speedtest/options_test.cc
+++ b/speedtest/options_test.cc
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015 Google Inc. All rights reserved.
+ * Copyright 2016 Google Inc. All rights reserved.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -83,19 +83,25 @@
 TEST(OptionsTest, Empty_ValidDefault) {
   Options options;
   TestValidOptions({}, &options);
+  EXPECT_FALSE(options.usage);
+  EXPECT_FALSE(options.verbose);
   EXPECT_TRUE(options.global);
   EXPECT_EQ(http::Url("any.speed.gfsvc.com"), options.global_host);
-  EXPECT_FALSE(options.verbose);
-  EXPECT_FALSE(options.usage);
   EXPECT_FALSE(options.disable_dns_cache);
   EXPECT_EQ(0, options.max_connections);
+  EXPECT_EQ(0, options.progress_millis);
+  EXPECT_FALSE(options.exponential_moving_average);
+
   EXPECT_EQ(0, options.num_downloads);
   EXPECT_EQ(0, options.download_size);
   EXPECT_EQ(0, options.num_uploads);
   EXPECT_EQ(0, options.upload_size);
-  EXPECT_EQ(0, options.progress_millis);
-  EXPECT_EQ(0, options.min_transfer_time);
-  EXPECT_EQ(0, options.max_transfer_time);
+  EXPECT_EQ(0, options.min_transfer_runtime);
+  EXPECT_EQ(0, options.max_transfer_runtime);
+  EXPECT_EQ(0, options.min_transfer_intervals);
+  EXPECT_EQ(0, options.max_transfer_intervals);
+  EXPECT_EQ(0, options.max_transfer_variance);
+  EXPECT_EQ(0, options.interval_millis);
   EXPECT_EQ(0, options.ping_runtime);
   EXPECT_EQ(0, options.ping_timeout);
   EXPECT_THAT(options.hosts, testing::IsEmpty());
@@ -122,7 +128,8 @@
 
 TEST(OptionsTest, ShortOptions_Valid) {
   Options options;
-  TestValidOptions({"-s", "5122",
+  TestValidOptions({"-v",
+                    "-s", "5122",
                     "-t", "7653",
                     "-d", "20",
                     "-u", "15",
@@ -132,17 +139,28 @@
                     "foo.speed.googlefiber.net",
                     "bar.speed.googlefiber.net"},
                     &options);
-  EXPECT_EQ(5122, options.download_size);
-  EXPECT_EQ(7653, options.upload_size);
+  EXPECT_TRUE(options.verbose);
   EXPECT_EQ(20, options.num_downloads);
+  EXPECT_EQ(5122, options.download_size);
   EXPECT_EQ(15, options.num_uploads);
+  EXPECT_EQ(7653, options.upload_size);
   EXPECT_EQ(500, options.progress_millis);
+  EXPECT_FALSE(options.global);
   EXPECT_EQ(http::Url("speed.gfsvc.com"), options.global_host);
   EXPECT_EQ("CrOS", options.user_agent);
-  EXPECT_EQ(0, options.min_transfer_time);
-  EXPECT_EQ(0, options.max_transfer_time);
+
+  EXPECT_EQ(0, options.max_connections);
+  EXPECT_FALSE(options.disable_dns_cache);
+  EXPECT_FALSE(options.exponential_moving_average);
+  EXPECT_EQ(0, options.min_transfer_runtime);
+  EXPECT_EQ(0, options.max_transfer_runtime);
+  EXPECT_EQ(0, options.min_transfer_intervals);
+  EXPECT_EQ(0, options.max_transfer_intervals);
+  EXPECT_EQ(0, options.max_transfer_variance);
+  EXPECT_EQ(0, options.interval_millis);
   EXPECT_EQ(0, options.ping_runtime);
   EXPECT_EQ(0, options.ping_timeout);
+
   EXPECT_THAT(options.hosts, testing::UnorderedElementsAre(
       http::Url("foo.speed.googlefiber.net"),
       http::Url("bar.speed.googlefiber.net")));
@@ -150,33 +168,47 @@
 
 TEST(OptionsTest, LongOptions_Valid) {
   Options options;
-  TestValidOptions({"--download_size", "5122",
-                    "--upload_size", "7653",
-                    "--progress", "1000",
-                    "--num_uploads", "12",
-                    "--num_downloads", "16",
+  TestValidOptions({"--verbose",
                     "--global_host", "speed.gfsvc.com",
                     "--user_agent", "CrOS",
+                    "--progress_millis", "1000",
                     "--disable_dns_cache",
                     "--max_connections", "23",
-                    "--min_transfer_time", "7500",
-                    "--max_transfer_time", "13500",
+                    "--exponential_moving_average",
+                    "--num_downloads", "16",
+                    "--download_size", "5122",
+                    "--num_uploads", "12",
+                    "--upload_size", "7653",
+                    "--min_transfer_runtime", "7500",
+                    "--max_transfer_runtime", "13500",
+                    "--min_transfer_intervals", "13",
+                    "--max_transfer_intervals", "22",
+                    "--max_transfer_variance", "0.12",
+                    "--interval_millis", "250",
                     "--ping_runtime", "2500",
                     "--ping_timeout", "300",
                     "foo.speed.googlefiber.net",
                     "bar.speed.googlefiber.net"},
                     &options);
-  EXPECT_TRUE(options.disable_dns_cache);
-  EXPECT_EQ(5122, options.download_size);
-  EXPECT_EQ(7653, options.upload_size);
-  EXPECT_EQ(16, options.num_downloads);
-  EXPECT_EQ(23, options.max_connections);
-  EXPECT_EQ(12, options.num_uploads);
-  EXPECT_EQ(1000, options.progress_millis);
+  EXPECT_TRUE(options.verbose);
+  EXPECT_FALSE(options.global);
   EXPECT_EQ(http::Url("speed.gfsvc.com"), options.global_host);
   EXPECT_EQ("CrOS", options.user_agent);
-  EXPECT_EQ(7500, options.min_transfer_time);
-  EXPECT_EQ(13500, options.max_transfer_time);
+  EXPECT_EQ(1000, options.progress_millis);
+  EXPECT_TRUE(options.disable_dns_cache);
+  EXPECT_EQ(23, options.max_connections);
+  EXPECT_TRUE(options.exponential_moving_average);
+  EXPECT_EQ(16, options.num_downloads);
+  EXPECT_EQ(5122, options.download_size);
+  EXPECT_EQ(12, options.num_uploads);
+  EXPECT_EQ(7653, options.upload_size);
+  EXPECT_EQ("CrOS", options.user_agent);
+  EXPECT_EQ(7500, options.min_transfer_runtime);
+  EXPECT_EQ(13500, options.max_transfer_runtime);
+  EXPECT_EQ(13, options.min_transfer_intervals);
+  EXPECT_EQ(22, options.max_transfer_intervals);
+  EXPECT_EQ(0.12, options.max_transfer_variance);
+  EXPECT_EQ(250, options.interval_millis);
   EXPECT_EQ(2500, options.ping_runtime);
   EXPECT_EQ(300, options.ping_timeout);
   EXPECT_THAT(options.hosts, testing::UnorderedElementsAre(
diff --git a/speedtest/ping_test.cc b/speedtest/ping_task.cc
similarity index 84%
rename from speedtest/ping_test.cc
rename to speedtest/ping_task.cc
index 999b53d..7a1c7be 100644
--- a/speedtest/ping_test.cc
+++ b/speedtest/ping_task.cc
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015 Google Inc. All rights reserved.
+ * Copyright 2016 Google Inc. All rights reserved.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,24 +14,23 @@
  * limitations under the License.
  */
 
-#include "ping_test.h"
+#include "ping_task.h"
 
 #include <algorithm>
 #include <cassert>
 #include <iomanip>
 #include <iostream>
-#include "generic_test.h"
 #include "utils.h"
 
 namespace speedtest {
 
-PingTest::PingTest(const Options &options)
-    : GenericTest(options),
+PingTask::PingTask(const Options &options)
+    : HttpTask(options),
       options_(options) {
   assert(options_.num_pings > 0);
 }
 
-void PingTest::RunInternal() {
+void PingTask::RunInternal() {
   ResetCounters();
   success_ = false;
   threads_.clear();
@@ -42,7 +41,7 @@
   }
 }
 
-void PingTest::StopInternal() {
+void PingTask::StopInternal() {
   std::for_each(threads_.begin(), threads_.end(), [](std::thread &t) {
     t.join();
   });
@@ -76,17 +75,16 @@
   if (!min_stats) {
     // no servers respondeded
     success_ = false;
-    return;
   } else {
     fastest_ = *min_stats;
     success_ = true;
   }
 }
 
-void PingTest::RunPing(size_t index) {
-  GenericTest::RequestPtr ping = options_.request_factory(index);
+void PingTask::RunPing(size_t index) {
+  http::Request::Ptr ping = options_.request_factory(index);
   stats_[index].url = ping->url();
-  while (GetStatus() == TestStatus::RUNNING) {
+  while (GetStatus() == TaskStatus::RUNNING) {
     long req_start = SystemTimeMicros();
     if (ping->Get() == CURLE_OK) {
       long req_end = SystemTimeMicros();
@@ -100,16 +98,16 @@
   }
 }
 
-bool PingTest::IsSucceeded() const {
+bool PingTask::IsSucceeded() const {
   return success_;
 }
 
-PingStats PingTest::GetFastest() const {
+PingStats PingTask::GetFastest() const {
   std::lock_guard<std::mutex> lock(mutex_);
   return fastest_;
 }
 
-void PingTest::ResetCounters() {
+void PingTask::ResetCounters() {
   stats_.clear();
   stats_.resize(options_.num_pings);
 }
diff --git a/speedtest/ping_test.h b/speedtest/ping_task.h
similarity index 77%
rename from speedtest/ping_test.h
rename to speedtest/ping_task.h
index 558c799..b2923a8 100644
--- a/speedtest/ping_test.h
+++ b/speedtest/ping_task.h
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015 Google Inc. All rights reserved.
+ * Copyright 2016 Google Inc. All rights reserved.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,8 +14,8 @@
  * limitations under the License.
  */
 
-#ifndef PING_TEST_H
-#define PING_TEST_H
+#ifndef SPEEDTEST_PING_TASK_H
+#define SPEEDTEST_PING_TASK_H
 
 #include <atomic>
 #include <functional>
@@ -24,7 +24,7 @@
 #include <mutex>
 #include <thread>
 #include <vector>
-#include "generic_test.h"
+#include "http_task.h"
 #include "request.h"
 #include "url.h"
 
@@ -37,15 +37,14 @@
   http::Url url;
 };
 
-class PingTest : public GenericTest {
+class PingTask : public HttpTask {
  public:
-  struct Options : GenericTest::Options {
+  struct Options : HttpTask::Options {
     int timeout = 0;
     int num_pings = 0;
-    std::function<RequestPtr(int)> request_factory;
   };
 
-  explicit PingTest(const Options &options);
+  explicit PingTask(const Options &options);
 
   bool IsSucceeded() const;
 
@@ -69,10 +68,10 @@
   PingStats fastest_;
 
   // disallowed
-  PingTest(const PingTest &) = delete;
-  void operator=(const PingTest &) = delete;
+  PingTask(const PingTask &) = delete;
+  void operator=(const PingTask &) = delete;
 };
 
 }  // namespace speedtest
 
-#endif  // PING_TEST_H
+#endif  // SPEEDTEST_PING_TASK_H
diff --git a/speedtest/request.cc b/speedtest/request.cc
index 1542053..ef46d2d 100644
--- a/speedtest/request.cc
+++ b/speedtest/request.cc
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015 Google Inc. All rights reserved.
+ * Copyright 2016 Google Inc. All rights reserved.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/speedtest/request.h b/speedtest/request.h
index f3344d2..8588e29 100644
--- a/speedtest/request.h
+++ b/speedtest/request.h
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015 Google Inc. All rights reserved.
+ * Copyright 2016 Google Inc. All rights reserved.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -42,6 +42,7 @@
                                         curl_off_t,
                                         curl_off_t,
                                         curl_off_t)>;
+  using Ptr = std::unique_ptr<Request>;
 
   Request(std::shared_ptr<CURL> handle, const Url &url);
   virtual ~Request();
diff --git a/speedtest/request_test.cc b/speedtest/request_test.cc
index 4926419..ae7b74f 100644
--- a/speedtest/request_test.cc
+++ b/speedtest/request_test.cc
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015 Google Inc. All rights reserved.
+ * Copyright 2016 Google Inc. All rights reserved.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -30,11 +30,11 @@
 
   void SetUp() override {
     env = CurlEnv::NewCurlEnv({});
+    request = env->NewRequest(http::Url("http://example.com/foo"));
   }
 
   void VerifyQueryString(const char *expected,
                          Request::QueryStringParams params) {
-    request = env->NewRequest(Url());
     request->params() = params;
     request->UpdateUrl();
     EXPECT_EQ(expected, request->url().query_string());
@@ -43,7 +43,7 @@
   void VerifyUrl(const char *expected,
                  const char *url,
                  Request::QueryStringParams params) {
-    request = env->NewRequest(Url(url));
+    request->set_url(Url(url));
     request->params() = params;
     request->UpdateUrl();
     EXPECT_EQ(expected, request->url().url());
@@ -91,9 +91,9 @@
 }
 
 TEST_F(RequestTest, Url_OneParamTwoValues_Ok) {
-  VerifyUrl("http://example.com/?abc=def&abc=ghi",
+  VerifyUrl("http://example.com/?abc=def&abc=def",
             "http://example.com",
-            {{"abc", "def"}, {"abc", "ghi"}});
+            {{"abc", "def"}, {"abc", "def"}});
 }
 
 TEST_F(RequestTest, Url_EscapeParam_Ok) {
diff --git a/speedtest/speedtest.cc b/speedtest/speedtest.cc
index 1cd9286..ed1263d 100644
--- a/speedtest/speedtest.cc
+++ b/speedtest/speedtest.cc
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015 Google Inc. All rights reserved.
+ * Copyright 2016 Google Inc. All rights reserved.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -26,7 +26,8 @@
 #include <streambuf>
 
 #include "errors.h"
-#include "runner.h"
+#include "timed_runner.h"
+#include "transfer_runner.h"
 #include "utils.h"
 
 namespace speedtest {
@@ -100,9 +101,13 @@
     std::string version = LoadFile(kFileVersion);
     Trim(&serial);
     Trim(&version);
-    user_agent_ = std::string("CPE/") +
-                  (version.empty() ? "unknown version" : version) + "/" +
-                  (serial.empty() ? "unknown serial" : serial);
+    user_agent_ = "CPE";
+    if (!version.empty()) {
+      user_agent_ += "/" + version;
+      if (!serial.empty()) {
+        user_agent_ += "/" + serial;
+      }
+    }
   } else {
     user_agent_ = options_.user_agent;
     return;
@@ -155,7 +160,7 @@
     return;
   }
 
-  PingTest::Options options;
+  PingTask::Options options;
   options.verbose = options_.verbose;
   options.timeout = PingTimeout();
   std::vector<http::Url> hosts;
@@ -171,14 +176,14 @@
       std::cout << "  " << host.url() << "\n";
     }
   }
-  options.request_factory = [&](int id) -> GenericTest::RequestPtr{
+  options.request_factory = [&](int id) -> http::Request::Ptr{
     return MakeRequest(hosts[id]);
   };
-  PingTest find_nearest(options);
+  PingTask find_nearest(options);
   if (options_.verbose) {
     std::cout << "Starting to find nearest server\n";
   }
-  TimedRun(&find_nearest, 1500);
+  RunTimed(&find_nearest, 1500);
   find_nearest.WaitForEnd();
   if (find_nearest.IsSucceeded()) {
     PingStats fastest = find_nearest.GetFastest();
@@ -198,7 +203,7 @@
   if (options_.verbose) {
     std::cout << "Loading config from " << config_url.url() << "\n";
   }
-  GenericTest::RequestPtr request = MakeRequest(config_url);
+  http::Request::Ptr request = MakeRequest(config_url);
   request->set_url(config_url);
   std::string json;
   request->Get([&](void *data, size_t size){
@@ -208,20 +213,20 @@
 }
 
 void Speedtest::RunPingTest() {
-  PingTest::Options options;
+  PingTask::Options options;
   options.verbose = options_.verbose;
   options.timeout = PingTimeout();
   options.num_pings = 1;
   http::Url ping_url(*server_url_);
   ping_url.set_path("/ping");
-  options.request_factory = [&](int id) -> GenericTest::RequestPtr{
+  options.request_factory = [&](int id) -> http::Request::Ptr{
     return MakeRequest(ping_url);
   };
-  ping_test_.reset(new PingTest(options));
-  TimedRun(ping_test_.get(), PingRunTime());
-  ping_test_->WaitForEnd();
-  PingStats fastest = ping_test_->GetFastest();
-  if (ping_test_->IsSucceeded()) {
+  std::unique_ptr<PingTask> ping(new PingTask(options));
+  RunTimed(ping.get(), PingRunTime());
+  ping->WaitForEnd();
+  PingStats fastest = ping->GetFastest();
+  if (ping->IsSucceeded()) {
     long micros = fastest.min_micros;
     std::cout << "Ping time: " << round(micros / 1000.0d, 3) << " ms\n";
   } else {
@@ -232,52 +237,95 @@
 
 void Speedtest::RunDownloadTest() {
   if (options_.verbose) {
-    std::cout << "Starting download test at " << config_.location_name
+    std::cout << "Starting download test to " << config_.location_name
               << " (" << server_url_->url() << ")\n";
   }
-  DownloadTest::Options options;
-  options.verbose = options_.verbose;
-  options.num_transfers = NumDownloads();
-  options.download_size = DownloadSize();
-  options.request_factory = [this](int id) -> GenericTest::RequestPtr{
+  DownloadTask::Options download_options;
+  download_options.verbose = options_.verbose;
+  download_options.num_transfers = NumDownloads();
+  download_options.download_size = DownloadSize();
+  download_options.request_factory = [this](int id) -> http::Request::Ptr{
     return MakeTransferRequest(id, "/download");
   };
-  download_test_.reset(new DownloadTest(options));
-  TimedRun(download_test_.get(), 5000);
-  download_test_->WaitForEnd();
-  long bytes = download_test_->bytes_transferred();
-  long micros = download_test_->GetRunningTime();
+  std::unique_ptr<DownloadTask> download(new DownloadTask(download_options));
+  TransferRunner::Options runner_options;
+  runner_options.verbose = options_.verbose;
+  runner_options.task = download.get();
+  runner_options.min_runtime = MinTransferRuntime();
+  runner_options.max_runtime = MaxTransferRuntime();
+  runner_options.min_intervals = MinTransferIntervals();
+  runner_options.max_intervals = MaxTransferIntervals();
+  runner_options.max_variance = MaxTransferVariance();
+  runner_options.interval_millis = IntervalMillis();
+  if (options_.progress_millis > 0) {
+    runner_options.progress_millis = options_.progress_millis;
+    runner_options.progress_fn = [](Interval interval) {
+      double speed_variance = variance(interval.short_megabits,
+                                       interval.long_megabits);
+      std::cout << "[+" << round(interval.running_time / 1000.0, 0) << " ms] "
+                << "Download speed: " << round(interval.short_megabits, 2)
+                << " - " << round(interval.long_megabits, 2)
+                << " Mbps (" << interval.bytes << " bytes, variance "
+                << round(speed_variance, 4) << ")\n";
+    };
+  }
+  TransferRunner runner(runner_options);
+  runner.Run();
+  runner.WaitForEnd();
   if (options_.verbose) {
-    std::cout << "Downloaded " << bytes << " bytes in "
-              << round(micros / 1000.0d, 2) << " ms\n";
+    long running_time = download->GetRunningTimeMicros();
+    std::cout << "Downloaded " << download->bytes_transferred()
+              << " bytes in " << round(running_time / 1000.0, 0) << " ms\n";
   }
   std::cout << "Download speed: "
-            << round(ToMegabits(bytes, micros), 3) << " Mbps\n";
+            << round(runner.GetSpeedInMegabits(), 2) << " Mbps\n";
 }
 
 void Speedtest::RunUploadTest() {
   if (options_.verbose) {
-    std::cout << "Starting upload test at " << config_.location_name
+    std::cout << "Starting upload test to " << config_.location_name
               << " (" << server_url_->url() << ")\n";
   }
-  UploadTest::Options options;
-  options.verbose = options_.verbose;
-  options.num_transfers = NumUploads();
-  options.payload = MakeRandomData(UploadSize());
-  options.request_factory = [this](int id) -> GenericTest::RequestPtr{
+  UploadTask::Options upload_options;
+  upload_options.verbose = options_.verbose;
+  upload_options.num_transfers = NumUploads();
+  upload_options.payload = MakeRandomData(UploadSize());
+  upload_options.request_factory = [this](int id) -> http::Request::Ptr{
     return MakeTransferRequest(id, "/upload");
   };
-  upload_test_.reset(new UploadTest(options));
-  TimedRun(upload_test_.get(), 5000);
-  upload_test_->WaitForEnd();
-  long bytes = upload_test_->bytes_transferred();
-  long micros = upload_test_->GetRunningTime();
+
+  std::unique_ptr<UploadTask> upload(new UploadTask(upload_options));
+  TransferRunner::Options runner_options;
+  runner_options.verbose = options_.verbose;
+  runner_options.task = upload.get();
+  runner_options.min_runtime = MinTransferRuntime();
+  runner_options.max_runtime = MaxTransferRuntime();
+  runner_options.min_intervals = MinTransferIntervals();
+  runner_options.max_intervals = MaxTransferIntervals();
+  runner_options.max_variance = MaxTransferVariance();
+  runner_options.interval_millis = IntervalMillis();
+  if (options_.progress_millis > 0) {
+    runner_options.progress_millis = options_.progress_millis;
+    runner_options.progress_fn = [](Interval interval) {
+      double speed_variance = variance(interval.short_megabits,
+                                       interval.long_megabits);
+      std::cout << "[+" << round(interval.running_time / 1000.0, 0) << " ms] "
+                << "Upload speed: " << round(interval.short_megabits, 2)
+                << " - " << round(interval.long_megabits, 2)
+                << " Mbps (" << interval.bytes << " bytes, variance "
+                << round(speed_variance, 4) << ")\n";
+    };
+  }
+  TransferRunner runner(runner_options);
+  runner.Run();
+  runner.WaitForEnd();
   if (options_.verbose) {
-    std::cout << "Uploaded " << bytes << " bytes in "
-              << round(micros / 1000.0d, 2) << " ms\n";
+    long running_time = upload->GetRunningTimeMicros();
+    std::cout << "Uploaded " << upload->bytes_transferred()
+              << " bytes in " << round(running_time / 1000.0, 0) << " ms\n";
   }
   std::cout << "Upload speed: "
-            << round(ToMegabits(bytes, micros), 3) << " Mbps\n";
+            << round(runner.GetSpeedInMegabits(), 2) << " Mbps\n";
 }
 
 int Speedtest::NumDownloads() const {
@@ -316,22 +364,58 @@
          : config_.ping_timeout;
 }
 
-GenericTest::RequestPtr Speedtest::MakeRequest(const http::Url &url) {
-  GenericTest::RequestPtr request = env_->NewRequest(url);
+int Speedtest::MinTransferRuntime() const {
+  return options_.min_transfer_runtime
+         ? options_.min_transfer_runtime
+         : config_.min_transfer_runtime;
+}
+
+int Speedtest::MaxTransferRuntime() const {
+  return options_.max_transfer_runtime
+         ? options_.max_transfer_runtime
+         : config_.max_transfer_runtime;
+}
+
+int Speedtest::MinTransferIntervals() const {
+  return options_.min_transfer_intervals
+         ? options_.min_transfer_intervals
+         : config_.min_transfer_intervals;
+}
+
+int Speedtest::MaxTransferIntervals() const {
+  return options_.max_transfer_intervals
+         ? options_.max_transfer_intervals
+         : config_.max_transfer_intervals;
+}
+
+double Speedtest::MaxTransferVariance() const {
+  return options_.max_transfer_variance
+         ? options_.max_transfer_variance
+         : config_.max_transfer_variance;
+}
+
+int Speedtest::IntervalMillis() const {
+  return options_.interval_millis
+         ? options_.interval_millis
+         : config_.interval_millis;
+}
+
+http::Request::Ptr Speedtest::MakeRequest(const http::Url &url) {
+  http::Request::Ptr request = env_->NewRequest(url);
   if (!user_agent_.empty()) {
     request->set_user_agent(user_agent_);
   }
   return std::move(request);
 }
 
-GenericTest::RequestPtr Speedtest::MakeBaseRequest(
+http::Request::Ptr Speedtest::MakeBaseRequest(
     int id, const std::string &path) {
   http::Url url(*server_url_);
   url.set_path(path);
   return MakeRequest(url);
 }
 
-GenericTest::RequestPtr Speedtest::MakeTransferRequest(
+http::Request::Ptr Speedtest::MakeTransferRequest(
     int id, const std::string &path) {
   http::Url url(*server_url_);
   int port_start = config_.transfer_port_start;
diff --git a/speedtest/speedtest.h b/speedtest/speedtest.h
index ea509f7..fb32355 100644
--- a/speedtest/speedtest.h
+++ b/speedtest/speedtest.h
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015 Google Inc. All rights reserved.
+ * Copyright 2016 Google Inc. All rights reserved.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -23,11 +23,10 @@
 
 #include "config.h"
 #include "curl_env.h"
-#include "download_test.h"
-#include "generic_test.h"
+#include "download_task.h"
 #include "options.h"
-#include "ping_test.h"
-#include "upload_test.h"
+#include "ping_task.h"
+#include "upload_task.h"
 #include "url.h"
 #include "request.h"
 
@@ -55,10 +54,16 @@
   int UploadSize() const;
   int PingTimeout() const;
   int PingRunTime() const;
+  int MinTransferRuntime() const;
+  int MaxTransferRuntime() const;
+  int MinTransferIntervals() const;
+  int MaxTransferIntervals() const;
+  double MaxTransferVariance() const;
+  int IntervalMillis() const;
 
-  GenericTest::RequestPtr MakeRequest(const http::Url &url);
-  GenericTest::RequestPtr MakeBaseRequest(int id, const std::string &path);
-  GenericTest::RequestPtr MakeTransferRequest(int id, const std::string &path);
+  http::Request::Ptr MakeRequest(const http::Url &url);
+  http::Request::Ptr MakeBaseRequest(int id, const std::string &path);
+  http::Request::Ptr MakeTransferRequest(int id, const std::string &path);
 
   std::shared_ptr <http::CurlEnv> env_;
   Options options_;
@@ -67,9 +72,6 @@
   std::vector<http::Url> servers_;
   std::unique_ptr<http::Url> server_url_;
   std::unique_ptr<std::string> send_data_;
-  std::unique_ptr<PingTest> ping_test_;
-  std::unique_ptr<DownloadTest> download_test_;
-  std::unique_ptr<UploadTest> upload_test_;
 
   // disable
   Speedtest(const Speedtest &) = delete;
diff --git a/speedtest/speedtest_main.cc b/speedtest/speedtest_main.cc
index 8a9c2c9..d756c4b 100644
--- a/speedtest/speedtest_main.cc
+++ b/speedtest/speedtest_main.cc
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015 Google Inc. All rights reserved.
+ * Copyright 2016 Google Inc. All rights reserved.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/speedtest/task.cc b/speedtest/task.cc
new file mode 100644
index 0000000..84d12c9
--- /dev/null
+++ b/speedtest/task.cc
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2016 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "task.h"
+
+#include <algorithm>
+#include <cassert>
+#include <iostream>
+#include <thread>
+#include "utils.h"
+
+namespace speedtest {
+
+const char *AsString(TaskStatus status) {
+  switch (status) {
+    case TaskStatus::NOT_STARTED: return "NOT_STARTED";
+    case TaskStatus::RUNNING: return "RUNNING";
+    case TaskStatus::STOPPING: return "STOPPING";
+    case TaskStatus::STOPPED: return "STOPPED";
+  }
+  std::exit(1);
+}
+
+Task::Task(const Options &options)
+    : status_(TaskStatus::NOT_STARTED) {
+  assert(options.request_factory);
+}
+
+Task::~Task() {
+  Stop();
+  if (runner_.joinable()) {
+    runner_.join();
+  }
+  if (stopper_.joinable()) {
+    stopper_.join();
+  }
+}
+
+void Task::Run() {
+  runner_ = std::thread([=]{
+    {
+      std::lock_guard <std::mutex> lock(mutex_);
+      if (status_ != TaskStatus::NOT_STARTED &&
+          status_ != TaskStatus::STOPPED) {
+        return;
+      }
+      UpdateStatusLocked(TaskStatus::RUNNING);
+      start_time_ = SystemTimeMicros();
+    }
+    RunInternal();
+  });
+  stopper_ = std::thread([=]{
+    WaitFor(TaskStatus::STOPPING);
+    StopInternal();
+    std::lock_guard <std::mutex> lock(mutex_);
+    UpdateStatusLocked(TaskStatus::STOPPED);
+    end_time_ = SystemTimeMicros();
+  });
+}
+
+void Task::Stop() {
+  std::lock_guard <std::mutex> lock(mutex_);
+  if (status_ != TaskStatus::RUNNING) {
+    return;
+  }
+  UpdateStatusLocked(TaskStatus::STOPPING);
+}
+
+TaskStatus Task::GetStatus() const {
+  std::lock_guard <std::mutex> lock(mutex_);
+  return status_;
+}
+
+long Task::GetStartTime() const {
+  std::lock_guard <std::mutex> lock(mutex_);
+  return start_time_;
+}
+
+long Task::GetEndTime() const {
+  std::lock_guard <std::mutex> lock(mutex_);
+  return end_time_;
+}
+
+long Task::GetRunningTimeMicros() const {
+  std::lock_guard <std::mutex> lock(mutex_);
+  switch (status_) {
+    case TaskStatus::NOT_STARTED:
+      break;
+    case TaskStatus::RUNNING:
+    case TaskStatus::STOPPING:
+      return SystemTimeMicros() - start_time_;
+    case TaskStatus::STOPPED:
+      return end_time_ - start_time_;
+  }
+  return 0;
+}
+
+void Task::WaitForEnd() {
+  WaitFor(TaskStatus::STOPPED);
+}
+
+void Task::UpdateStatusLocked(TaskStatus status) {
+  status_ = status;
+  status_cond_.notify_all();
+}
+
+void Task::WaitFor(TaskStatus status) {
+  std::unique_lock<std::mutex> lock(mutex_);
+  status_cond_.wait(lock, [=]{
+    return status_ == status;
+  });
+}
+
+}  // namespace speedtest
diff --git a/speedtest/generic_test.h b/speedtest/task.h
similarity index 60%
rename from speedtest/generic_test.h
rename to speedtest/task.h
index cf0d37c..429b078 100644
--- a/speedtest/generic_test.h
+++ b/speedtest/task.h
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015 Google Inc. All rights reserved.
+ * Copyright 2016 Google Inc. All rights reserved.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,42 +14,42 @@
  * limitations under the License.
  */
 
-#ifndef SPEEDTEST_GENERIC_TEST_H
-#define SPEEDTEST_GENERIC_TEST_H
+#ifndef SPEEDTEST_TASK_H
+#define SPEEDTEST_TASK_H
 
 #include <condition_variable>
 #include <functional>
 #include <memory>
 #include <mutex>
-#include "request.h"
+#include <thread>
 
 namespace speedtest {
 
-enum class TestStatus {
+enum class TaskStatus {
   NOT_STARTED,
   RUNNING,
   STOPPING,
   STOPPED
 };
 
-const char *AsString(TestStatus status);
+const char *AsString(TaskStatus status);
 
-class GenericTest {
+class Task {
  public:
-  using RequestPtr = std::unique_ptr<http::Request>;
-
   struct Options {
     bool verbose = false;
-    std::function<RequestPtr(int)> request_factory;
   };
 
-  explicit GenericTest(const Options &options);
+  explicit Task(const Options &options);
+  virtual ~Task();
 
   void Run();
   void Stop();
 
-  TestStatus GetStatus() const;
-  long GetRunningTime() const;
+  TaskStatus GetStatus() const;
+  long GetStartTime() const;
+  long GetEndTime() const;
+  long GetRunningTimeMicros() const;
   void WaitForEnd();
 
  protected:
@@ -57,19 +57,24 @@
   virtual void StopInternal() {}
 
  private:
+  // Only call with mutex_
+  void UpdateStatusLocked(TaskStatus status);
+
+  void WaitFor(TaskStatus status);
+
   mutable std::mutex mutex_;
-  std::condition_variable condition_;
-  TestStatus status_;
-  bool running_ = false;
+  std::thread runner_;
+  std::thread stopper_;
+  std::condition_variable status_cond_;
+  TaskStatus status_;
   long start_time_;
   long end_time_;
 
   // disallowed
-  GenericTest(const GenericTest &) = delete;
-
-  void operator=(const GenericTest &) = delete;
+  Task(const Task &) = delete;
+  void operator=(const Task &) = delete;
 };
 
 }  // namespace speedtest
 
-#endif  //SPEEDTEST_GENERIC_TEST_H
+#endif  //SPEEDTEST_TASK_H
diff --git a/speedtest/runner.cc b/speedtest/timed_runner.cc
similarity index 82%
rename from speedtest/runner.cc
rename to speedtest/timed_runner.cc
index 0dae542..bf7c4cc 100644
--- a/speedtest/runner.cc
+++ b/speedtest/timed_runner.cc
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015 Google Inc. All rights reserved.
+ * Copyright 2016 Google Inc. All rights reserved.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,20 +14,20 @@
  * limitations under the License.
  */
 
-#include "runner.h"
+#include "timed_runner.h"
 
 #include <cassert>
 #include <thread>
 
 namespace speedtest {
 
-void TimedRun(GenericTest *test, long millis) {
-  assert(test);
-  test->Run();
+void RunTimed(Task *task, long millis) {
+  assert(task);
+  task->Run();
   std::thread timer([=] {
     std::this_thread::sleep_for(
         std::chrono::milliseconds(millis));
-    test->Stop();
+    task->Stop();
   });
   timer.join();
 }
diff --git a/speedtest/runner.h b/speedtest/timed_runner.h
similarity index 83%
rename from speedtest/runner.h
rename to speedtest/timed_runner.h
index 4d677fc..02e673f 100644
--- a/speedtest/runner.h
+++ b/speedtest/timed_runner.h
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015 Google Inc. All rights reserved.
+ * Copyright 2016 Google Inc. All rights reserved.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -17,11 +17,12 @@
 #ifndef SPEEDTEST_RUNNER_H
 #define SPEEDTEST_RUNNER_H
 
-#include "generic_test.h"
+#include "task.h"
 
 namespace speedtest {
 
-void TimedRun(GenericTest *test, long millis);
+// Run a task for a set duration
+void RunTimed(Task *task, long millis);
 
 }  // namespace speedtest
 
diff --git a/speedtest/transfer_runner.cc b/speedtest/transfer_runner.cc
new file mode 100644
index 0000000..d37f087
--- /dev/null
+++ b/speedtest/transfer_runner.cc
@@ -0,0 +1,171 @@
+/*
+ * Copyright 2016 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "transfer_runner.h"
+
+#include <algorithm>
+#include <cassert>
+#include <chrono>
+#include <iostream>
+#include <thread>
+#include "transfer_task.h"
+#include "utils.h"
+
+namespace speedtest {
+namespace {
+
+const int kDefaultIntervalMillis = 200;
+
+}  // namespace
+
+TransferRunner::TransferRunner(const Options &options)
+    : Task(options),
+      options_(options) {
+  if (options_.interval_millis <= 0) {
+    options_.interval_millis = kDefaultIntervalMillis;
+  }
+}
+
+void TransferRunner::RunInternal() {
+  threads_.clear();
+  intervals_.clear();
+
+  // sentinel value of all zeroes
+  intervals_.emplace_back();
+
+  // If progress updates are created add a thread to send updates
+  if (options_.progress_fn && options_.progress_millis > 0) {
+    if (options_.verbose) {
+      std::cout << "Progress updates every "
+                << options_.progress_millis << " ms\n";
+    }
+    threads_.emplace_back([&] {
+      std::this_thread::sleep_for(
+          std::chrono::milliseconds(options_.progress_millis));
+      while (GetStatus() == TaskStatus::RUNNING) {
+        Interval progress = GetLastInterval();
+        options_.progress_fn(progress);
+        std::this_thread::sleep_for(
+            std::chrono::milliseconds(options_.progress_millis));
+      }
+      Interval progress = GetLastInterval();
+      options_.progress_fn(progress);
+    });
+  } else if (options_.verbose) {
+    std::cout << "No progress updates\n";
+  }
+
+  // Updating thread
+  if (options_.verbose) {
+    std::cout << "Transfer runner updates every "
+              << options_.interval_millis << " ms\n";
+  }
+  threads_.emplace_back([&] {
+    std::this_thread::sleep_for(
+        std::chrono::milliseconds(options_.interval_millis));
+    while (GetStatus() == TaskStatus::RUNNING) {
+      const Interval &interval = AddInterval();
+      if (interval.running_time > options_.max_runtime * 1000) {
+        Stop();
+        return;
+      }
+      if (interval.running_time >= options_.min_runtime * 1000 &&
+          interval.long_megabits > 0 &&
+          interval.short_megabits > 0) {
+        double speed_variance = variance(interval.short_megabits,
+                                         interval.long_megabits);
+        if (speed_variance <= options_.max_variance) {
+          Stop();
+          return;
+        }
+      }
+      std::this_thread::sleep_for(
+          std::chrono::milliseconds(options_.interval_millis));
+    }
+  });
+
+  options_.task->Run();
+}
+
+void TransferRunner::StopInternal() {
+  options_.task->Stop();
+  options_.task->WaitForEnd();
+  std::for_each(threads_.begin(), threads_.end(), [](std::thread &t) {
+    t.join();
+  });
+  threads_.clear();
+}
+
+const Interval &TransferRunner::AddInterval() {
+  std::lock_guard <std::mutex> lock(mutex_);
+  intervals_.emplace_back();
+  Interval &interval = intervals_[intervals_.size() - 1];
+  interval.running_time = options_.task->GetRunningTimeMicros();
+  interval.bytes = options_.task->bytes_transferred();
+  if (options_.exponential_moving_average) {
+    interval.short_megabits = GetShortEma(options_.min_intervals);
+    interval.long_megabits = GetLongEma(options_.max_intervals);
+  } else {
+    interval.short_megabits = GetSimpleAverage(options_.min_intervals);
+    interval.long_megabits = GetSimpleAverage(options_.max_intervals);
+  }
+  speed_ = interval.long_megabits;
+  return intervals_.back();
+}
+
+Interval TransferRunner::GetLastInterval() const {
+  std::lock_guard <std::mutex> lock(mutex_);
+  return intervals_.back();
+}
+
+double TransferRunner::GetSpeedInMegabits() const {
+  std::lock_guard <std::mutex> lock(mutex_);
+  return speed_;
+}
+
+double TransferRunner::GetShortEma(int num_intervals) {
+  if (intervals_.empty() || num_intervals <= 0) {
+    return 0.0;
+  }
+  Interval last_interval = GetLastInterval();
+  double percent = 2.0d / (num_intervals + 1);
+  return GetSimpleAverage(1) * percent +
+      last_interval.short_megabits * (1 - percent);
+}
+
+double TransferRunner::GetLongEma(int num_intervals) {
+  if (intervals_.empty() || num_intervals <= 0) {
+    return 0.0;
+  }
+  Interval last_interval = GetLastInterval();
+  double percent = 2.0d / (num_intervals + 1);
+  return GetSimpleAverage(1) * percent +
+      last_interval.long_megabits * (1 - percent);
+}
+
+double TransferRunner::GetSimpleAverage(int num_intervals) {
+  if (intervals_.empty() || num_intervals <= 0) {
+    return 0.0;
+  }
+  int end_index = intervals_.size() - 1;
+  int start_index = std::max(0, end_index - num_intervals);
+  const Interval &end = intervals_[end_index];
+  const Interval &start = intervals_[start_index];
+  return ToMegabits(end.bytes - start.bytes,
+                    end.running_time - start.running_time);
+}
+
+}  // namespace
diff --git a/speedtest/transfer_runner.h b/speedtest/transfer_runner.h
new file mode 100644
index 0000000..793c8ec
--- /dev/null
+++ b/speedtest/transfer_runner.h
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2016 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SPEEDTEST_TRANSFER_RUNNER_H
+#define SPEEDTEST_TRANSFER_RUNNER_H
+
+#include <functional>
+#include <mutex>
+#include <thread>
+#include <vector>
+#include "task.h"
+#include "transfer_task.h"
+
+namespace speedtest {
+
+struct Interval {
+  long bytes = 0;
+  long running_time = 0;
+  double short_megabits = 0.0;
+  double long_megabits = 0.0;
+};
+
+// Run a variable length transfer test using two moving averages.
+// The test runs between min_runtime and max_runtime and otherwise
+// ends when the speed is "stable" meaning the two moving averages
+// are relatively close to one another.
+class TransferRunner : public Task {
+ public:
+  struct Options : public Task::Options {
+    TransferTask *task = nullptr;
+    int min_runtime = 0;
+    int max_runtime = 0;
+    int interval_millis = 0;
+    int progress_millis = 0;
+    int min_intervals = 0;
+    int max_intervals = 0;
+    double max_variance = 0.0;
+    bool exponential_moving_average = false;
+    std::function<void(Interval)> progress_fn;
+  };
+
+  explicit TransferRunner(const Options &options);
+
+  double GetSpeedInMegabits() const;
+  Interval GetLastInterval() const;
+
+ protected:
+  void RunInternal() override;
+  void StopInternal() override;
+
+ private:
+  const Interval &AddInterval();
+  double GetSimpleAverage(int num_intervals);
+  double GetShortEma(int num_intervals);
+  double GetLongEma(int num_intervals);
+
+  Options options_;
+
+  mutable std::mutex mutex_;
+  std::vector<Interval> intervals_;
+  std::vector<std::thread> threads_;
+  double speed_;
+
+  // disallowed
+  TransferRunner(const TransferRunner &) = delete;
+  void operator=(const TransferRunner &) = delete;
+};
+
+}  // namespace
+
+#endif //SPEEDTEST_TRANSFER_RUNNER_H
diff --git a/speedtest/transfer_test.cc b/speedtest/transfer_task.cc
similarity index 72%
rename from speedtest/transfer_test.cc
rename to speedtest/transfer_task.cc
index 213d5d9..d742d87 100644
--- a/speedtest/transfer_test.cc
+++ b/speedtest/transfer_task.cc
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015 Google Inc. All rights reserved.
+ * Copyright 2016 Google Inc. All rights reserved.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,35 +14,37 @@
  * limitations under the License.
  */
 
-#include "transfer_test.h"
+#include "transfer_task.h"
 
 #include <cassert>
+#include <thread>
+#include <vector>
 
 namespace speedtest {
 
-TransferTest::TransferTest(const Options &options)
-    : GenericTest(options),
+TransferTask::TransferTask(const Options &options)
+    : HttpTask(options),
       bytes_transferred_(0),
       requests_started_(0),
       requests_ended_(0) {
   assert(options.num_transfers > 0);
 }
 
-void TransferTest::ResetCounters() {
+void TransferTask::ResetCounters() {
   bytes_transferred_ = 0;
   requests_started_ = 0;
   requests_ended_ = 0;
 }
 
-void TransferTest::StartRequest() {
+void TransferTask::StartRequest() {
   requests_started_++;
 }
 
-void TransferTest::EndRequest() {
+void TransferTask::EndRequest() {
   requests_ended_++;
 }
 
-void TransferTest::TransferBytes(long bytes) {
+void TransferTask::TransferBytes(long bytes) {
   bytes_transferred_ += bytes;
 }
 
diff --git a/speedtest/transfer_test.h b/speedtest/transfer_task.h
similarity index 78%
rename from speedtest/transfer_test.h
rename to speedtest/transfer_task.h
index d7307a6..83cff9e 100644
--- a/speedtest/transfer_test.h
+++ b/speedtest/transfer_task.h
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015 Google Inc. All rights reserved.
+ * Copyright 2016 Google Inc. All rights reserved.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -18,17 +18,17 @@
 #define SPEEDTEST_TRANSFER_TEST_H
 
 #include <atomic>
-#include "generic_test.h"
+#include "http_task.h"
 
 namespace speedtest {
 
-class TransferTest : public GenericTest {
+class TransferTask : public HttpTask {
  public:
-  struct Options : GenericTest::Options {
-    int num_transfers;
+  struct Options : HttpTask::Options {
+    int num_transfers = 0;
   };
 
-  explicit TransferTest(const Options &options);
+  explicit TransferTask(const Options &options);
 
   long bytes_transferred() const { return bytes_transferred_; }
   long requests_started() const { return requests_started_; }
@@ -46,8 +46,8 @@
   std::atomic_int requests_ended_;
 
   // disallowed
-  TransferTest(const TransferTest &) = delete;
-  void operator=(const TransferTest &) = delete;
+  TransferTask(const TransferTask &) = delete;
+  void operator=(const TransferTask &) = delete;
 };
 
 }  // namespace speedtest
diff --git a/speedtest/upload_test.cc b/speedtest/upload_task.cc
similarity index 82%
rename from speedtest/upload_test.cc
rename to speedtest/upload_task.cc
index 8c014ba..251fc41 100644
--- a/speedtest/upload_test.cc
+++ b/speedtest/upload_task.cc
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015 Google Inc. All rights reserved.
+ * Copyright 2016 Google Inc. All rights reserved.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,24 +14,23 @@
  * limitations under the License.
  */
 
-#include "upload_test.h"
+#include "upload_task.h"
 
 #include <algorithm>
 #include <cassert>
 #include <iostream>
-#include "generic_test.h"
 #include "utils.h"
 
 namespace speedtest {
 
-UploadTest::UploadTest(const Options &options)
-    : TransferTest(options),
+UploadTask::UploadTask(const Options &options)
+    : TransferTask(options),
       options_(options) {
   assert(options_.payload);
   assert(options_.payload->size() > 0);
 }
 
-void UploadTest::RunInternal() {
+void UploadTask::RunInternal() {
   ResetCounters();
   threads_.clear();
   if (options_.verbose) {
@@ -45,15 +44,15 @@
   }
 }
 
-void UploadTest::StopInternal() {
+void UploadTask::StopInternal() {
   std::for_each(threads_.begin(), threads_.end(), [](std::thread &t) {
     t.join();
   });
 }
 
-void UploadTest::RunUpload(int id) {
-  GenericTest::RequestPtr upload = options_.request_factory(id);
-  while (GetStatus() == TestStatus::RUNNING) {
+void UploadTask::RunUpload(int id) {
+  http::Request::Ptr upload = options_.request_factory(id);
+  while (GetStatus() == TaskStatus::RUNNING) {
     long uploaded = 0;
     upload->set_param("i", to_string(id));
     upload->set_param("time", to_string(SystemTimeMicros()));
@@ -65,7 +64,7 @@
         TransferBytes(ulnow - uploaded);
         uploaded = ulnow;
       }
-      return GetStatus() != TestStatus::RUNNING;
+      return GetStatus() != TaskStatus::RUNNING;
     });
 
     // disable the Expect header as the server isn't expecting it (perhaps
diff --git a/speedtest/upload_test.h b/speedtest/upload_task.h
similarity index 70%
rename from speedtest/upload_test.h
rename to speedtest/upload_task.h
index 509eed6..323f904 100644
--- a/speedtest/upload_test.h
+++ b/speedtest/upload_task.h
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015 Google Inc. All rights reserved.
+ * Copyright 2016 Google Inc. All rights reserved.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,24 +14,24 @@
  * limitations under the License.
  */
 
-#ifndef SPEEDTEST_UPLOAD_TEST_H
-#define SPEEDTEST_UPLOAD_TEST_H
+#ifndef SPEEDTEST_UPLOAD_TASK_H
+#define SPEEDTEST_UPLOAD_TASK_H
 
 #include <memory>
 #include <string>
 #include <thread>
 #include <vector>
-#include "transfer_test.h"
+#include "transfer_task.h"
 
 namespace speedtest {
 
-class UploadTest : public TransferTest {
+class UploadTask : public TransferTask {
  public:
-  struct Options : TransferTest::Options {
+  struct Options : TransferTask::Options {
     std::shared_ptr<std::string> payload;
   };
 
-  explicit UploadTest(const Options &options);
+  explicit UploadTask(const Options &options);
 
  protected:
   void RunInternal() override;
@@ -44,10 +44,10 @@
   std::vector<std::thread> threads_;
 
   // disallowed
-  UploadTest(const UploadTest &) = delete;
-  void operator=(const UploadTest &) = delete;
+  UploadTask(const UploadTask &) = delete;
+  void operator=(const UploadTask &) = delete;
 };
 
 }  // namespace speedtest
 
-#endif  // SPEEDTEST_UPLOAD_TEST_H
+#endif  // SPEEDTEST_UPLOAD_TASK_H
diff --git a/speedtest/url.cc b/speedtest/url.cc
index 6292790..61588a0 100644
--- a/speedtest/url.cc
+++ b/speedtest/url.cc
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015 Google Inc. All rights reserved.
+ * Copyright 2016 Google Inc. All rights reserved.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/speedtest/url.h b/speedtest/url.h
index b02bdd0..4844916 100644
--- a/speedtest/url.h
+++ b/speedtest/url.h
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015 Google Inc. All rights reserved.
+ * Copyright 2016 Google Inc. All rights reserved.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/speedtest/url_test.cc b/speedtest/url_test.cc
index b43d4fb..f2945a5 100644
--- a/speedtest/url_test.cc
+++ b/speedtest/url_test.cc
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015 Google Inc. All rights reserved.
+ * Copyright 2016 Google Inc. All rights reserved.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
diff --git a/speedtest/utils.cc b/speedtest/utils.cc
index b41bb1f..580b54b 100644
--- a/speedtest/utils.cc
+++ b/speedtest/utils.cc
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015 Google Inc. All rights reserved.
+ * Copyright 2016 Google Inc. All rights reserved.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -49,6 +49,15 @@
   return buf;
 }
 
+double variance(double d1, double d2) {
+  if (d2 == 0) {
+    return 0.0;
+  }
+  double smaller = std::min(d1, d2);
+  double larger = std::max(d1, d2);
+  return 1.0 - smaller / larger;
+}
+
 double ToMegabits(long bytes, long micros) {
   return (8.0d * bytes) / micros;
 }
diff --git a/speedtest/utils.h b/speedtest/utils.h
index f3e5f1f..7e8d251 100644
--- a/speedtest/utils.h
+++ b/speedtest/utils.h
@@ -1,5 +1,5 @@
 /*
- * Copyright 2015 Google Inc. All rights reserved.
+ * Copyright 2016 Google Inc. All rights reserved.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -31,6 +31,9 @@
 // Round a double to a minimum number of significant digits
 std::string round(double d, int digits);
 
+// Return 1 - (shorter / larger)
+double variance(double d1, double d2);
+
 // Convert bytes and time in micros to speed in megabits
 double ToMegabits(long bytes, long micros);