Skip to content

Commit

Permalink
Disk performance tests (#375)
Browse files Browse the repository at this point in the history
- added disk performance tests
- added the new `testdiskspeed` API method:
    #### Arguments
    - **dirPath** `(string)` - The path to the directory where the test file will be created.
    - **writeBuffer** `(int)` - The size of the buffer used for writing data to the file (in KiB).
    - **maxFileSize** `(int)` - The maximum size of the file to be created (in GiB).
    - **timeout** `(int)` - Test timeout (in seconds).
    
    #### Return value
    - **SizeMB** `(int)` - Written data size (in MiB)`.
    - **DurationMS** `(int)` - Test duration (in milliseconds).
  • Loading branch information
dnzbk authored Sep 3, 2024
1 parent cc8ff58 commit 220e842
Show file tree
Hide file tree
Showing 13 changed files with 740 additions and 33 deletions.
3 changes: 2 additions & 1 deletion daemon/main/nzbget.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
* 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 <http://www.gnu.org/licenses/>.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/


Expand Down Expand Up @@ -224,6 +224,7 @@ compiled */
#include <variant>
#include <limits>
#include <type_traits>
#include <random>

#include <libxml/parser.h>
#include <libxml/xmlreader.h>
Expand Down
118 changes: 118 additions & 0 deletions daemon/remote/XmlRpc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
#include "UrlCoordinator.h"
#include "ExtensionManager.h"
#include "SystemInfo.h"
#include "Benchmark.h"
#include "Xml.h"

extern void ExitProc();
extern void Reload();
Expand Down Expand Up @@ -361,6 +363,42 @@ class TestServerSpeedXmlCommand: public SafeXmlCommand
virtual void Execute();
};

class TestDiskSpeedXmlCommand final : public SafeXmlCommand
{
public:
void Execute() override;

std::string ToJsonStr(uint64_t size, double time)
{
Json::JsonObject json;

json["SizeMB"] = size / 1024ull / 1024ull;
json["DurationMS"] = time;

return Json::Serialize(json);
}

std::string ToXmlStr(uint64_t size, double time)
{
xmlNodePtr rootNode = xmlNewNode(nullptr, BAD_CAST "value");
xmlNodePtr structNode = xmlNewNode(nullptr, BAD_CAST "struct");

std::string sizeMB = std::to_string(size / 1024ull / 1024ull);
std::string durationMS = std::to_string(time);

Xml::AddNewNode(structNode, "SizeMB", "i4", sizeMB.c_str());
Xml::AddNewNode(structNode, "DurationMS", "double", durationMS.c_str());

xmlAddChild(rootNode, structNode);

std::string result = Xml::Serialize(rootNode);

xmlFreeNode(rootNode);

return result;
}
};

class StartScriptXmlCommand : public XmlCommand
{
public:
Expand Down Expand Up @@ -817,6 +855,10 @@ std::unique_ptr<XmlCommand> XmlRpcProcessor::CreateCommand(const char* methodNam
{
command = std::make_unique<TestServerSpeedXmlCommand>();
}
else if (!strcasecmp(methodName, "testdiskspeed"))
{
command = std::make_unique<TestDiskSpeedXmlCommand>();
}
else if (!strcasecmp(methodName, "startscript"))
{
command = std::make_unique<StartScriptXmlCommand>();
Expand Down Expand Up @@ -3696,6 +3738,82 @@ void TestServerSpeedXmlCommand::Execute()
BuildBoolResponse(true);
}



void TestDiskSpeedXmlCommand::Execute()
{
char* dirPath;
int writeBufferKiB;
int maxFileSizeGiB;
int timeoutSec;

if (!NextParamAsStr(&dirPath))
{
BuildErrorResponse(2, "Invalid argument (Path)");
return;
}

if (!NextParamAsInt(&writeBufferKiB))
{
BuildErrorResponse(2, "Invalid argument (Write Buffer)");
return;
}

if (writeBufferKiB < 0)
{
BuildErrorResponse(2, "Write Buffer cannot be negative");
return;
}

if (!NextParamAsInt(&maxFileSizeGiB))
{
BuildErrorResponse(2, "Invalid argument (Max file size)");
return;
}

if (maxFileSizeGiB < 1)
{
BuildErrorResponse(2, "Max file size must be positive");
return;
}

if (!NextParamAsInt(&timeoutSec))
{
BuildErrorResponse(2, "Invalid argument (Timeout)");
return;
}

if (timeoutSec < 1)
{
BuildErrorResponse(2, "Timeout must be positive");
return;
}

try
{
size_t bufferSizeBytes = writeBufferKiB * 1024;
uint64_t maxFileSizeBytes = maxFileSizeGiB * 1024ull * 1024ull * 1024ull;

Benchmark::DiskBenchmark db;
auto [size, time] = db.Run(
dirPath,
bufferSizeBytes,
maxFileSizeBytes,
std::chrono::seconds(timeoutSec)
);

std::string jsonStr = IsJson() ?
ToJsonStr(size, time) :
ToXmlStr(size, time);

AppendResponse(jsonStr.c_str());
}
catch (const std::exception& e)
{
BuildErrorResponse(2, e.what());
}
}

// bool startscript(string script, string command, string context, struct[] options);
void StartScriptXmlCommand::Execute()
{
Expand Down
1 change: 1 addition & 0 deletions daemon/sources.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ set(SRC
${CMAKE_SOURCE_DIR}/daemon/util/Util.cpp
${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/system/SystemInfo.cpp
${CMAKE_SOURCE_DIR}/daemon/system/OS.cpp
Expand Down
170 changes: 170 additions & 0 deletions daemon/util/Benchmark.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/*
* This file is part of nzbget. See <https://nzbget.com>.
*
* Copyright (C) 2024 Denis <denis@nzbget.com>
*
* 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 <exception>
#include <random>
#include "FileSystem.h"
#include "Benchmark.h"

namespace Benchmark
{
using namespace std::chrono;

std::pair<uint64_t, double> DiskBenchmark::Run(
const std::string& dir,
size_t bufferSizeBytes,
uint64_t maxFileSizeBytes,
seconds timeout) const noexcept(false)
{
ValidateBufferSize(bufferSizeBytes);

const std::string filename = dir + PATH_SEPARATOR + GetUniqueFilename();
std::ofstream file = OpenFile(filename);

std::vector<char> buffer;
UseBuffer(file, buffer, bufferSizeBytes);

size_t dataSize = bufferSizeBytes > 0 ? bufferSizeBytes : 1024;
std::vector<char> data = GenerateRandomCharsVec(dataSize);

nanoseconds timeoutNS = duration_cast<nanoseconds>(timeout);

return RunBench(file, filename, data, maxFileSizeBytes, timeoutNS);
}

std::pair<uint64_t, double> DiskBenchmark::RunBench(
std::ofstream& file,
const std::string& filename,
const std::vector<char>& data,
uint64_t maxFileSizeBytes,
std::chrono::nanoseconds timeoutNS) const noexcept(false)
{
uint64_t totalWritten = 0;

auto start = steady_clock::now();
try
{
while (totalWritten < maxFileSizeBytes && (steady_clock::now() - start) < timeoutNS)
{
file.write(data.data(), data.size());
totalWritten += data.size();
}
}
catch (const std::exception& e)
{
CleanUp(file, filename);

std::string errMsg = "Failed to write data to file " + filename + ". " + e.what();
throw std::runtime_error(errMsg);
}
auto finish = steady_clock::now();
double elapsed = duration<double, std::milli>(finish - start).count();

CleanUp(file, filename);

return { totalWritten, elapsed };
}

void DiskBenchmark::DeleteFile(const std::string& filename) const noexcept(false)
{
if (!FileSystem::DeleteFile(filename.c_str()))
{
std::string errMsg = "Failed to delete " + filename + " test file";
throw std::runtime_error(errMsg);
}
}

void DiskBenchmark::CleanUp(
std::ofstream& file,
const std::string& filename) const noexcept(false)
{
file.close();
DeleteFile(filename);
}

std::string DiskBenchmark::GetUniqueFilename() const noexcept(false)
{
return std::to_string(
high_resolution_clock::now().time_since_epoch().count()
) + ".bin";
}

std::ofstream DiskBenchmark::OpenFile(const std::string& filename) const noexcept(false)
{
std::ofstream file(filename, std::ios::binary);

if (!file.is_open())
{
std::string errMsg = std::string("Failed to create test file: ") + strerror(errno);
throw std::runtime_error(errMsg);
}

file.exceptions(std::ofstream::badbit | std::ofstream::failbit);
file.sync_with_stdio(false);

return file;
}

void DiskBenchmark::ValidateBufferSize(size_t bufferSz) const noexcept(false)
{
if (bufferSz > m_maxBufferSize)
{
throw std::invalid_argument("The buffer size is too big");
}
}

void DiskBenchmark::UseBuffer(
std::ofstream& file,
std::vector<char>& buffer,
size_t buffSize
) const noexcept(false)
{
if (buffSize > 0)
{
buffer.resize(buffSize);
file.rdbuf()->pubsetbuf(buffer.data(), buffSize);
}
else
{
file.rdbuf()->pubsetbuf(nullptr, 0);
}
}

std::vector<char> DiskBenchmark::GenerateRandomCharsVec(size_t size) const noexcept(false)
{
if (size == 0)
{
return {};
}

std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<> distrib(0, 127);

std::vector<char> v(size);
std::generate(begin(v), end(v), [&distrib, &gen]()
{
return static_cast<char>(distrib(gen));
});

return v;
}
}
Loading

0 comments on commit 220e842

Please sign in to comment.