Skip to content

Commit

Permalink
Add: network speed tests
Browse files Browse the repository at this point in the history
  • Loading branch information
dnzbk committed Jan 30, 2025
1 parent c7bd317 commit 28928f3
Show file tree
Hide file tree
Showing 19 changed files with 708 additions and 33 deletions.
2 changes: 1 addition & 1 deletion daemon/connect/HttpClient.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
#include "HttpClient.h"
#include "Util.h"

namespace HttpClient
namespace Network
{
namespace asio = boost::asio;
using tcp = boost::asio::ip::tcp;
Expand Down
2 changes: 1 addition & 1 deletion daemon/connect/HttpClient.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
#include <boost/asio/ssl.hpp>
#endif

namespace HttpClient
namespace Network
{
#if !defined(DISABLE_TLS) && defined(HAVE_OPENSSL)
using Socket = boost::asio::ssl::stream<boost::asio::ip::tcp::socket>;
Expand Down
177 changes: 177 additions & 0 deletions daemon/connect/NetworkSpeedTest.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
/*
* This file is part of nzbget. See <https://nzbget.com>.
*
* Copyright (C) 2025 Denis <[email protected]>
*
* 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 <https://www.gnu.org/licenses/>.
*/

#include "nzbget.h"

#include <sstream>
#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<uint32_t, duration<double>> 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<double>(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();
}
}
96 changes: 96 additions & 0 deletions daemon/connect/NetworkSpeedTest.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* This file is part of nzbget. See <https://nzbget.com>.
*
* Copyright (C) 2025 Denis <[email protected]>
*
* 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 <https://www.gnu.org/licenses/>.
*/

#ifndef NETWORK_SPEED_TEST_H
#define NETWORK_SPEED_TEST_H

#include <array>
#include <chrono>
#include <string_view>

#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<TestSpec, 7> 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<uint32_t, std::chrono::duration<double>> 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<DataAnalytics> m_dataset;
};
}

#endif
1 change: 1 addition & 0 deletions daemon/main/nzbget.h
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@
#include <unordered_map>
#include <iterator>
#include <algorithm>
#include <numeric>
#include <fstream>
#include <memory>
#include <functional>
Expand Down
61 changes: 61 additions & 0 deletions daemon/remote/XmlRpc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
#include "ExtensionManager.h"
#include "SystemInfo.h"
#include "Benchmark.h"
#include "NetworkSpeedTest.h"
#include "Xml.h"

extern void ExitProc();
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -859,6 +893,10 @@ std::unique_ptr<XmlCommand> XmlRpcProcessor::CreateCommand(const char* methodNam
{
command = std::make_unique<TestDiskSpeedXmlCommand>();
}
else if (!strcasecmp(methodName, "testnetworkspeed"))
{
command = std::make_unique<TestNetworkSpeedXmlCommand>();
}
else if (!strcasecmp(methodName, "startscript"))
{
command = std::make_unique<StartScriptXmlCommand>();
Expand Down Expand Up @@ -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()
{
Expand Down
Loading

0 comments on commit 28928f3

Please sign in to comment.