Updated Speedtest CLI for latest Speedtest server changes
- Load server list from the global server
- Allow override for global server and server list
- Load/parse JSON config responses
- Load speedtest config form the speedtest server
- Split upload/download/ping into separate tests
- More verbose output
- Correctly determine transfer ports from server config
Change-Id: Idd8455989d822dcad1729cd8d4c19b47fa370399
diff --git a/speedtest/Makefile b/speedtest/Makefile
index 56dc3e1..b3b625c 100644
--- a/speedtest/Makefile
+++ b/speedtest/Makefile
@@ -11,20 +11,52 @@
GMOCK_DIR=googlemock
TFLAGS=$(DEBUG) -isystem ${GTEST_DIR}/include -isystem $(GMOCK_DIR)/include -pthread -std=c++11
-LIBS=-lcurl -lpthread
+LIBS=-lcurl -lpthread -ljsoncpp
TOBJS=curl_env.o url.o errors.o request.o utils.o
-OBJS=errors.o curl_env.o options.o request.o utils.o speedtest.o url.o
+OBJS=config.o \
+ curl_env.o \
+ download_test.o \
+ errors.o \
+ generic_test.o \
+ options.o \
+ ping_test.o \
+ request.o \
+ runner.o \
+ speedtest.o \
+ transfer_test.o \
+ upload_test.o \
+ url.o \
+ utils.o
all: speedtest
+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
options.o: options.cc options.h url.h
-utils.o: utils.cc options.h
-request.o: request.cc request.h curl_env.h url.h
-url.o: url.cc url.h
-speedtest.o: speedtest.cc speedtest.h curl_env.h options.h request.h url.h
+ping_test.o: ping_test.cc ping_test.h generic_test.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 \
+ options.h \
+ ping_test.h \
+ request.h \
+ runner.h \
+ upload_test.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
+utils.o: utils.cc options.h
+url.o: url.cc url.h utils.h
+
speedtest: speedtest_main.o $(OBJS)
$(CXX) -o $@ $< $(OBJS) $(LDFLAGS) $(LIBS)
@@ -45,14 +77,14 @@
libspeedtesttest.a: $(TOBJS)
ar -rv libspeedtesttest.a $(TOBJS)
-%_test.o: %_test.cc %.h
+%_test.o: %_test.cc %.h %.cc
$(CXX) -c $< $(TFLAGS) $(CXXFLAGS)
%_test: %_test.o %.o libgmock.a libspeedtesttest.a
$(CXX) -o $@ $(TFLAGS) googlemock/src/gmock_main.cc $< $*.o $(LDFLAGS) $(LIBS) libgmock.a libspeedtesttest.a
./$@
-test: options_test request_test url_test
+test: config_test options_test request_test url_test
install: speedtest
$(INSTALL) -m 0755 speedtest $(BINDIR)/
diff --git a/speedtest/config.cc b/speedtest/config.cc
new file mode 100644
index 0000000..d12b4e5
--- /dev/null
+++ b/speedtest/config.cc
@@ -0,0 +1,97 @@
+/*
+ * 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 "config.h"
+
+// For some reason, the libjsoncpp package installs to /usr/include/jsoncpp/json
+// instead of /usr{,/local}/include/json
+#include <jsoncpp/json/json.h>
+
+namespace speedtest {
+
+bool ParseConfig(const std::string &json, Config *config) {
+ if (!config) {
+ return false;
+ }
+
+ Json::Reader reader;
+ Json::Value root;
+ if (!reader.parse(json, root, false)) {
+ return false;
+ }
+
+ config->download_size = root["downloadSize"].asInt();
+ config->upload_size = root["uploadSize"].asInt();
+ config->interval_size = root["intervalSize"].asInt();
+ config->location_name = root["locationName"].asString();
+ config->min_transfer_intervals = root["minTransferIntervals"].asInt();
+ config->max_transfer_intervals = root["maxTransferIntervals"].asInt();
+ config->min_transfer_runtime = root["minTransferRunTime"].asInt();
+ config->max_transfer_runtime = root["maxTransferRunTime"].asInt();
+ config->max_transfer_variance = root["maxTransferVariance"].asDouble();
+ config->num_uploads = root["numConcurrentUploads"].asInt();
+ config->num_downloads = root["numConcurrentDownloads"].asInt();
+ config->ping_runtime = root["pingRunTime"].asInt();
+ config->ping_timeout = root["pingTimeout"].asInt();
+ config->transfer_port_start = root["transferPortStart"].asInt();
+ config->transfer_port_end = root["transferPortEnd"].asInt();
+ return true;
+}
+
+bool ParseServers(const std::string &json, std::vector<http::Url> *servers) {
+ if (!servers) {
+ return false;
+ }
+
+ Json::Reader reader;
+ Json::Value root;
+ if (!reader.parse(json, root, false)) {
+ return false;
+ }
+
+ for (const auto &it : root["regionalServers"]) {
+ http::Url url(it.asString());
+ if (!url.ok()) {
+ return false;
+ }
+ servers->emplace_back(url);
+ }
+ return true;
+}
+
+void PrintConfig(const Config &config) {
+ PrintConfig(std::cout, config);
+}
+
+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"
+ << "Location name: " << config.location_name << "\n"
+ << "Min transfer intervals: " << config.min_transfer_intervals << "\n"
+ << "Max transfer intervals: " << config.max_transfer_intervals << "\n"
+ << "Min transfer runtime: " << config.min_transfer_runtime << " ms\n"
+ << "Max transfer runtime: " << config.max_transfer_runtime << " ms\n"
+ << "Max transfer variance: " << config.max_transfer_variance << "\n"
+ << "Number of downloads: " << config.num_downloads << "\n"
+ << "Number of uploads: " << config.num_uploads << "\n"
+ << "Ping runtime: " << config.ping_runtime << " ms\n"
+ << "Ping timeout: " << config.ping_timeout << " ms\n"
+ << "Transfer port start: " << config.transfer_port_start << "\n"
+ << "Transfer port end: " << config.transfer_port_end << "\n";
+}
+
+} // namespace
diff --git a/speedtest/config.h b/speedtest/config.h
new file mode 100644
index 0000000..01b5483
--- /dev/null
+++ b/speedtest/config.h
@@ -0,0 +1,60 @@
+/*
+ * 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.
+ */
+
+#ifndef SPEEDTEST_CONFIG_H
+#define SPEEDTEST_CONFIG_H
+
+#include <iostream>
+#include <string>
+#include <vector>
+#include "url.h"
+
+namespace speedtest {
+
+struct Config {
+ int download_size = 0;
+ int upload_size = 0;
+ int interval_size = 0;
+ std::string location_name;
+ int min_transfer_intervals = 0;
+ int max_transfer_intervals = 0;
+ int min_transfer_runtime = 0;
+ int max_transfer_runtime = 0;
+ double max_transfer_variance = 0;
+ int num_downloads = 0;
+ int num_uploads = 0;
+ int ping_runtime = 0;
+ int ping_timeout = 0;
+ int transfer_port_start = 0;
+ int transfer_port_end = 0;
+};
+
+// Parses a JSON document into a config struct.
+// Returns true with the config struct populated on success.
+// Returns false if the JSON is invalid or config is null.
+bool ParseConfig(const std::string &json, Config *config);
+
+// Parses a JSON document into a list of server URLs
+// Returns true with the servers populated in the vector on success.
+// Returns false if the JSON is invalid or servers is null.
+bool ParseServers(const std::string &json, std::vector<http::Url> *servers);
+
+void PrintConfig(const Config &config);
+void PrintConfig(std::ostream &out, const Config &config);
+
+} // namespace speedtest
+
+#endif //SPEEDTEST_CONFIG_H
diff --git a/speedtest/config_test.cc b/speedtest/config_test.cc
new file mode 100644
index 0000000..681e0fc
--- /dev/null
+++ b/speedtest/config_test.cc
@@ -0,0 +1,132 @@
+/*
+ * 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 "config.h"
+
+#include <gtest/gtest.h>
+#include <gmock/gmock.h>
+
+namespace speedtest {
+namespace {
+
+const char *kValidConfig = R"CONFIG(
+{
+ "downloadSize": 10000000,
+ "intervalSize": 200,
+ "locationName": "Kansas City",
+ "maxTransferIntervals": 25,
+ "maxTransferRunTime": 20000,
+ "maxTransferVariance": 0.08,
+ "minTransferIntervals": 10,
+ "minTransferRunTime": 5000,
+ "numConcurrentDownloads": 20,
+ "numConcurrentUploads": 15,
+ "pingRunTime": 3000,
+ "pingTimeout": 300,
+ "transferPortEnd": 3023,
+ "transferPortStart": 3004,
+ "uploadSize": 20000000
+}
+)CONFIG";
+
+const char *kValidServers = R"SERVERS(
+{
+ "locationName": "Kansas City",
+ "regionalServers": [
+ "http://austin.speed.googlefiber.net/",
+ "http://kansas.speed.googlefiber.net/",
+ "http://provo.speed.googlefiber.net/",
+ "http://stanford.speed.googlefiber.net/"
+ ]
+}
+)SERVERS";
+
+const char *kInvalidServers = R"SERVERS(
+{
+ "locationName": "Kansas City",
+ "regionalServers": [
+ "example.com..",
+ ]
+}
+)SERVERS";
+
+const char *kInvalidJson = "{{}{";
+
+TEST(ParseConfigTest, NullConfig_Invalid) {
+ EXPECT_FALSE(ParseConfig(kValidConfig, nullptr));
+}
+
+TEST(ParseConfigTest, EmptyJson_Invalid) {
+ Config config;
+ EXPECT_FALSE(ParseConfig("", &config));
+}
+
+TEST(ParseConfigTest, InvalidJson_Invalid) {
+ Config config;
+ EXPECT_FALSE(ParseConfig(kInvalidJson, &config));
+}
+
+TEST(ParseConfigTest, FullConfig_Valid) {
+ Config config;
+ EXPECT_TRUE(ParseConfig(kValidConfig, &config));
+ EXPECT_EQ(10000000, config.download_size);
+ 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("Kansas City", config.location_name);
+ EXPECT_EQ(10, config.min_transfer_intervals);
+ EXPECT_EQ(25, config.max_transfer_intervals);
+ EXPECT_EQ(5000, config.min_transfer_runtime);
+ EXPECT_EQ(20000, config.max_transfer_runtime);
+ EXPECT_EQ(0.08, config.max_transfer_variance);
+ EXPECT_EQ(3000, config.ping_runtime);
+ EXPECT_EQ(300, config.ping_timeout);
+ EXPECT_EQ(3004, config.transfer_port_start);
+ EXPECT_EQ(3023, config.transfer_port_end);
+}
+
+TEST(ParseServersTest, NullServers_Invalid) {
+ EXPECT_FALSE(ParseServers(kValidServers, nullptr));
+}
+
+TEST(ParseServersTest, EmptyServers_Invalid) {
+ std::vector<http::Url> servers;
+ EXPECT_FALSE(ParseServers("", &servers));
+}
+
+TEST(ParseServersTest, InvalidJson_Invalid) {
+ std::vector<http::Url> servers;
+ EXPECT_FALSE(ParseServers(kInvalidJson, &servers));
+}
+
+TEST(ParseServersTest, FullServers_Valid) {
+ std::vector<http::Url> servers;
+ EXPECT_TRUE(ParseServers(kValidServers, &servers));
+ EXPECT_THAT(servers, testing::UnorderedElementsAre(
+ http::Url("http://austin.speed.googlefiber.net/"),
+ http::Url("http://kansas.speed.googlefiber.net/"),
+ http::Url("http://provo.speed.googlefiber.net/"),
+ http::Url("http://stanford.speed.googlefiber.net/")));
+}
+
+TEST(ParseServersTest, InvalidServers_Invalid) {
+ std::vector<http::Url> servers;
+ EXPECT_FALSE(ParseServers(kInvalidServers, &servers));
+}
+
+} // namespace
+} // namespace speedtest
diff --git a/speedtest/curl_env.cc b/speedtest/curl_env.cc
index c02835c..dd21dd1 100644
--- a/speedtest/curl_env.cc
+++ b/speedtest/curl_env.cc
@@ -18,34 +18,102 @@
#include <cstdlib>
#include <iostream>
-#include <curl/curl.h>
#include "errors.h"
#include "request.h"
namespace http {
+namespace {
-CurlEnv::CurlEnv() {
- init(CURL_GLOBAL_NOTHING);
+void LockFn(CURL *handle,
+ curl_lock_data data,
+ curl_lock_access access,
+ void *userp) {
+ CurlEnv *env = static_cast<CurlEnv *>(userp);
+ env->Lock(data);
}
-CurlEnv::CurlEnv(int init_options) {
- init(init_options);
+void UnlockFn(CURL *handle, curl_lock_data data, void *userp) {
+ CurlEnv *env = static_cast<CurlEnv *>(userp);
+ env->Unlock(data);
}
-CurlEnv::~CurlEnv() {
- curl_global_cleanup();
-}
+} // namespace
-std::unique_ptr<Request> CurlEnv::NewRequest() {
- return std::unique_ptr<Request>(new Request(shared_from_this()));
-}
-
-void CurlEnv::init(int init_flags) {
- CURLcode status = curl_global_init(init_flags);
+CurlEnv::CurlEnv(const Options &options)
+ : options_(options),
+ set_max_connections_(false),
+ share_(nullptr) {
+ CURLcode status;
+ {
+ std::lock_guard <std::mutex> lock(curl_mutex_);
+ status = curl_global_init(options_.curl_options);
+ }
if (status != 0) {
std::cerr << "Curl initialization failed: " << ErrorString(status);
std::exit(1);
}
+ if (!options_.disable_dns_cache) {
+ share_ = curl_share_init();
+ curl_share_setopt(share_, CURLSHOPT_SHARE, CURL_LOCK_DATA_DNS);
+ curl_share_setopt(share_, CURLSHOPT_USERDATA, this);
+ curl_share_setopt(share_, CURLSHOPT_LOCKFUNC, &LockFn);
+ curl_share_setopt(share_, CURLSHOPT_UNLOCKFUNC, &UnlockFn);
+ }
+}
+
+CurlEnv::~CurlEnv() {
+ curl_share_cleanup(share_);
+ share_ = nullptr;
+ curl_global_cleanup();
+}
+
+std::unique_ptr<Request> CurlEnv::NewRequest(const Url &url) {
+ // curl_global_init is not threadsafe and calling curl_easy_init may
+ // implicitly call it so we need to mutex lock on creating all requests
+ // to ensure the global initialization is done in a threadsafe manner.
+ std::lock_guard <std::mutex> lock(curl_mutex_);
+
+ // We use an aliasing constructor on a shared_ptr to keep a reference to
+ // CurlEnv as when the refcount drops to 0 we want to do global cleanup.
+ // So the CURL handle for this shared_ptr is _unmanaged_ and the Request
+ // object is responsible for cleaning it up, which involves calling
+ // curl_easy_cleanup().
+ //
+ // This way Request doesn't need to know about CurlEnv at all, while
+ // all Request instances will still keep an implicit reference to
+ // CurlEnv.
+ std::shared_ptr<CURL> handle(shared_from_this(), curl_easy_init());
+
+ // For some reason libcurl sets the max connections on a handle.
+ // According to the docs, doing so when there are open connections may
+ // close them so we maintain this boolean so as to set the maximum
+ // number of connections on the connection pool associated with this
+ // handle before any connections are opened.
+ if (!set_max_connections_ && options_.max_connections > 0) {
+ curl_easy_setopt(handle.get(),
+ CURLOPT_MAXCONNECTS,
+ options_.max_connections);
+ set_max_connections_ = true;
+ }
+
+ curl_easy_setopt(handle.get(), CURLOPT_SHARE, share_);
+ return std::unique_ptr<Request>(new Request(handle, url));
+}
+
+void CurlEnv::Lock(curl_lock_data lock_type) {
+ if (lock_type == CURL_LOCK_DATA_DNS) {
+ // It is ill-advised to call lock directly but libcurl uses
+ // separate lock/unlock functions.
+ dns_mutex_.lock();
+ }
+}
+
+void CurlEnv::Unlock(curl_lock_data lock_type) {
+ if (lock_type == CURL_LOCK_DATA_DNS) {
+ // It is ill-advised to call lock directly but libcurl uses
+ // separate lock/unlock functiounknns.
+ dns_mutex_.unlock();
+ }
}
} // namespace http
diff --git a/speedtest/curl_env.h b/speedtest/curl_env.h
index 09511fa..5b1c89d 100644
--- a/speedtest/curl_env.h
+++ b/speedtest/curl_env.h
@@ -17,27 +17,43 @@
#ifndef HTTP_CURL_ENV_H
#define HTTP_CURL_ENV_H
+#include <curl/curl.h>
#include <memory>
+#include <mutex>
+#include "url.h"
namespace http {
class Request;
-// Curl initialization to cleanup automatically
class CurlEnv : public std::enable_shared_from_this<CurlEnv> {
public:
- CurlEnv();
- explicit CurlEnv(int init_options);
+ struct Options {
+ int curl_options = CURL_GLOBAL_NOTHING;
+ bool disable_dns_cache = false;
+ int max_connections = 0;
+ };
+
+ explicit CurlEnv(const Options &options);
virtual ~CurlEnv();
- std::unique_ptr<Request> NewRequest();
+ std::unique_ptr<Request> NewRequest(const Url &url);
+
+ void Lock(curl_lock_data lock_type);
+ void Unlock(curl_lock_data lock_type);
private:
- void init(int flags);
+ Options options_;
+
+ // used to lock on curl global state
+ std::mutex curl_mutex_;
+ bool set_max_connections_;
+
+ std::mutex dns_mutex_;
+ CURLSH *share_; // owned
// disable
CurlEnv(const CurlEnv &other) = delete;
-
void operator=(const CurlEnv &other) = delete;
};
diff --git a/speedtest/download_test.cc b/speedtest/download_test.cc
new file mode 100644
index 0000000..f23d97a
--- /dev/null
+++ b/speedtest/download_test.cc
@@ -0,0 +1,79 @@
+/*
+ * 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 "download_test.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_),
+ options_(options) {
+ assert(options_.num_transfers > 0);
+ assert(options_.download_size > 0);
+}
+
+void DownloadTest::RunInternal() {
+ ResetCounters();
+ threads_.clear();
+ if (options_.verbose) {
+ std::cout << "Downloading " << options_.num_transfers
+ << " threads with " << options_.download_size << " bytes\n";
+ }
+ for (int i = 0; i < options_.num_transfers; ++i) {
+ threads_.emplace_back([=]{
+ RunDownload(i);
+ });
+ }
+}
+
+void DownloadTest::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) {
+ long downloaded = 0;
+ download->set_param("i", to_string(id));
+ download->set_param("size", to_string(options_.download_size));
+ download->set_param("time", to_string(SystemTimeMicros()));
+ download->set_progress_fn([&](curl_off_t,
+ curl_off_t dlnow,
+ curl_off_t,
+ curl_off_t) -> bool {
+ if (dlnow > downloaded) {
+ TransferBytes(dlnow - downloaded);
+ downloaded = dlnow;
+ }
+ return GetStatus() != TestStatus::RUNNING;
+ });
+ StartRequest();
+ download->Get();
+ EndRequest();
+ download->Reset();
+ }
+}
+
+} // namespace speedtest
diff --git a/speedtest/download_test.h b/speedtest/download_test.h
new file mode 100644
index 0000000..f8a249f
--- /dev/null
+++ b/speedtest/download_test.h
@@ -0,0 +1,51 @@
+/*
+ * 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.
+ */
+
+#ifndef SPEEDTEST_DOWNLOAD_TEST_H
+#define SPEEDTEST_DOWNLOAD_TEST_H
+
+#include <thread>
+#include <vector>
+#include "transfer_test.h"
+
+namespace speedtest {
+
+class DownloadTest : public TransferTest {
+ public:
+ struct Options : TransferTest::Options {
+ int download_size = 0;
+ };
+
+ explicit DownloadTest(const Options &options);
+
+ protected:
+ void RunInternal() override;
+ void StopInternal() override;
+
+ private:
+ void RunDownload(int id);
+
+ Options options_;
+ std::vector<std::thread> threads_;
+
+ // disallowed
+ DownloadTest(const DownloadTest &) = delete;
+ void operator=(const DownloadTest &) = delete;
+};
+
+} // namespace speedtest
+
+#endif // SPEEDTEST_DOWNLOAD_TEST_H
diff --git a/speedtest/generic_test.cc b/speedtest/generic_test.cc
new file mode 100644
index 0000000..be012ba
--- /dev/null
+++ b/speedtest/generic_test.cc
@@ -0,0 +1,92 @@
+/*
+ * 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/generic_test.h b/speedtest/generic_test.h
new file mode 100644
index 0000000..cf0d37c
--- /dev/null
+++ b/speedtest/generic_test.h
@@ -0,0 +1,75 @@
+/*
+ * 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.
+ */
+
+#ifndef SPEEDTEST_GENERIC_TEST_H
+#define SPEEDTEST_GENERIC_TEST_H
+
+#include <condition_variable>
+#include <functional>
+#include <memory>
+#include <mutex>
+#include "request.h"
+
+namespace speedtest {
+
+enum class TestStatus {
+ NOT_STARTED,
+ RUNNING,
+ STOPPING,
+ STOPPED
+};
+
+const char *AsString(TestStatus status);
+
+class GenericTest {
+ public:
+ using RequestPtr = std::unique_ptr<http::Request>;
+
+ struct Options {
+ bool verbose = false;
+ std::function<RequestPtr(int)> request_factory;
+ };
+
+ explicit GenericTest(const Options &options);
+
+ void Run();
+ void Stop();
+
+ TestStatus GetStatus() const;
+ long GetRunningTime() const;
+ void WaitForEnd();
+
+ protected:
+ virtual void RunInternal() = 0;
+ virtual void StopInternal() {}
+
+ private:
+ mutable std::mutex mutex_;
+ std::condition_variable condition_;
+ TestStatus status_;
+ bool running_ = false;
+ long start_time_;
+ long end_time_;
+
+ // disallowed
+ GenericTest(const GenericTest &) = delete;
+
+ void operator=(const GenericTest &) = delete;
+};
+
+} // namespace speedtest
+
+#endif //SPEEDTEST_GENERIC_TEST_H
diff --git a/speedtest/options.cc b/speedtest/options.cc
index 4d07099..db7f3ae 100644
--- a/speedtest/options.cc
+++ b/speedtest/options.cc
@@ -25,7 +25,7 @@
namespace speedtest {
-const char* kDefaultHost = "speedtest.googlefiber.net";
+const char* kDefaultHost = "any.speed.gfsvc.com";
namespace {
@@ -58,67 +58,98 @@
return true;
}
-const char *kShortOpts = "d:u:t:n:p:s:vh";
+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 char *kShortOpts = "hvg:a:d:s:t:u:p:";
struct option kLongOpts[] = {
{"help", no_argument, nullptr, 'h'},
- {"number", required_argument, nullptr, 'n'},
- {"download_size", required_argument, nullptr, 'd'},
- {"upload_size", required_argument, nullptr, 'u'},
- {"progress", required_argument, nullptr, 'p'},
- {"serverid", required_argument, nullptr, 's'}, // ignored
- {"time", required_argument, nullptr, 't'},
{"verbose", no_argument, nullptr, 'v'},
+ {"global_host", required_argument, nullptr, 'g'},
+ {"user_agent", required_argument, nullptr, 'a'},
+ {"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},
+ {"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 kDefaultNumber = 10;
-const int kDefaultDownloadSize = 10000000;
-const int kDefaultUploadSize = 10000000;
-const int kDefaultTimeMillis = 5000;
const int kMaxNumber = 1000;
const int kMaxProgress = 1000000;
+const char *kSpeedtestHelp = R"USAGE(: run an HTTP speedtest.
+
+If no hosts are specified, the global host is queried for a list
+of servers to use, otherwise the list of supplied hosts will be
+used. Each will be pinged several times and the one with the
+lowest ping time will be used. If only one host is supplied, it
+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
+
+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;
+)USAGE";
+
} // namespace
bool ParseOptions(int argc, char *argv[], Options *options) {
assert(options != nullptr);
- options->number = kDefaultNumber;
- options->download_size = kDefaultDownloadSize;
- options->upload_size = kDefaultUploadSize;
- options->time_millis = kDefaultTimeMillis;
- options->progress_millis = 0;
- options->verbose = false;
options->usage = false;
+ options->verbose = false;
+ options->global_host = http::Url(kDefaultHost);
+ options->global = false;
+ options->disable_dns_cache = false;
+ options->max_connections = 0;
+ 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->ping_runtime = 0;
+ options->ping_timeout = 0;
options->hosts.clear();
+ if (!options->global_host.ok()) {
+ std::cerr << "Invalid global host " << kDefaultHost << "\n";
+ return false;
+ }
+
// Manually set this to 0 to allow repeated calls
optind = 0;
-
int opt = 0, long_index = 0;
while ((opt = getopt_long(argc, argv,
kShortOpts, kLongOpts, &long_index)) != -1) {
switch (opt) {
- case 'd':
- if (!ParseSize(optarg, &options->download_size)) {
- std::cerr << "Invalid download size '" << optarg << "'\n";
- return false;
- }
+ case 'a':
+ options->user_agent = optarg;
break;
- case 'u':
- if (!ParseSize(optarg, &options->upload_size)) {
- std::cerr << "Invalid upload size '" << optarg << "'\n";
- return false;
- }
- break;
- case 't':
- options->time_millis = atoi(optarg);
- break;
- case 'v':
- options->verbose = true;
- break;
- case 'h':
- options->usage = true;
- return true;
- case 'n': {
+ case 'd': {
long number;
char *endptr;
if (!ParseLong(optarg, &endptr, &number)) {
@@ -127,12 +158,24 @@
}
if (number < 1 || number > kMaxNumber) {
std::cerr << "Number must be between 1 and " << kMaxNumber
- << ", got '" << optarg << "'\n";
+ << ", got '" << optarg << "'\n";
return false;
}
- options->number = static_cast<int>(number);
+ options->num_downloads = static_cast<int>(number);
break;
}
+ case 'g': {
+ http::Url url(optarg);
+ if (!url.ok()) {
+ std::cerr << "Invalid global host " << optarg << "\n";
+ return false;
+ }
+ options->global_host = url;
+ break;
+ }
+ case 'h':
+ options->usage = true;
+ return true;
case 'p': {
long progress;
char *endptr;
@@ -142,16 +185,122 @@
}
if (progress < 0 || progress > kMaxProgress) {
std::cerr << "Number must be between 0 and " << kMaxProgress
- << ", got '" << optarg << "'\n";
+ << ", got '" << optarg << "'\n";
return false;
}
options->progress_millis = static_cast<int>(progress);
break;
}
case 's':
- // serverid is an argument supported by the older speedtest
- // implementation. It is ignored here to ease the transition.
+ if (!ParseSize(optarg, &options->download_size)) {
+ std::cerr << "Invalid download size '" << optarg << "'\n";
+ return false;
+ }
break;
+ case 't':
+ if (!ParseSize(optarg, &options->upload_size)) {
+ std::cerr << "Invalid upload size '" << optarg << "'\n";
+ return false;
+ }
+ break;
+ case 'u': {
+ long number;
+ char *endptr;
+ if (!ParseLong(optarg, &endptr, &number)) {
+ std::cerr << "Could not parse number '" << optarg << "'\n";
+ return false;
+ }
+ if (number < 1 || number > kMaxNumber) {
+ std::cerr << "Number must be between 1 and " << kMaxNumber
+ << ", got '" << optarg << "'\n";
+ return false;
+ }
+ options->num_uploads = static_cast<int>(number);
+ break;
+ }
+ case 'v':
+ options->verbose = true;
+ break;
+ case kOptMinTransferTime: {
+ long transfer_time;
+ char *endptr;
+ if (!ParseLong(optarg, &endptr, &transfer_time)) {
+ std::cerr << "Could not parse minimum transfer time '"
+ << optarg << "'\n";
+ return false;
+ }
+ if (transfer_time < 0) {
+ std::cerr << "Minimum transfer time must be nonnegative, got "
+ << optarg << "'\n";
+ return false;
+ }
+ options->min_transfer_time = static_cast<int>(transfer_time);
+ break;
+ }
+ case kOptMaxTransferTime: {
+ long transfer_time;
+ char *endptr;
+ if (!ParseLong(optarg, &endptr, &transfer_time)) {
+ std::cerr << "Could not parse maximum transfer time '"
+ << optarg << "'\n";
+ return false;
+ }
+ if (transfer_time < 0) {
+ std::cerr << "Maximum transfer must be nonnegative, got "
+ << optarg << "'\n";
+ return false;
+ }
+ options->max_transfer_time = static_cast<int>(transfer_time);
+ break;
+ }
+ case kOptPingRuntime: {
+ long ping_runtime;
+ char *endptr;
+ if (!ParseLong(optarg, &endptr, &ping_runtime)) {
+ std::cerr << "Could not parse ping time '" << optarg << "'\n";
+ return false;
+ }
+ if (ping_runtime < 0) {
+ std::cerr << "Ping time must be nonnegative, got "
+ << optarg << "'\n";
+ return false;
+ }
+ options->ping_runtime = static_cast<int>(ping_runtime);
+ break;
+ }
+ case kOptPingTimeout: {
+ long ping_timeout;
+ char *endptr;
+ if (!ParseLong(optarg, &endptr, &ping_timeout)) {
+ std::cerr << "Could not parse ping timeout '" << optarg << "'\n";
+ return false;
+ }
+ if (ping_timeout < 0) {
+ std::cerr << "Ping timeout must be nonnegative, got "
+ << optarg << "'\n";
+ return false;
+ }
+ 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;
}
@@ -174,7 +323,7 @@
}
}
if (options->hosts.empty()) {
- options->hosts.emplace_back(http::Url(kDefaultHost));
+ options->global = true;
}
return true;
}
@@ -184,13 +333,23 @@
}
void PrintOptions(std::ostream &out, const Options &options) {
- out << "Number: " << options.number << "\n"
- << "Upload size: " << options.upload_size << "\n"
- << "Download size: " << options.download_size << "\n"
- << "Time: " << options.time_millis << " ms\n"
- << "Progress interval: " << options.progress_millis << " ms\n"
+ out << "Usage: " << (options.usage ? "true" : "false") << "\n"
<< "Verbose: " << (options.verbose ? "true" : "false") << "\n"
- << "Usage: " << (options.usage ? "true" : "false") << "\n"
+ << "Global host: " << options.global_host.url() << "\n"
+ << "Global: " << (options.global ? "true" : "false") << "\n"
+ << "User agent: " << options.user_agent << "\n"
+ << "Disable DNS cache: "
+ << (options.disable_dns_cache ? "true" : "false") << "\n"
+ << "Max connections: " << options.max_connections << "\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"
+ << "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";
@@ -205,15 +364,7 @@
assert(app_path != nullptr);
const char *last_slash = strrchr(app_path, '/');
const char *app_name = last_slash == nullptr ? app_path : last_slash + 1;
- out << basename(app_name) << ": run an HTTP speedtest\n\n"
- << "Usage: speedtest [options] <host> ...\n"
- << " -h, --help This help text\n"
- << " -n, --number NUM Number of simultaneous transfers\n"
- << " -d, --download_size SIZE Download size in bytes\n"
- << " -u, --upload_size SIZE Upload size in bytes\n"
- << " -t, --time TIME Time per test in milliseconds\n"
- << " -p, --progress TIME Progress intervals in milliseconds\n"
- << " -v, --verbose Verbose output\n";
+ out << basename(app_name) << kSpeedtestHelp;
}
} // namespace speedtest
diff --git a/speedtest/options.h b/speedtest/options.h
index 0996798..0413798 100644
--- a/speedtest/options.h
+++ b/speedtest/options.h
@@ -27,14 +27,26 @@
extern const char* kDefaultHost;
struct Options {
- std::vector<http::Url> hosts;
- int number;
- long download_size;
- long upload_size;
- int time_millis;
- int progress_millis;
bool verbose;
bool usage;
+ http::Url global_host;
+ bool global;
+ std::string user_agent;
+ bool disable_dns_cache;
+ int max_connections;
+
+ // 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;
+
+ std::vector<http::Url> hosts;
};
// Parse command line options putting results into 'options'
diff --git a/speedtest/options_test.cc b/speedtest/options_test.cc
index 2754552..113f730 100644
--- a/speedtest/options_test.cc
+++ b/speedtest/options_test.cc
@@ -14,12 +14,14 @@
* limitations under the License.
*/
+#include "options.h"
+
#include <gtest/gtest.h>
#include <gmock/gmock.h>
#include <mutex>
#include <string.h>
#include <vector>
-#include "options.h"
+#include "url.h"
namespace speedtest {
namespace {
@@ -81,8 +83,22 @@
TEST(OptionsTest, Empty_ValidDefault) {
Options options;
TestValidOptions({}, &options);
+ 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.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.ping_runtime);
+ EXPECT_EQ(0, options.ping_timeout);
+ EXPECT_THAT(options.hosts, testing::IsEmpty());
}
TEST(OptionsTest, Usage_Valid) {
@@ -104,35 +120,68 @@
EXPECT_THAT(options.hosts, testing::ElementsAre(http::Url("efgh")));
}
-TEST(OptionsTest, FullShort_Valid) {
+TEST(OptionsTest, ShortOptions_Valid) {
Options options;
- TestValidOptions({"-d", "5122",
- "-u", "7653",
- "-t", "123",
- "-n", "15",
- "-p", "500"},
+ TestValidOptions({"-s", "5122",
+ "-t", "7653",
+ "-d", "20",
+ "-u", "15",
+ "-p", "500",
+ "-g", "speed.gfsvc.com",
+ "-a", "CrOS",
+ "foo.speed.googlefiber.net",
+ "bar.speed.googlefiber.net"},
&options);
EXPECT_EQ(5122, options.download_size);
EXPECT_EQ(7653, options.upload_size);
- EXPECT_EQ(123, options.time_millis);
- EXPECT_EQ(15, options.number);
+ EXPECT_EQ(20, options.num_downloads);
+ EXPECT_EQ(15, options.num_uploads);
EXPECT_EQ(500, options.progress_millis);
+ 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.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")));
}
-TEST(OptionsTest, FullLong_Valid) {
+TEST(OptionsTest, LongOptions_Valid) {
Options options;
TestValidOptions({"--download_size", "5122",
"--upload_size", "7653",
- "--time", "123",
"--progress", "1000",
- "--number", "12"},
+ "--num_uploads", "12",
+ "--num_downloads", "16",
+ "--global_host", "speed.gfsvc.com",
+ "--user_agent", "CrOS",
+ "--disable_dns_cache",
+ "--max_connections", "23",
+ "--min_transfer_time", "7500",
+ "--max_transfer_time", "13500",
+ "--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(123, options.time_millis);
- EXPECT_EQ(12, options.number);
+ 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_THAT(options.hosts, testing::ElementsAre(http::Url(kDefaultHost)));
+ 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(2500, options.ping_runtime);
+ EXPECT_EQ(300, options.ping_timeout);
+ EXPECT_THAT(options.hosts, testing::UnorderedElementsAre(
+ http::Url("foo.speed.googlefiber.net"),
+ http::Url("bar.speed.googlefiber.net")));
}
} // namespace
diff --git a/speedtest/ping_test.cc b/speedtest/ping_test.cc
new file mode 100644
index 0000000..999b53d
--- /dev/null
+++ b/speedtest/ping_test.cc
@@ -0,0 +1,117 @@
+/*
+ * 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 "ping_test.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),
+ options_(options) {
+ assert(options_.num_pings > 0);
+}
+
+void PingTest::RunInternal() {
+ ResetCounters();
+ success_ = false;
+ threads_.clear();
+ for (int i = 0; i < options_.num_pings; ++i) {
+ threads_.emplace_back([=]() {
+ RunPing(i);
+ });
+ }
+}
+
+void PingTest::StopInternal() {
+ std::for_each(threads_.begin(), threads_.end(), [](std::thread &t) {
+ t.join();
+ });
+ threads_.clear();
+ if (options_.verbose) {
+ std::cout << "Pinged " << options_.num_pings << " "
+ << (options_.num_pings == 1 ? "host" : "hosts") << ":\n";
+ }
+ const PingStats *min_stats = nullptr;
+ for (const auto &stat : stats_) {
+ if (options_.verbose) {
+ std::cout << " " << stat.url.url() << ": ";
+ if (stat.pings_received == 0) {
+ std::cout << "no packets received";
+ } else {
+ double mean_micros = ((double) stat.total_micros) / stat.pings_received;
+ std::cout << "min " << round(stat.min_micros / 1000.0d, 2) << " ms"
+ << " from " << stat.pings_received << " pings"
+ << " (mean " << round(mean_micros / 1000.0d, 2) << " ms)";
+ }
+ std::cout << "\n";
+ }
+ if (stat.pings_received > 0) {
+ if (!min_stats || stat.min_micros < min_stats->min_micros) {
+ min_stats = &stat;
+ }
+ }
+ }
+
+ std::lock_guard<std::mutex> lock(mutex_);
+ 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);
+ stats_[index].url = ping->url();
+ while (GetStatus() == TestStatus::RUNNING) {
+ long req_start = SystemTimeMicros();
+ if (ping->Get() == CURLE_OK) {
+ long req_end = SystemTimeMicros();
+ long ping_time = req_end - req_start;
+ stats_[index].total_micros += ping_time;
+ stats_[index].pings_received++;
+ stats_[index].min_micros = std::min(stats_[index].min_micros, ping_time);
+ }
+ ping->Reset();
+ std::this_thread::sleep_for(std::chrono::milliseconds(100));
+ }
+}
+
+bool PingTest::IsSucceeded() const {
+ return success_;
+}
+
+PingStats PingTest::GetFastest() const {
+ std::lock_guard<std::mutex> lock(mutex_);
+ return fastest_;
+}
+
+void PingTest::ResetCounters() {
+ stats_.clear();
+ stats_.resize(options_.num_pings);
+}
+
+} // namespace speedtest
diff --git a/speedtest/ping_test.h b/speedtest/ping_test.h
new file mode 100644
index 0000000..558c799
--- /dev/null
+++ b/speedtest/ping_test.h
@@ -0,0 +1,78 @@
+/*
+ * 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.
+ */
+
+#ifndef PING_TEST_H
+#define PING_TEST_H
+
+#include <atomic>
+#include <functional>
+#include <limits>
+#include <memory>
+#include <mutex>
+#include <thread>
+#include <vector>
+#include "generic_test.h"
+#include "request.h"
+#include "url.h"
+
+namespace speedtest {
+
+struct PingStats {
+ long total_micros = 0;
+ int pings_received = 0;
+ long min_micros = std::numeric_limits<long>::max();
+ http::Url url;
+};
+
+class PingTest : public GenericTest {
+ public:
+ struct Options : GenericTest::Options {
+ int timeout = 0;
+ int num_pings = 0;
+ std::function<RequestPtr(int)> request_factory;
+ };
+
+ explicit PingTest(const Options &options);
+
+ bool IsSucceeded() const;
+
+ PingStats GetFastest() const;
+
+ protected:
+ void RunInternal() override;
+ void StopInternal() override;
+
+ private:
+ void RunPing(size_t index);
+
+ void ResetCounters();
+
+ Options options_;
+ std::vector<PingStats> stats_;
+ std::vector<std::thread> threads_;
+ std::atomic_bool success_;
+
+ mutable std::mutex mutex_;
+ PingStats fastest_;
+
+ // disallowed
+ PingTest(const PingTest &) = delete;
+ void operator=(const PingTest &) = delete;
+};
+
+} // namespace speedtest
+
+#endif // PING_TEST_H
diff --git a/speedtest/request.cc b/speedtest/request.cc
index b0a2b89..1542053 100644
--- a/speedtest/request.cc
+++ b/speedtest/request.cc
@@ -64,17 +64,14 @@
const int kDefaultQueryStringSize = 200;
+const Request::DownloadFn noop = [](void *, size_t) { };
+
} // namespace
-Request::Request(std::shared_ptr<CurlEnv> env)
- : curl_headers_(nullptr),
- env_(env) {
- assert(env_);
- handle_ = curl_easy_init();
- if (!handle_) {
- std::cerr << "Failed to create handle\n";
- std::exit(1);
- }
+Request::Request(std::shared_ptr<CURL> handle, const Url &url)
+ : handle_(handle),
+ curl_headers_(nullptr),
+ url_(url) {
}
Request::~Request() {
@@ -82,35 +79,40 @@
curl_slist_free_all(curl_headers_);
curl_headers_ = nullptr;
}
- curl_easy_cleanup(handle_);
+
+ curl_easy_cleanup(handle_.get());
+}
+
+CURLcode Request::Get() {
+ return Get(noop);
}
CURLcode Request::Get(DownloadFn download_fn) {
CommonSetup();
if (download_fn) {
- curl_easy_setopt(handle_, CURLOPT_WRITEFUNCTION, &WriteCallback);
- curl_easy_setopt(handle_, CURLOPT_WRITEDATA, &download_fn);
+ curl_easy_setopt(handle_.get(), CURLOPT_WRITEFUNCTION, &WriteCallback);
+ curl_easy_setopt(handle_.get(), CURLOPT_WRITEDATA, &download_fn);
}
return Execute();
}
CURLcode Request::Post(UploadFn upload_fn) {
CommonSetup();
- curl_easy_setopt(handle_, CURLOPT_UPLOAD, 1);
- curl_easy_setopt(handle_, CURLOPT_READFUNCTION, &ReadCallback);
- curl_easy_setopt(handle_, CURLOPT_READDATA, &upload_fn);
+ curl_easy_setopt(handle_.get(), CURLOPT_UPLOAD, 1);
+ curl_easy_setopt(handle_.get(), CURLOPT_READFUNCTION, &ReadCallback);
+ curl_easy_setopt(handle_.get(), CURLOPT_READDATA, &upload_fn);
return Execute();
}
CURLcode Request::Post(const char *data, curl_off_t data_len) {
CommonSetup();
- curl_easy_setopt(handle_, CURLOPT_POSTFIELDSIZE_LARGE, data_len);
- curl_easy_setopt(handle_, CURLOPT_POSTFIELDS, data);
+ curl_easy_setopt(handle_.get(), CURLOPT_POSTFIELDSIZE_LARGE, data_len);
+ curl_easy_setopt(handle_.get(), CURLOPT_POSTFIELDS, data);
return Execute();
}
void Request::Reset() {
- curl_easy_reset(handle_);
+ curl_easy_reset(handle_.get());
clear_progress_fn();
clear_headers();
clear_params();
@@ -156,6 +158,10 @@
params_.clear();
}
+void Request::set_timeout_millis(long millis) {
+ curl_easy_setopt(handle_.get(), CURLOPT_TIMEOUT_MS, millis);
+}
+
void Request::UpdateUrl() {
std::string query_string;
query_string.reserve(kDefaultQueryStringSize);
@@ -165,10 +171,10 @@
if (!query_string.empty()) {
query_string.append("&");
}
- char *name = curl_easy_escape(handle_,
+ char *name = curl_easy_escape(handle_.get(),
iter->first.data(),
iter->first.length());
- char *value = curl_easy_escape(handle_,
+ char *value = curl_easy_escape(handle_.get(),
iter->second.data(),
iter->second.length());
query_string.append(name);
@@ -183,12 +189,14 @@
void Request::CommonSetup() {
UpdateUrl();
std::string request_url = url_.url();
- curl_easy_setopt(handle_, CURLOPT_URL, request_url.c_str());
- curl_easy_setopt(handle_, CURLOPT_USERAGENT, user_agent_.c_str());
+ curl_easy_setopt(handle_.get(), CURLOPT_URL, request_url.c_str());
+ curl_easy_setopt(handle_.get(), CURLOPT_USERAGENT, user_agent_.c_str());
if (progress_fn_) {
- curl_easy_setopt(handle_, CURLOPT_NOPROGRESS, 0);
- curl_easy_setopt(handle_, CURLOPT_XFERINFOFUNCTION, &ProgressCallback);
- curl_easy_setopt(handle_, CURLOPT_XFERINFODATA, &progress_fn_);
+ curl_easy_setopt(handle_.get(), CURLOPT_NOPROGRESS, 0);
+ curl_easy_setopt(handle_.get(),
+ CURLOPT_XFERINFOFUNCTION,
+ &ProgressCallback);
+ curl_easy_setopt(handle_.get(), CURLOPT_XFERINFODATA, &progress_fn_);
}
if (!headers_.empty()) {
struct curl_slist *headers = nullptr;
@@ -200,12 +208,12 @@
header.append(iter->second);
headers = curl_slist_append(headers, header.c_str());
}
- curl_easy_setopt(handle_, CURLOPT_HTTPHEADER, headers);
+ curl_easy_setopt(handle_.get(), CURLOPT_HTTPHEADER, headers);
}
}
CURLcode Request::Execute() {
- return curl_easy_perform(handle_);
+ return curl_easy_perform(handle_.get());
}
} // namespace http
diff --git a/speedtest/request.h b/speedtest/request.h
index 837eecc..f3344d2 100644
--- a/speedtest/request.h
+++ b/speedtest/request.h
@@ -22,8 +22,6 @@
#include <map>
#include <memory>
#include <string>
-
-#include "curl_env.h"
#include "url.h"
namespace http {
@@ -45,9 +43,10 @@
curl_off_t,
curl_off_t)>;
- explicit Request(std::shared_ptr<CurlEnv> env);
+ Request(std::shared_ptr<CURL> handle, const Url &url);
virtual ~Request();
+ CURLcode Get();
CURLcode Get(DownloadFn download_fn);
CURLcode Post(UploadFn upload_fn);
CURLcode Post(const char *data, curl_off_t data_len);
@@ -80,24 +79,25 @@
void set_progress_fn(ProgressFn progress_fn) { progress_fn_ = progress_fn; }
void clear_progress_fn() { progress_fn_ = nullptr; }
+ // Request timeout
+ void set_timeout_millis(long millis);
+
void UpdateUrl();
private:
void CommonSetup();
+
CURLcode Execute();
// owned
- CURL *handle_;
+ std::shared_ptr<CURL> handle_;
struct curl_slist *curl_headers_;
-
- // ref-count CURL global config
- std::shared_ptr<CurlEnv> env_;
Url url_;
std::string user_agent_;
Headers headers_;
QueryStringParams params_;
- ProgressFn progress_fn_; // unowned
+ ProgressFn progress_fn_;
// disable
Request(const Request &) = delete;
diff --git a/speedtest/request_test.cc b/speedtest/request_test.cc
index d8b11e9..859cd2b 100644
--- a/speedtest/request_test.cc
+++ b/speedtest/request_test.cc
@@ -14,10 +14,9 @@
* limitations under the License.
*/
-#include <gtest/gtest.h>
-
#include "request.h"
+#include <gtest/gtest.h>
#include <memory>
#include "curl_env.h"
diff --git a/speedtest/runner.cc b/speedtest/runner.cc
new file mode 100644
index 0000000..0dae542
--- /dev/null
+++ b/speedtest/runner.cc
@@ -0,0 +1,35 @@
+/*
+ * 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 "runner.h"
+
+#include <cassert>
+#include <thread>
+
+namespace speedtest {
+
+void TimedRun(GenericTest *test, long millis) {
+ assert(test);
+ test->Run();
+ std::thread timer([=] {
+ std::this_thread::sleep_for(
+ std::chrono::milliseconds(millis));
+ test->Stop();
+ });
+ timer.join();
+}
+
+} // namespace speedtest
diff --git a/speedtest/runner.h b/speedtest/runner.h
new file mode 100644
index 0000000..4d677fc
--- /dev/null
+++ b/speedtest/runner.h
@@ -0,0 +1,28 @@
+/*
+ * 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.
+ */
+
+#ifndef SPEEDTEST_RUNNER_H
+#define SPEEDTEST_RUNNER_H
+
+#include "generic_test.h"
+
+namespace speedtest {
+
+void TimedRun(GenericTest *test, long millis);
+
+} // namespace speedtest
+
+#endif // SPEEDTEST_RUNNER_H
diff --git a/speedtest/speedtest.cc b/speedtest/speedtest.cc
index bf8fe3c..042e87c 100644
--- a/speedtest/speedtest.cc
+++ b/speedtest/speedtest.cc
@@ -16,241 +16,332 @@
#include "speedtest.h"
-#include <assert.h>
#include <chrono>
#include <cstring>
#include <limits>
#include <random>
#include <thread>
#include <iomanip>
+#include <fstream>
+#include <streambuf>
#include "errors.h"
+#include "runner.h"
#include "utils.h"
namespace speedtest {
+namespace {
-Speedtest::Speedtest(const Options &options)
- : options_(options),
- bytes_downloaded_(0),
- bytes_uploaded_(0) {
- assert(!options_.hosts.empty());
- env_ = std::make_shared<http::CurlEnv>();
+std::shared_ptr<std::string> MakeRandomData(size_t size) {
std::random_device rd;
std::default_random_engine random_engine(rd());
std::uniform_int_distribution<char> uniform_dist(1, 255);
- char *data = new char[options_.upload_size];
- for (int i = 0; i < options_.upload_size; ++i) {
- data[i] = uniform_dist(random_engine);
+ auto random_data = std::make_shared<std::string>();
+ random_data->resize(size);
+ for (size_t i = 0; i < size; ++i) {
+ (*random_data)[i] = uniform_dist(random_engine);
}
- send_data_ = data;
+ return std::move(random_data);
+}
+
+const char *kFileSerial = "/etc/serial";
+const char *kFileVersion = "/etc/version";
+
+std::string LoadFile(const std::string &file_name) {
+ std::ifstream in(file_name);
+ return std::string(std::istreambuf_iterator<char>(in),
+ std::istreambuf_iterator<char>());
+}
+
+} // namespace
+
+Speedtest::Speedtest(const Options &options)
+ : options_(options) {
+ http::CurlEnv::Options curl_options;
+ curl_options.disable_dns_cache = options_.disable_dns_cache;
+ curl_options.max_connections = options_.max_connections;
+ env_ = std::make_shared<http::CurlEnv>(curl_options);
}
Speedtest::~Speedtest() {
- delete[] send_data_;
}
void Speedtest::Run() {
- if (!RunPingTest()) {
+ InitUserAgent();
+ LoadServerList();
+ if (servers_.empty()) {
+ std::cerr << "No servers found in global server list\n";
+ std::exit(1);
+ }
+ FindNearestServer();
+ if (!server_url_) {
std::cout << "No servers responded. Exiting\n";
return;
}
+ std::string json = LoadConfig(*server_url_);
+ if (!ParseConfig(json, &config_)) {
+ std::cout << "Could not parse config\n";
+ return;
+ }
+ if (options_.verbose) {
+ std::cout << "Server config:\n";
+ PrintConfig(config_);
+ }
+ std::cout << "Location: " << config_.location_name << "\n";
+ std::cout << "URL: " << server_url_->url() << "\n";
RunDownloadTest();
RunUploadTest();
+ RunPingTest();
+}
+
+void Speedtest::InitUserAgent() {
+ if (options_.user_agent.empty()) {
+ std::string serial = LoadFile(kFileSerial);
+ std::string version = LoadFile(kFileVersion);
+ Trim(&serial);
+ Trim(&version);
+ user_agent_ = std::string("CPE/") +
+ (version.empty() ? "unknown version" : version) + "/" +
+ (serial.empty() ? "unknown serial" : serial);
+ } else {
+ user_agent_ = options_.user_agent;
+ return;
+ }
+ if (options_.verbose) {
+ std::cout << "Setting user agent to " << user_agent_ << "\n";
+ }
+}
+
+void Speedtest::LoadServerList() {
+ servers_.clear();
+ if (!options_.global) {
+ if (options_.verbose) {
+ std::cout << "Explicit server list:\n";
+ for (const auto &url : options_.hosts) {
+ std::cout << " " << url.url() << "\n";
+ }
+ }
+ servers_ = options_.hosts;
+ return;
+ }
+
+ std::string json = LoadConfig(options_.global_host);
+ if (json.empty()) {
+ std::cerr << "Failed to load config JSON\n";
+ std::exit(1);
+ }
+ if (options_.verbose) {
+ std::cout << "Loaded config JSON: " << json << "\n";
+ }
+ if (!ParseServers(json, &servers_)) {
+ std::cerr << "Failed to parse server list: " << json << "\n";
+ std::exit(1);
+ }
+ if (options_.verbose) {
+ std::cout << "Loaded servers:\n";
+ for (const auto &url : servers_) {
+ std::cout << " " << url.url() << "\n";
+ }
+ }
+}
+
+void Speedtest::FindNearestServer() {
+ server_url_.reset();
+ if (servers_.size() == 1) {
+ server_url_.reset(new http::Url(servers_[0]));
+ if (options_.verbose) {
+ std::cout << "Only 1 server so using " << server_url_->url() << "\n";
+ }
+ return;
+ }
+
+ PingTest::Options options;
+ options.verbose = options_.verbose;
+ options.timeout = PingTimeout();
+ std::vector<http::Url> hosts;
+ for (const auto &server : servers_) {
+ http::Url url(server);
+ url.set_path("/ping");
+ hosts.emplace_back(url);
+ }
+ options.num_pings = hosts.size();
+ if (options_.verbose) {
+ std::cout << "There are " << hosts.size() << " ping URLs:\n";
+ for (const auto &host : hosts) {
+ std::cout << " " << host.url() << "\n";
+ }
+ }
+ options.request_factory = [&](int id) -> GenericTest::RequestPtr{
+ return MakeRequest(hosts[id]);
+ };
+ PingTest find_nearest(options);
+ if (options_.verbose) {
+ std::cout << "Starting to find nearest server\n";
+ }
+ TimedRun(&find_nearest, 1500);
+ find_nearest.WaitForEnd();
+ if (find_nearest.IsSucceeded()) {
+ PingStats fastest = find_nearest.GetFastest();
+ server_url_.reset(new http::Url(fastest.url));
+ server_url_->clear_path();
+ if (options_.verbose) {
+ double ping_millis = fastest.min_micros / 1000.0d;
+ std::cout << "Found nearest server: " << fastest.url.url()
+ << " (" << round(ping_millis, 2) << " ms)\n";
+ }
+ }
+}
+
+std::string Speedtest::LoadConfig(const http::Url &url) {
+ http::Url config_url(url);
+ config_url.set_path("/config");
+ if (options_.verbose) {
+ std::cout << "Loading config from " << config_url.url() << "\n";
+ }
+ GenericTest::RequestPtr request = MakeRequest(config_url);
+ request->set_url(config_url);
+ std::string json;
+ request->Get([&](void *data, size_t size){
+ json.assign(static_cast<const char *>(data), size);
+ });
+ return json;
+}
+
+void Speedtest::RunPingTest() {
+ PingTest::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{
+ 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()) {
+ long micros = fastest.min_micros;
+ std::cout << "Ping time: " << round(micros / 1000.0d, 3) << " ms\n";
+ } else {
+ std::cout << "Failed to get ping response from "
+ << config_.location_name << " (" << fastest.url << ")\n";
+ }
}
void Speedtest::RunDownloadTest() {
- end_download_ = false;
- long start_time = SystemTimeMicros();
- bytes_downloaded_ = 0;
- std::thread threads[options_.number];
- for (int i = 0; i < options_.number; ++i) {
- threads[i] = std::thread([=]() {
- RunDownload(i);
- });
+ if (options_.verbose) {
+ std::cout << "Starting download test at " << config_.location_name
+ << " (" << server_url_->url() << ")\n";
}
- std::thread timer([&]{
- std::this_thread::sleep_for(
- std::chrono::milliseconds(options_.time_millis));
- end_download_ = true;
- });
- timer.join();
- for (auto &thread : threads) {
- thread.join();
+ DownloadTest::Options options;
+ options.verbose = options_.verbose;
+ options.num_transfers = NumDownloads();
+ options.download_size = DownloadSize();
+ options.request_factory = [this](int id) -> GenericTest::RequestPtr{
+ 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();
+ if (options_.verbose) {
+ std::cout << "Downloaded " << bytes << " bytes in "
+ << round(micros / 1000.0d, 2) << " ms\n";
}
- long end_time = speedtest::SystemTimeMicros();
-
- double running_time = (end_time - start_time) / 1000000.0;
- double megabits = bytes_downloaded_ * 8 / 1000000.0 / running_time;
- std::cout << "Downloaded " << bytes_downloaded_
- << " bytes in " << running_time * 1000 << " ms ("
- << megabits << " Mbps)\n";
-}
-
-void Speedtest::RunDownload(int id) {
- auto download = MakeRequest(id, "/download");
- http::Request::DownloadFn noop = [](void *, size_t) {};
- while (!end_download_) {
- long downloaded = 0;
- download->set_param("i", speedtest::to_string(id));
- download->set_param("size", speedtest::to_string(options_.download_size));
- download->set_param("time", speedtest::to_string(SystemTimeMicros()));
- download->set_progress_fn([&](curl_off_t,
- curl_off_t dlnow,
- curl_off_t,
- curl_off_t) -> bool {
- if (dlnow > downloaded) {
- bytes_downloaded_ += dlnow - downloaded;
- downloaded = dlnow;
- }
- return end_download_;
- });
- download->Get(noop);
- download->Reset();
- }
+ std::cout << "Download speed: "
+ << round(ToMegabits(bytes, micros), 3) << " Mbps\n";
}
void Speedtest::RunUploadTest() {
- end_upload_ = false;
- long start_time = SystemTimeMicros();
- bytes_uploaded_ = 0;
- std::thread threads[options_.number];
- for (int i = 0; i < options_.number; ++i) {
- threads[i] = std::thread([=]() {
- RunUpload(i);
- });
- }
- std::thread timer([&]{
- std::this_thread::sleep_for(
- std::chrono::milliseconds(options_.time_millis));
- end_upload_ = true;
- });
- timer.join();
- for (auto &thread : threads) {
- thread.join();
- }
- long end_time = speedtest::SystemTimeMicros();
-
- double running_time = (end_time - start_time) / 1000000.0;
- double megabits = bytes_uploaded_ * 8 / 1000000.0 / running_time;
- std::cout << "Uploaded " << bytes_uploaded_
- << " bytes in " << running_time * 1000 << " ms ("
- << megabits << " Mbps)\n";
-}
-
-void Speedtest::RunUpload(int id) {
- auto upload = MakeRequest(id, "/upload");
- while (!end_upload_) {
- long uploaded = 0;
- upload->set_progress_fn([&](curl_off_t,
- curl_off_t,
- curl_off_t,
- curl_off_t ulnow) -> bool {
- if (ulnow > uploaded) {
- bytes_uploaded_ += ulnow - uploaded;
- uploaded = ulnow;
- }
- return end_upload_;
- });
-
- // disable the Expect header as the server isn't expecting it (perhaps
- // it should?). If the server isn't then libcurl waits for 1 second
- // before sending the data anyway. So sending this header eliminated
- // the 1 second delay.
- upload->set_header("Expect", "");
- upload->Post(send_data_, options_.upload_size);
- upload->Reset();
- }
-}
-
-bool Speedtest::RunPingTest() {
- end_ping_ = false;
- size_t num_hosts = options_.hosts.size();
- std::thread threads[num_hosts];
- min_ping_micros_.clear();
- min_ping_micros_.resize(num_hosts);
- for (size_t i = 0; i < num_hosts; ++i) {
- threads[i] = std::thread([=]() {
- RunPing(i);
- });
- }
- std::thread timer([&]{
- std::this_thread::sleep_for(
- std::chrono::milliseconds(options_.time_millis));
- end_ping_ = true;
- });
- timer.join();
- for (auto &thread : threads) {
- thread.join();
- }
if (options_.verbose) {
- std::cout << "Pinged " << num_hosts << " "
- << (num_hosts == 1 ? "host" : "hosts:") << "\n";
+ std::cout << "Starting upload test at " << config_.location_name
+ << " (" << server_url_->url() << ")\n";
}
- size_t min_index = 0;
- for (size_t i = 0; i < num_hosts; ++i) {
- if (options_.verbose) {
- std::cout << " " << options_.hosts[i].url() << ": ";
- if (min_ping_micros_[i] == std::numeric_limits<long>::max()) {
- std::cout << "no packets received";
- } else {
- double ping_ms = min_ping_micros_[i] / 1000.0;
- if (ping_ms < 10) {
- std::cout << std::fixed << std::setprecision(1);
- } else {
- std::cout << std::fixed << std::setprecision(0);
- }
- std::cout << ping_ms << " ms";
- }
- std::cout << "\n";
- }
- if (min_ping_micros_[i] < min_ping_micros_[min_index]) {
- min_index = i;
- }
+ UploadTest::Options options;
+ options.verbose = options_.verbose;
+ options.num_transfers = NumUploads();
+ options.payload = MakeRandomData(UploadSize());
+ options.request_factory = [this](int id) -> GenericTest::RequestPtr{
+ 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();
+ if (options_.verbose) {
+ std::cout << "Uploaded " << bytes << " bytes in "
+ << round(micros / 1000.0d, 2) << " ms\n";
}
- if (min_ping_micros_[min_index] == std::numeric_limits<long>::max()) {
- // no servers respondeded
- return false;
- }
- url_ = options_.hosts[min_index];
- std::cout << "Host for Speedtest: " << url_.url() << " (";
- double ping_ms = min_ping_micros_[min_index] / 1000.0;
- if (ping_ms < 10) {
- std::cout << std::fixed << std::setprecision(1);
- } else {
- std::cout << std::fixed << std::setprecision(0);
- }
- std::cout << ping_ms << " ms)\n";
- return true;
+ std::cout << "Upload speed: "
+ << round(ToMegabits(bytes, micros), 3) << " Mbps\n";
}
-void Speedtest::RunPing(size_t index) {
- http::Request::DownloadFn noop = [](void *, size_t) {};
- min_ping_micros_[index] = std::numeric_limits<long>::max();
- http::Url url(options_.hosts[index]);
- url.set_path("/ping");
- auto ping = env_->NewRequest();
- ping->set_url(url);
- while (!end_ping_) {
- long req_start = SystemTimeMicros();
- if (ping->Get(noop) == CURLE_OK) {
- long req_end = SystemTimeMicros();
- long ping_time = req_end - req_start;
- min_ping_micros_[index] = std::min(min_ping_micros_[index], ping_time);
- }
- ping->Reset();
- std::this_thread::sleep_for(std::chrono::milliseconds(100));
- }
+int Speedtest::NumDownloads() const {
+ return options_.num_downloads
+ ? options_.num_downloads
+ : config_.num_downloads;
}
-std::unique_ptr<http::Request> Speedtest::MakeRequest(int id,
- const std::string &path) {
- auto request = env_->NewRequest();
- http::Url url(url_);
- int port = (id % 20) + url.port() + 1;
- url.set_port(port);
- url.set_path(path);
- request->set_url(url);
+int Speedtest::DownloadSize() const {
+ return options_.download_size
+ ? options_.download_size
+ : config_.download_size;
+}
+
+int Speedtest::NumUploads() const {
+ return options_.num_uploads
+ ? options_.num_uploads
+ : config_.num_uploads;
+}
+
+int Speedtest::UploadSize() const {
+ return options_.upload_size
+ ? options_.upload_size
+ : config_.upload_size;
+}
+
+int Speedtest::PingRunTime() const {
+ return options_.ping_runtime
+ ? options_.ping_runtime
+ : config_.ping_runtime;
+}
+
+int Speedtest::PingTimeout() const {
+ return options_.ping_timeout
+ ? options_.ping_timeout
+ : config_.ping_timeout;
+}
+
+GenericTest::RequestPtr Speedtest::MakeRequest(const http::Url &url) {
+ GenericTest::RequestPtr request = env_->NewRequest(url);
+ if (!user_agent_.empty()) {
+ request->set_user_agent(user_agent_);
+ }
return std::move(request);
}
+GenericTest::RequestPtr Speedtest::MakeBaseRequest(
+ int id, const std::string &path) {
+ http::Url url(*server_url_);
+ url.set_path(path);
+ return MakeRequest(url);
+}
+
+GenericTest::RequestPtr Speedtest::MakeTransferRequest(
+ int id, const std::string &path) {
+ http::Url url(*server_url_);
+ int port_start = config_.transfer_port_start;
+ int port_end = config_.transfer_port_end;
+ int num_ports = port_end - port_start + 1;
+ if (num_ports > 0) {
+ url.set_port(port_start + (id % num_ports));
+ }
+ url.set_path(path);
+ return MakeRequest(url);
+}
+
} // namespace speedtest
diff --git a/speedtest/speedtest.h b/speedtest/speedtest.h
index 01e6f75..ea509f7 100644
--- a/speedtest/speedtest.h
+++ b/speedtest/speedtest.h
@@ -21,8 +21,13 @@
#include <memory>
#include <string>
+#include "config.h"
#include "curl_env.h"
+#include "download_test.h"
+#include "generic_test.h"
#include "options.h"
+#include "ping_test.h"
+#include "upload_test.h"
#include "url.h"
#include "request.h"
@@ -34,27 +39,37 @@
virtual ~Speedtest();
void Run();
- void RunDownloadTest();
- void RunUploadTest();
- bool RunPingTest();
private:
- void RunDownload(int id);
- void RunUpload(int id);
- void RunPing(size_t host_index);
+ void InitUserAgent();
+ void LoadServerList();
+ void FindNearestServer();
+ std::string LoadConfig(const http::Url &url);
+ void RunPingTest();
+ void RunDownloadTest();
+ void RunUploadTest();
- std::unique_ptr<http::Request> MakeRequest(int id, const std::string &path);
+ int NumDownloads() const;
+ int DownloadSize() const;
+ int NumUploads() const;
+ int UploadSize() const;
+ int PingTimeout() const;
+ int PingRunTime() const;
- std::shared_ptr<http::CurlEnv> env_;
+ 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);
+
+ std::shared_ptr <http::CurlEnv> env_;
Options options_;
- http::Url url_;
- std::atomic_bool end_ping_;
- std::atomic_bool end_download_;
- std::atomic_bool end_upload_;
- std::atomic_long bytes_downloaded_;
- std::atomic_long bytes_uploaded_;
- std::vector<long> min_ping_micros_;
- const char *send_data_;
+ Config config_;
+ std::string user_agent_;
+ 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/transfer_test.cc b/speedtest/transfer_test.cc
new file mode 100644
index 0000000..213d5d9
--- /dev/null
+++ b/speedtest/transfer_test.cc
@@ -0,0 +1,49 @@
+/*
+ * 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 "transfer_test.h"
+
+#include <cassert>
+
+namespace speedtest {
+
+TransferTest::TransferTest(const Options &options)
+ : GenericTest(options),
+ bytes_transferred_(0),
+ requests_started_(0),
+ requests_ended_(0) {
+ assert(options.num_transfers > 0);
+}
+
+void TransferTest::ResetCounters() {
+ bytes_transferred_ = 0;
+ requests_started_ = 0;
+ requests_ended_ = 0;
+}
+
+void TransferTest::StartRequest() {
+ requests_started_++;
+}
+
+void TransferTest::EndRequest() {
+ requests_ended_++;
+}
+
+void TransferTest::TransferBytes(long bytes) {
+ bytes_transferred_ += bytes;
+}
+
+} // namespace speedtest
diff --git a/speedtest/transfer_test.h b/speedtest/transfer_test.h
new file mode 100644
index 0000000..d7307a6
--- /dev/null
+++ b/speedtest/transfer_test.h
@@ -0,0 +1,55 @@
+/*
+ * 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.
+ */
+
+#ifndef SPEEDTEST_TRANSFER_TEST_H
+#define SPEEDTEST_TRANSFER_TEST_H
+
+#include <atomic>
+#include "generic_test.h"
+
+namespace speedtest {
+
+class TransferTest : public GenericTest {
+ public:
+ struct Options : GenericTest::Options {
+ int num_transfers;
+ };
+
+ explicit TransferTest(const Options &options);
+
+ long bytes_transferred() const { return bytes_transferred_; }
+ long requests_started() const { return requests_started_; }
+ long requests_ended() const { return requests_ended_; }
+
+ protected:
+ void ResetCounters();
+ void StartRequest();
+ void EndRequest();
+ void TransferBytes(long bytes);
+
+ private:
+ std::atomic_long bytes_transferred_;
+ std::atomic_int requests_started_;
+ std::atomic_int requests_ended_;
+
+ // disallowed
+ TransferTest(const TransferTest &) = delete;
+ void operator=(const TransferTest &) = delete;
+};
+
+} // namespace speedtest
+
+#endif // SPEEDTEST_TRANSFER_TEST_H
diff --git a/speedtest/upload_test.cc b/speedtest/upload_test.cc
new file mode 100644
index 0000000..8c014ba
--- /dev/null
+++ b/speedtest/upload_test.cc
@@ -0,0 +1,84 @@
+/*
+ * 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 "upload_test.h"
+
+#include <algorithm>
+#include <cassert>
+#include <iostream>
+#include "generic_test.h"
+#include "utils.h"
+
+namespace speedtest {
+
+UploadTest::UploadTest(const Options &options)
+ : TransferTest(options),
+ options_(options) {
+ assert(options_.payload);
+ assert(options_.payload->size() > 0);
+}
+
+void UploadTest::RunInternal() {
+ ResetCounters();
+ threads_.clear();
+ if (options_.verbose) {
+ std::cout << "Uploading " << options_.num_transfers
+ << " threads with " << options_.payload->size() << " bytes\n";
+ }
+ for (int i = 0; i < options_.num_transfers; ++i) {
+ threads_.emplace_back([=]{
+ RunUpload(i);
+ });
+ }
+}
+
+void UploadTest::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) {
+ long uploaded = 0;
+ upload->set_param("i", to_string(id));
+ upload->set_param("time", to_string(SystemTimeMicros()));
+ upload->set_progress_fn([&](curl_off_t,
+ curl_off_t,
+ curl_off_t,
+ curl_off_t ulnow) -> bool {
+ if (ulnow > uploaded) {
+ TransferBytes(ulnow - uploaded);
+ uploaded = ulnow;
+ }
+ return GetStatus() != TestStatus::RUNNING;
+ });
+
+ // disable the Expect header as the server isn't expecting it (perhaps
+ // it should?). If the server isn't then libcurl waits for 1 second
+ // before sending the data anyway. So sending this header eliminated
+ // the 1 second delay.
+ upload->set_header("Expect", "");
+
+ StartRequest();
+ upload->Post(options_.payload->c_str(), options_.payload->size());
+ EndRequest();
+ upload->Reset();
+ }
+}
+
+} // namespace speedtest
diff --git a/speedtest/upload_test.h b/speedtest/upload_test.h
new file mode 100644
index 0000000..509eed6
--- /dev/null
+++ b/speedtest/upload_test.h
@@ -0,0 +1,53 @@
+/*
+ * 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.
+ */
+
+#ifndef SPEEDTEST_UPLOAD_TEST_H
+#define SPEEDTEST_UPLOAD_TEST_H
+
+#include <memory>
+#include <string>
+#include <thread>
+#include <vector>
+#include "transfer_test.h"
+
+namespace speedtest {
+
+class UploadTest : public TransferTest {
+ public:
+ struct Options : TransferTest::Options {
+ std::shared_ptr<std::string> payload;
+ };
+
+ explicit UploadTest(const Options &options);
+
+ protected:
+ void RunInternal() override;
+ void StopInternal() override;
+
+ private:
+ void RunUpload(int id);
+
+ Options options_;
+ std::vector<std::thread> threads_;
+
+ // disallowed
+ UploadTest(const UploadTest &) = delete;
+ void operator=(const UploadTest &) = delete;
+};
+
+} // namespace speedtest
+
+#endif // SPEEDTEST_UPLOAD_TEST_H
diff --git a/speedtest/url.cc b/speedtest/url.cc
index c0934a0..6292790 100644
--- a/speedtest/url.cc
+++ b/speedtest/url.cc
@@ -132,6 +132,10 @@
return url1.url() == url2.url();
}
+std::ostream &operator<<(std::ostream &os, const Url &url) {
+ return os << (url.ok() ? url.url() : "{invalid URL}");
+}
+
Url::Url(): parsed_(false), absolute_(false), port_(0) {
}
@@ -150,7 +154,8 @@
fragment_ = other.fragment_;
}
-Url::Url(const char *url): parsed_(false), absolute_(false), port_(0) {
+Url::Url(const std::string &url)
+ : parsed_(false), absolute_(false), port_(0) {
Parse(url);
}
@@ -411,8 +416,9 @@
return false;
}
std::string port(start, iter);
- int portnum = speedtest::stoi(port);
- if (portnum < 1 || portnum > 65535) {
+ int portnum;
+ if (!speedtest::ParseInt(port, &portnum) ||
+ portnum < 1 || portnum > 65535) {
return false;
}
current_ = iter;
diff --git a/speedtest/url.h b/speedtest/url.h
index 6aeb68a..e5a5654 100644
--- a/speedtest/url.h
+++ b/speedtest/url.h
@@ -43,7 +43,7 @@
public:
Url();
Url(const Url &other);
- explicit Url(const char *url);
+ explicit Url(const std::string &url);
Url &operator=(const Url &other);
bool Parse(const std::string &url);
@@ -75,6 +75,7 @@
std::string url() const;
friend bool operator==(const Url &url1, const Url &url2);
+ friend std::ostream &operator<<(std::ostream &os, const Url &url);
private:
using Iter = std::string::const_iterator;
diff --git a/speedtest/url_test.cc b/speedtest/url_test.cc
index 06c2d72..b43d4fb 100644
--- a/speedtest/url_test.cc
+++ b/speedtest/url_test.cc
@@ -14,10 +14,10 @@
* limitations under the License.
*/
-#include <gtest/gtest.h>
-
#include "url.h"
+#include <gtest/gtest.h>
+
namespace http {
namespace {
diff --git a/speedtest/utils.cc b/speedtest/utils.cc
index 012e1eb..b41bb1f 100644
--- a/speedtest/utils.cc
+++ b/speedtest/utils.cc
@@ -16,9 +16,13 @@
#include "utils.h"
+#include <algorithm>
+#include <cctype>
#include <cstdlib>
+#include <functional>
#include <iostream>
#include <stdexcept>
+#include <stdio.h>
#include <string>
#include <sstream>
@@ -33,24 +37,53 @@
return ts.tv_sec * 1000000 + ts.tv_nsec / 1000;
}
-std::string to_string(long n)
-{
+std::string to_string(long n) {
std::ostringstream s;
s << n;
return s.str();
}
-int stoi(const std::string& str)
-{
- int rc;
- std::istringstream n(str);
+std::string round(double d, int digits) {
+ char buf[20];
+ sprintf(buf, "%.*f", digits, d);
+ return buf;
+}
- if (!(n >> rc)) {
- std::cerr << "Not a number: " << str;
- std::exit(1);
+double ToMegabits(long bytes, long micros) {
+ return (8.0d * bytes) / micros;
+}
+
+bool ParseInt(const std::string &str, int *result) {
+ if (!result) {
+ return false;
}
+ std::istringstream n(str);
+ return n >> *result;
+}
- return rc;
+// Trim from start in place
+// Caller retains ownership
+void LeftTrim(std::string *s) {
+ s->erase(s->begin(),
+ std::find_if(s->begin(),
+ s->end(),
+ std::not1(std::ptr_fun<int, int>(std::isspace))));
+}
+
+// Trim from end in place
+// Caller retains ownership
+void RightTrim(std::string *s) {
+ s->erase(std::find_if(s->rbegin(),
+ s->rend(),
+ std::not1(std::ptr_fun<int, int>(std::isspace))).base(),
+ s->end());
+}
+
+// Trim from both ends in place
+// Caller retains ownership
+void Trim(std::string *s) {
+ LeftTrim(s);
+ RightTrim(s);
}
} // namespace speedtest
diff --git a/speedtest/utils.h b/speedtest/utils.h
index 7ad4289..f3e5f1f 100644
--- a/speedtest/utils.h
+++ b/speedtest/utils.h
@@ -28,8 +28,28 @@
// Return a string representation of n
std::string to_string(long n);
-// return an integer value from the string str.
-int stoi(const std::string& str);
+// Round a double to a minimum number of significant digits
+std::string round(double d, int digits);
+
+// Convert bytes and time in micros to speed in megabits
+double ToMegabits(long bytes, long micros);
+
+// Parse an int.
+// If successful, write result to result and return true.
+// If result is null or the int can't be parsed, return false.
+bool ParseInt(const std::string &str, int *result);
+
+// Trim from start in place
+// Caller retains ownership
+void LeftTrim(std::string *s);
+
+// Trim from end in place
+// Caller retains ownership
+void RightTrim(std::string *s);
+
+// Trim from both ends in place
+// Caller retains ownership
+void Trim(std::string *s);
} // namespace speedtst