From 230f6757dd79145e5184a458b1081d78e2ebcc90 Mon Sep 17 00:00:00 2001 From: dnzbk Date: Tue, 28 Jan 2025 11:00:36 +0300 Subject: [PATCH] Add: network speed tests --- daemon/connect/HttpClient.cpp | 2 +- daemon/connect/HttpClient.h | 2 +- daemon/connect/NetworkSpeedTest.cpp | 177 ++++++++++++++++++++++++++++ daemon/connect/NetworkSpeedTest.h | 96 +++++++++++++++ daemon/main/nzbget.h | 1 + daemon/remote/XmlRpc.cpp | 61 ++++++++++ daemon/sources.cmake | 2 + daemon/system/Network.cpp | 6 +- daemon/system/Network.h | 2 + daemon/util/DataAnalytics.cpp | 82 +++++++++++++ daemon/util/DataAnalytics.h | 50 ++++++++ docs/api/TESTDISKSPEED.md | 2 +- docs/api/TESTNETWORKSPEED.md | 15 +++ tests/util/CMakeLists.txt | 2 + tests/util/DataAnalyticsTest.cpp | 47 ++++++++ webui/index.html | 8 +- webui/style.css | 3 +- webui/system-info.js | 86 +++++++++++--- webui/util.js | 57 +++++++++ 19 files changed, 674 insertions(+), 27 deletions(-) create mode 100644 daemon/connect/NetworkSpeedTest.cpp create mode 100644 daemon/connect/NetworkSpeedTest.h create mode 100644 daemon/util/DataAnalytics.cpp create mode 100644 daemon/util/DataAnalytics.h create mode 100644 docs/api/TESTNETWORKSPEED.md create mode 100644 tests/util/DataAnalyticsTest.cpp diff --git a/daemon/connect/HttpClient.cpp b/daemon/connect/HttpClient.cpp index 89bd8fde..94dce80b 100644 --- a/daemon/connect/HttpClient.cpp +++ b/daemon/connect/HttpClient.cpp @@ -23,7 +23,7 @@ #include "HttpClient.h" #include "Util.h" -namespace HttpClient +namespace Network { namespace asio = boost::asio; using tcp = boost::asio::ip::tcp; diff --git a/daemon/connect/HttpClient.h b/daemon/connect/HttpClient.h index b37f4d7f..b10e0a4e 100644 --- a/daemon/connect/HttpClient.h +++ b/daemon/connect/HttpClient.h @@ -29,7 +29,7 @@ #include #endif -namespace HttpClient +namespace Network { #if !defined(DISABLE_TLS) && defined(HAVE_OPENSSL) using Socket = boost::asio::ssl::stream; diff --git a/daemon/connect/NetworkSpeedTest.cpp b/daemon/connect/NetworkSpeedTest.cpp new file mode 100644 index 00000000..ce185d35 --- /dev/null +++ b/daemon/connect/NetworkSpeedTest.cpp @@ -0,0 +1,177 @@ +/* + * This file is part of nzbget. See . + * + * Copyright (C) 2025 Denis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "nzbget.h" + +#include +#include "Util.h" +#include "NetworkSpeedTest.h" +#include "Connection.h" +#include "Log.h" + +using namespace std::chrono; + +namespace Network +{ + double SpeedTest::RunTest() noexcept(false) + { + info("Download speed test starting..."); + + for (TestSpec test : m_testSpecs) + { + if (!RunTest(test)) break; + if (!IsRequiredMoreTests()) break; + } + + if (m_dataset.empty()) + { + error(m_errMsg); + throw std::runtime_error(m_errMsg); + } + + double finalSpeed = GetFinalSpeed(); + + info("Download speed %.2f Mbps", finalSpeed); + + return finalSpeed; + } + + bool SpeedTest::RunTest(TestSpec testSpec) + { + DataAnalytics data; + auto [name, size, iterations, timeout] = testSpec; + int failures = 0; + + while (--iterations) + { + auto [downloaded, elapsedTime] = ExecuteTest(MakeRequest(size), timeout); + if (elapsedTime == seconds{ 0 } || downloaded < size) + { + ++failures; + if (HasExceededMaxFailures(failures, iterations)) + { + break; + } + + continue; + } + + double speed = CalculateSpeedInMbps(downloaded, elapsedTime.count()); + data.AddMeasurement(speed); + + info("Downloaded %s at %.2f Mbps", name, speed); + } + + data.Compute(); + m_dataset.push_back(std::move(data)); + + return true; + } + + std::pair> SpeedTest::ExecuteTest(std::string request, seconds timeout) + { + Connection connection(m_host.data(), m_port, m_useTls); + connection.SetTimeout(1); + + if (!connection.Connect()) + { + return { 0, seconds{0} }; + } + + if (!connection.Send(request.c_str(), request.size())) + { + return { 0, seconds{0} }; + } + + uint32_t totalRecieved = 0; + char buffer[m_bufferSize]; + auto start = steady_clock::now(); + while (int recieved = connection.TryRecv(buffer, m_bufferSize)) + { + if (recieved <= 0 || (steady_clock::now() - start) >= timeout) + { + break; + } + + totalRecieved += recieved; + } + auto finish = steady_clock::now(); + + return { totalRecieved, duration(finish - start) }; + } + + double SpeedTest::CalculateSpeedInMbps(uint32_t bytes, double timeSec) const + { + double bits = bytes * 8.0; + double speed = bits / timeSec; + double mbps = speed / 1000000.0; + return mbps; + } + + bool SpeedTest::IsRequiredMoreTests() const + { + if (m_dataset.size() < 2) + return true; + + const DataAnalytics& curr = m_dataset.back(); + const DataAnalytics& prev = m_dataset[m_dataset.size() - 2]; + + if (curr.GetMedian() > prev.GetPercentile25()) + { + return true; + } + + return false; + } + + bool SpeedTest::HasExceededMaxFailures(int failures, int iterations) const + { + return failures > std::max(iterations / 2, 2); + } + + double SpeedTest::GetFinalSpeed() const + { + const auto& dataAnalytics = m_dataset.front(); + + double maxSpeed = dataAnalytics.GetMedian(); + for (const auto& analytics : m_dataset) + { + double speed = analytics.GetPercentile90(); + if (maxSpeed < speed) + { + maxSpeed = speed; + } + } + + return maxSpeed; + } + + std::string SpeedTest::MakeRequest(uint32_t filesize) const + { + std::ostringstream request; + + request << "GET /" << m_path; + request << m_query << std::to_string(filesize); + request << " HTTP/1.1\r\n"; + request << "Host: " << m_host << "\r\n"; + request << "Connection: close\r\n\r\n"; + + return request.str(); + } +} diff --git a/daemon/connect/NetworkSpeedTest.h b/daemon/connect/NetworkSpeedTest.h new file mode 100644 index 00000000..d9fcdbaa --- /dev/null +++ b/daemon/connect/NetworkSpeedTest.h @@ -0,0 +1,96 @@ +/* + * This file is part of nzbget. See . + * + * Copyright (C) 2025 Denis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef NETWORK_SPEED_TEST_H +#define NETWORK_SPEED_TEST_H + +#include +#include +#include + +#include "DataAnalytics.h" + +namespace Network +{ + class SpeedTest final + { + public: + SpeedTest() = default; + SpeedTest(const SpeedTest&) = delete; + SpeedTest operator=(const SpeedTest&) = delete; + + double RunTest() noexcept(false); + + private: + struct TestSpec + { + const char* name; + uint32_t bytes; + int iterations; + std::chrono::seconds timeout; + }; + + enum BytesToDownload : uint32_t + { + KiB = 1024, + KiB100 = 1024 * 100, + MiB1 = 1024 * 1024, + MiB10 = 1024 * 1024 * 10, + MiB25 = 1024 * 1024 * 25, + MiB100 = 1024 * 1024 * 100, + MiB250 = 1024 * 1024 * 250, + GiB1 = 1024 * 1024 * 1024, + }; + + static constexpr std::array m_testSpecs + { { + { "100 KiB", KiB100, 10, std::chrono::seconds{1} }, + { "1 MiB", MiB1, 8, std::chrono::seconds{2} }, + { "10 MiB", MiB10, 6, std::chrono::seconds{2} }, + { "25 MiB", MiB25, 4, std::chrono::seconds{2} }, + { "100 MiB", MiB100, 3, std::chrono::seconds{3} }, + { "250 MiB", MiB250, 2, std::chrono::seconds{4} }, + { "1 GiB", GiB1, 2, std::chrono::seconds{5} }, + } }; + static constexpr size_t m_bufferSize = 8 * KiB; + static constexpr std::string_view m_host = "speed.cloudflare.com"; + static constexpr std::string_view m_path = "__down"; + static constexpr std::string_view m_query = "?bytes="; + const char* m_errMsg = "No speed data was collected during the network speed test"; + + bool RunTest(TestSpec testSpec); + std::pair> ExecuteTest(std::string request, std::chrono::seconds timeout); + std::string MakeRequest(uint32_t filesize) const; + double CalculateSpeedInMbps(uint32_t bytes, double timeSec) const; + bool IsRequiredMoreTests() const; + bool HasExceededMaxFailures(int failures, int iterations) const; + double GetFinalSpeed() const; + +#ifdef DISABLE_TLS + bool m_useTls = false; + int m_port = 80; +#else + bool m_useTls = true; + int m_port = 443; +#endif + std::vector m_dataset; + }; +} + +#endif diff --git a/daemon/main/nzbget.h b/daemon/main/nzbget.h index d24ebfac..b6e8d7a1 100644 --- a/daemon/main/nzbget.h +++ b/daemon/main/nzbget.h @@ -158,6 +158,7 @@ #include #include #include +#include #include #include #include diff --git a/daemon/remote/XmlRpc.cpp b/daemon/remote/XmlRpc.cpp index a28a81b1..7c06abb1 100644 --- a/daemon/remote/XmlRpc.cpp +++ b/daemon/remote/XmlRpc.cpp @@ -40,6 +40,7 @@ #include "ExtensionManager.h" #include "SystemInfo.h" #include "Benchmark.h" +#include "NetworkSpeedTest.h" #include "Xml.h" extern void ExitProc(); @@ -399,6 +400,39 @@ class TestDiskSpeedXmlCommand final : public SafeXmlCommand } }; +class TestNetworkSpeedXmlCommand final : public SafeXmlCommand +{ +public: + void Execute() override; + + std::string ToJsonStr(double speedMbps) + { + Json::JsonObject json; + + json["SpeedMbps"] = speedMbps; + + return Json::Serialize(json); + } + + std::string ToXmlStr(double speedMbps) + { + xmlNodePtr rootNode = xmlNewNode(nullptr, BAD_CAST "value"); + xmlNodePtr structNode = xmlNewNode(nullptr, BAD_CAST "struct"); + + std::string speedMbpsStr = std::to_string(speedMbps); + + Xml::AddNewNode(structNode, "SpeedMbps", "double", speedMbpsStr.c_str()); + + xmlAddChild(rootNode, structNode); + + std::string result = Xml::Serialize(rootNode); + + xmlFreeNode(rootNode); + + return result; + } +}; + class StartScriptXmlCommand : public XmlCommand { public: @@ -859,6 +893,10 @@ std::unique_ptr XmlRpcProcessor::CreateCommand(const char* methodNam { command = std::make_unique(); } + else if (!strcasecmp(methodName, "testnetworkspeed")) + { + command = std::make_unique(); + } else if (!strcasecmp(methodName, "startscript")) { command = std::make_unique(); @@ -3815,6 +3853,29 @@ void TestDiskSpeedXmlCommand::Execute() } } +void TestNetworkSpeedXmlCommand::Execute() +{ + try + { + g_WorkState->SetPauseDownload(true); + + Network::SpeedTest sp; + double speedMbps = sp.RunTest(); + std::string respStr = IsJson() ? + ToJsonStr(speedMbps) : + ToXmlStr(speedMbps); + + AppendResponse(respStr.c_str()); + + g_WorkState->SetPauseDownload(false); + } + catch (const std::exception& e) + { + BuildErrorResponse(2, e.what()); + g_WorkState->SetPauseDownload(false); + } +} + // bool startscript(string script, string command, string context, struct[] options); void StartScriptXmlCommand::Execute() { diff --git a/daemon/sources.cmake b/daemon/sources.cmake index 4596ad8c..32e98c20 100644 --- a/daemon/sources.cmake +++ b/daemon/sources.cmake @@ -3,6 +3,7 @@ set(SRC ${CMAKE_SOURCE_DIR}/daemon/connect/TlsSocket.cpp ${CMAKE_SOURCE_DIR}/daemon/connect/WebDownloader.cpp ${CMAKE_SOURCE_DIR}/daemon/connect/HttpClient.cpp + ${CMAKE_SOURCE_DIR}/daemon/connect/NetworkSpeedTest.cpp ${CMAKE_SOURCE_DIR}/daemon/extension/CommandScript.cpp ${CMAKE_SOURCE_DIR}/daemon/extension/FeedScript.cpp @@ -96,6 +97,7 @@ set(SRC ${CMAKE_SOURCE_DIR}/daemon/util/Json.cpp ${CMAKE_SOURCE_DIR}/daemon/util/Xml.cpp ${CMAKE_SOURCE_DIR}/daemon/util/Benchmark.cpp + ${CMAKE_SOURCE_DIR}/daemon/util/DataAnalytics.cpp ${CMAKE_SOURCE_DIR}/daemon/system/SystemInfo.cpp ${CMAKE_SOURCE_DIR}/daemon/system/OS.cpp diff --git a/daemon/system/Network.cpp b/daemon/system/Network.cpp index c7d58329..3d368ba3 100644 --- a/daemon/system/Network.cpp +++ b/daemon/system/Network.cpp @@ -27,15 +27,13 @@ namespace System { - static const char* IP_SERVICE = "ip.nzbget.com"; - Network GetNetwork() { - Network network{}; + Network network; try { - HttpClient::HttpClient httpClient; + ::Network::HttpClient httpClient; auto result = httpClient.GET(IP_SERVICE).get(); if (result.statusCode == 200) { diff --git a/daemon/system/Network.h b/daemon/system/Network.h index 8d1df3b2..d528c58a 100644 --- a/daemon/system/Network.h +++ b/daemon/system/Network.h @@ -24,6 +24,8 @@ namespace System { + static const char* IP_SERVICE = "ip.nzbget.com"; + struct Network { std::string publicIP; diff --git a/daemon/util/DataAnalytics.cpp b/daemon/util/DataAnalytics.cpp new file mode 100644 index 00000000..aff36bf9 --- /dev/null +++ b/daemon/util/DataAnalytics.cpp @@ -0,0 +1,82 @@ +/* + * This file is part of nzbget. See . + * + * Copyright (C) 2025 Denis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + + +#include "nzbget.h" + +#include "DataAnalytics.h" + +double DataAnalytics::GetMin() const { return m_min; } +double DataAnalytics::GetMax() const { return m_max; } +double DataAnalytics::GetAvg() const { return m_avg; } +double DataAnalytics::GetMedian() const { return m_median; } +double DataAnalytics::GetPercentile25() const { return m_percentile25; } +double DataAnalytics::GetPercentile75() const { return m_percentile75; } +double DataAnalytics::GetPercentile90() const { return m_percentile90; } + +void DataAnalytics::Compute() +{ + if (m_measurements.empty()) + { + return; + } + + std::sort(begin(m_measurements), end(m_measurements)); + + m_min = m_measurements.front(); + m_max = m_measurements.back(); + m_avg = std::accumulate(cbegin(m_measurements), cend(m_measurements), 0.0) / m_measurements.size(); + m_median = CalculatePercentile(0.5); + m_percentile25 = CalculatePercentile(0.25); + m_percentile75 = CalculatePercentile(0.75); + m_percentile90 = CalculatePercentile(0.90); +} + +void DataAnalytics::AddMeasurement(double measuremen) +{ + m_measurements.push_back(measuremen); +} + +double DataAnalytics::CalculatePercentile(double percentile) +{ + if (m_measurements.empty()) + { + return 0.0; + } + + if (percentile == 0.0) + { + return m_measurements.front(); + } + + if (percentile == 1.0) + { + return m_measurements.back(); + } + + double rank = percentile * (m_measurements.size() - 1); + size_t idx = static_cast(rank); + double fraction = rank - idx; + + if (idx + 1 < m_measurements.size()) { + return m_measurements[idx] * (1.0 - fraction) + m_measurements[idx + 1] * fraction; + } + + return m_measurements[idx]; +} diff --git a/daemon/util/DataAnalytics.h b/daemon/util/DataAnalytics.h new file mode 100644 index 00000000..00967162 --- /dev/null +++ b/daemon/util/DataAnalytics.h @@ -0,0 +1,50 @@ +/* + * This file is part of nzbget. See . + * + * Copyright (C) 2025 Denis + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include + +class DataAnalytics final +{ +public: + DataAnalytics() = default; + ~DataAnalytics() = default; + + double GetMin() const; + double GetMax() const; + double GetAvg() const; + double GetMedian() const; + double GetPercentile25() const; + double GetPercentile75() const; + double GetPercentile90() const; + + void Compute(); + void AddMeasurement(double measuremen); + +private: + double CalculatePercentile(double percentile); + + double m_min = 0.0; + double m_max = 0.0; + double m_avg = 0.0; + double m_median = 0.0; + double m_percentile25 = 0.0; + double m_percentile75 = 0.0; + double m_percentile90 = 0.0; + std::vector m_measurements; +}; diff --git a/docs/api/TESTDISKSPEED.md b/docs/api/TESTDISKSPEED.md index de894f1c..cc7e8019 100644 --- a/docs/api/TESTDISKSPEED.md +++ b/docs/api/TESTDISKSPEED.md @@ -5,7 +5,7 @@ ### Signature ``` c++ -bool testdiskspeed( +struct testdiskspeed( string dirPath, int writeBufferSize, int maxFileSize, diff --git a/docs/api/TESTNETWORKSPEED.md b/docs/api/TESTNETWORKSPEED.md new file mode 100644 index 00000000..7552d4c3 --- /dev/null +++ b/docs/api/TESTNETWORKSPEED.md @@ -0,0 +1,15 @@ +## API-method `testnetworkspeed` + +## Since +`v24.6` + +### Signature +``` c++ +struct testnetworkspeed(); +``` + +### Description +Starts the network speed test. + +### Return value +- **SpeedMbps** `(double)` - Written data size (in Mbps). diff --git a/tests/util/CMakeLists.txt b/tests/util/CMakeLists.txt index e88cd1e7..ab7958fd 100644 --- a/tests/util/CMakeLists.txt +++ b/tests/util/CMakeLists.txt @@ -5,6 +5,7 @@ set(UtilTestSrc NStringTest.cpp JsonTest.cpp BenchmarkTest.cpp + DataAnalyticsTest.cpp ${CMAKE_SOURCE_DIR}/daemon/util/FileSystem.cpp ${CMAKE_SOURCE_DIR}/daemon/util/NString.cpp ${CMAKE_SOURCE_DIR}/daemon/util/Util.cpp @@ -12,6 +13,7 @@ set(UtilTestSrc ${CMAKE_SOURCE_DIR}/daemon/util/Xml.cpp ${CMAKE_SOURCE_DIR}/daemon/util/Log.cpp ${CMAKE_SOURCE_DIR}/daemon/util/Benchmark.cpp + ${CMAKE_SOURCE_DIR}/daemon/util/DataAnalytics.cpp ) if(WIN32) diff --git a/tests/util/DataAnalyticsTest.cpp b/tests/util/DataAnalyticsTest.cpp new file mode 100644 index 00000000..31f970e7 --- /dev/null +++ b/tests/util/DataAnalyticsTest.cpp @@ -0,0 +1,47 @@ +#include "nzbget.h" + +#include +#include "DataAnalytics.h" + +BOOST_AUTO_TEST_CASE(test_data_analytics) +{ + DataAnalytics da; + + da.Compute(); + + BOOST_CHECK_EQUAL(da.GetMin(), 0.0); + BOOST_CHECK_EQUAL(da.GetMax(), 0.0); + BOOST_CHECK_EQUAL(da.GetAvg(), 0.0); + BOOST_CHECK_EQUAL(da.GetMedian(), 0.0); + BOOST_CHECK_EQUAL(da.GetPercentile25(), 0.0); + BOOST_CHECK_EQUAL(da.GetPercentile75(), 0.0); + BOOST_CHECK_EQUAL(da.GetPercentile90(), 0.0); + + da.AddMeasurement(5.0); + da.AddMeasurement(10.0); + da.AddMeasurement(15.0); + + da.Compute(); + + BOOST_CHECK_EQUAL(da.GetMin(), 5.0); + BOOST_CHECK_EQUAL(da.GetMax(), 15.0); + BOOST_CHECK_EQUAL(da.GetAvg(), 10.0); + BOOST_CHECK_EQUAL(da.GetMedian(), 10.0); + BOOST_CHECK_EQUAL(da.GetPercentile25(), 7.5); + BOOST_CHECK_EQUAL(da.GetPercentile75(), 12.5); + BOOST_CHECK_EQUAL(da.GetPercentile90(), 14.0); + + + da.AddMeasurement(2.0); + da.AddMeasurement(8.0); + + da.Compute(); + + BOOST_CHECK_EQUAL(da.GetMin(), 2.0); + BOOST_CHECK_EQUAL(da.GetMax(), 15.0); + BOOST_CHECK_EQUAL(da.GetAvg(), 8.0); + BOOST_CHECK_EQUAL(da.GetMedian(), 8.0); + BOOST_CHECK_EQUAL(da.GetPercentile25(), 5.0); + BOOST_CHECK_EQUAL(da.GetPercentile75(), 10.0); + BOOST_CHECK_EQUAL(da.GetPercentile90(), 13.0); +} diff --git a/webui/index.html b/webui/index.html index 202bd569..f6385c0a 100644 --- a/webui/index.html +++ b/webui/index.html @@ -847,7 +847,13 @@

System

Private / Public IP - + + + + + diff --git a/webui/style.css b/webui/style.css index 6e1713ca..50fda922 100644 --- a/webui/style.css +++ b/webui/style.css @@ -105,6 +105,7 @@ body { gap: 10px; } +.system-info__network-speed-test-btn, #ConfigContent select.dist-speedtest__select--width { width: 80px; } @@ -161,7 +162,7 @@ body { } .btn--disabled { - opacity: 0.2 !important; + opacity: 0.7 !important; pointer-events: none; } diff --git a/webui/system-info.js b/webui/system-info.js index accfa1fd..cec8cb94 100644 --- a/webui/system-info.js +++ b/webui/system-info.js @@ -17,6 +17,9 @@ * along with this program. If not, see . */ +var SPINNER = 'progress_activity'; +var TEST_BTN_DEFAULT_TEXT = 'Run test'; +var NETWORK_SPEED_TEST_RUNNING = false; function DiskSpeedTestsForm() { @@ -28,9 +31,6 @@ function DiskSpeedTestsForm() var $diskSpeedTestInput; var $diskSpeedTestBtn; var $diskSpeedTestErrorTxt; - - var SPINNER = 'progress_activity'; - var TEST_BTN_DEFAULT_TEXT = 'Run test'; this.init = function(writeBuffer, dirPath, label, lsKey) { @@ -44,7 +44,7 @@ function DiskSpeedTestsForm() disableBtnToggle(false); - $diskSpeedTestBtn.text(getSpeedResFromLS(lsKey) || TEST_BTN_DEFAULT_TEXT); + $diskSpeedTestBtn.text(Util.getFromLocalStorage(lsKey) || TEST_BTN_DEFAULT_TEXT); $writeBufferInput.val(writeBuffer); $diskSpeedTestInputLabel.text(label); @@ -93,7 +93,7 @@ function DiskSpeedTestsForm() { var res = makeResults(rawRes); saveSpeedResToLS(lsKey, res); - $diskSpeedTestBtn.html(makeResults(rawRes)); + $diskSpeedTestBtn.html(res); disableBtnToggle(false); }, function(res) @@ -142,16 +142,6 @@ function DiskSpeedTestsForm() return path !== ''; } - - function saveSpeedResToLS(key, res) - { - localStorage.setItem(key, res); - } - - function getSpeedResFromLS(key) - { - return localStorage.getItem(key) || ''; - } } var SystemInfo = (new function($) @@ -169,6 +159,8 @@ var SystemInfo = (new function($) var $SysInfo_InterDiskSpace; var $SysInfo_DestDirDiskTestBtn; var $SysInfo_InterDirDiskTestBtn; + var $SysInfo_NetworkSpeedTestBtn; + var $SysInfo_NetworkSpeedTestErrorTxt; var $SysInfo_DestDiskSpaceContainer; var $SysInfo_InterDiskSpaceContainer; var $SysInfo_ArticleCache; @@ -180,7 +172,6 @@ var SystemInfo = (new function($) var $SysInfo_ErrorAlertText; var $SpeedTest_Stats; var $SpeedTest_StatsHeader; - var $SpeedTest_StatsTable; var $DiskSpeedTest_Modal; var $SystemInfo_Spinner; var $SystemInfo_MainContent; @@ -208,6 +199,8 @@ var SystemInfo = (new function($) var DEST_DIR_LS_KEY = 'DestDirSpeedResults'; var INTER_DIR_LS_KEY = 'InterDirSpeedResults'; + var NETWORK_SPEED_TEST_LS_KEY = 'NetworkSpeedResults'; + var NETWORK_SPEED_TEST_DATE_LS_KEY = 'NetworkSpeedDate'; var lastTestStatsBtns = {}; var spinners = {}; @@ -259,6 +252,8 @@ var SystemInfo = (new function($) $SysInfo_InterDiskSpace = $('#SysInfo_InterDiskSpace'); $SysInfo_DestDirDiskTestBtn = $('#SysInfo_DestDirDiskTestBtn'); $SysInfo_InterDirDiskTestBtn = $('#SysInfo_InterDirDiskTestBtn'); + $SysInfo_NetworkSpeedTestBtn = $('#SysInfo_NetworkSpeedTestBtn'); + $SysInfo_NetworkSpeedTestErrorTxt = $('#SysInfo_NetworkSpeedTestErrorTxt'); $SysInfo_InterDiskSpaceContainer = $('#SysInfo_InterDiskSpaceContainer'); $SysInfo_DestDiskSpaceContainer = $('#SysInfo_DestDiskSpaceContainer'); $SysInfo_ArticleCache = $('#SysInfo_ArticleCache'); @@ -275,6 +270,7 @@ var SystemInfo = (new function($) $SpeedTest_StatsTime = $('#SpeedTest_StatsTime'); $SpeedTest_StatsDate = $('#SpeedTest_StatsDate'); $DiskSpeedTest_Modal = $('#DiskSpeedTest_Modal'); + $NetworkSpeedTest_Modal = $('#NetworkSpeedTest_Modal'); $SystemInfo_Spinner = $('#SystemInfo_Spinner'); $SystemInfo_MainContent = $('#SystemInfo_MainContent'); @@ -449,7 +445,7 @@ var SystemInfo = (new function($) showDiskSpeedModal(writeBufferKB, dirPath, 'DestDir', DEST_DIR_LS_KEY); }); - var savedResults = localStorage.getItem(DEST_DIR_LS_KEY); + var savedResults = Util.getFromLocalStorage(DEST_DIR_LS_KEY); if (savedResults) { $SysInfo_DestDirDiskTestBtn.text(savedResults); @@ -465,7 +461,7 @@ var SystemInfo = (new function($) showDiskSpeedModal(writeBufferKB, dirPath, 'InterDir', INTER_DIR_LS_KEY); }); - var savedResults = localStorage.getItem(INTER_DIR_LS_KEY); + var savedResults = Util.getFromLocalStorage(INTER_DIR_LS_KEY); if (savedResults) { $SysInfo_InterDirDiskTestBtn.text(savedResults); @@ -488,6 +484,60 @@ var SystemInfo = (new function($) var privateIP = network.PrivateIP ? network.PrivateIP : 'N/A'; var publicIP = network.PublicIP ? network.PublicIP : 'N/A'; $SysInfo_IP.text(privateIP + ' / ' + publicIP); + + renderNetworkSpeedTestBtn(); + } + + function renderNetworkSpeedTestBtn() + { + var savedResults = Util.getFromLocalStorage(NETWORK_SPEED_TEST_LS_KEY); + if (savedResults && !NETWORK_SPEED_TEST_RUNNING) + { + $SysInfo_NetworkSpeedTestBtn.text(Util.formatNetworkSpeed(savedResults)); + } + else if (NETWORK_SPEED_TEST_RUNNING) + { + $SysInfo_NetworkSpeedTestBtn.html(SPINNER); + } + + var savedDate = Util.getFromLocalStorage(NETWORK_SPEED_TEST_DATE_LS_KEY); + if (savedDate) + { + renderNetworkSpeedTestBtnTitle(savedDate); + } + + $SysInfo_NetworkSpeedTestBtn.off('click').on('click', function() + { + $SysInfo_NetworkSpeedTestBtn.html(SPINNER); + $SysInfo_NetworkSpeedTestBtn.addClass('btn--disabled'); + $SysInfo_NetworkSpeedTestErrorTxt.empty(); + NETWORK_SPEED_TEST_RUNNING = true; + + RPC.call('testnetworkspeed', [], + function(rawRes) + { + Util.saveToLocalStorage(NETWORK_SPEED_TEST_LS_KEY, rawRes.SpeedMbps); + Util.saveToLocalStorage(NETWORK_SPEED_TEST_DATE_LS_KEY, Date.now()); + $SysInfo_NetworkSpeedTestBtn.html(Util.formatNetworkSpeed(rawRes.SpeedMbps)); + renderNetworkSpeedTestBtnTitle(savedDate); + $SysInfo_NetworkSpeedTestBtn.removeClass('btn--disabled'); + NETWORK_SPEED_TEST_RUNNING = false; + }, + function(res) + { + $SysInfo_NetworkSpeedTestBtn.text(TEST_BTN_DEFAULT_TEXT); + $SysInfo_NetworkSpeedTestBtn.removeClass('btn--disabled'); + var errTxt = res.split('
')[0]; + $SysInfo_NetworkSpeedTestErrorTxt.html(errTxt); + NETWORK_SPEED_TEST_RUNNING = false; + }, + ); + }); + } + + function renderNetworkSpeedTestBtnTitle(date) + { + $SysInfo_NetworkSpeedTestBtn.attr('title', 'Date: ' + Util.formatDateTime(date)); } function renderAppVersion(version) diff --git a/webui/util.js b/webui/util.js index 86cd4352..a6448a0d 100644 --- a/webui/util.js +++ b/webui/util.js @@ -31,6 +31,27 @@ var Util = (new function($) { 'use strict'; + this.saveToLocalStorage = function(key, val) + { + try + { + localStorage.setItem(key, val); + } catch (error) + { + console.error(error); + } + } + + this.getFromLocalStorage = function(key) + { + return localStorage.getItem(key); + } + + this.removeFromLocalStorage = function(key) + { + return localStorage.removeItem(key); + } + this.formatTimeHMS = function(sec) { var hms = ''; @@ -172,6 +193,42 @@ var Util = (new function($) return Util.round0(bytesPerSec / 1024.0) + ' KB/s'; } + this.formatNetworkSpeed = function(speedMbps) + { + if (speedMbps <= 0) + { + return ''; + } + + if (speedMbps >= 10000) + { + return Util.round1(speedMbps / 10000) + ' Gbps'; + } + + if (speedMbps >= 1000) + { + return Util.round1(speedMbps / 1000) + ' Gbps'; + } + else if (speedMbps >= 100) + { + return Util.round0(speedMbps) + ' Mbps'; + } + else if (speedMbps >= 10) + { + return Util.round1(speedMbps) + ' Mbps'; + } + else if (speedMbps >= 1) + { + return Util.round2(speedMbps) + ' Mbps'; + } + else if (speedMbps >= 0.1) + { + return Util.round0(speedMbps * 1000) + ' Kbps'; + } + + return Util.round0(speedMbps * 1000) + ' Bbps'; + } + this.formatSpeedWithCustomUnit = function (bytesPerSec, unit) { var res = this.formatSpeed(bytesPerSec);