mirror of
https://github.com/kasmtech/KasmVNC.git
synced 2025-07-22 00:28:10 +02:00
397 lines
12 KiB
C++
397 lines
12 KiB
C++
/* Copyright 2015 Pierre Ossman <ossman@cendio.se> for Cendio AB
|
|
* Copyright (C) 2015 D. R. Commander. All Rights Reserved.
|
|
* Copyright (C) 2025 Kasm Technologies Corp
|
|
*
|
|
* This 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 software 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 software; if not, write to the Free Software
|
|
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
|
|
* USA.
|
|
*/
|
|
|
|
#include "benchmark.h"
|
|
#include <string_view>
|
|
#include <rfb/LogWriter.h>
|
|
#include <numeric>
|
|
#include <tinyxml2.h>
|
|
#include <algorithm>
|
|
#include <cassert>
|
|
|
|
#include "ServerCore.h"
|
|
#include <cmath>
|
|
|
|
#include "EncCache.h"
|
|
#include "EncodeManager.h"
|
|
#include "SConnection.h"
|
|
#include "screenTypes.h"
|
|
#include "SMsgWriter.h"
|
|
#include "UpdateTracker.h"
|
|
#include "rdr/BufferedInStream.h"
|
|
#include "rdr/OutStream.h"
|
|
#include "ffmpeg.h"
|
|
|
|
namespace benchmarking {
|
|
class MockBufferStream final : public rdr::BufferedInStream {
|
|
bool fillBuffer(size_t maxSize, bool wait) override {
|
|
return true;
|
|
}
|
|
};
|
|
|
|
class MockStream final : public rdr::OutStream {
|
|
public:
|
|
MockStream() {
|
|
offset = 0;
|
|
ptr = buf;
|
|
end = buf + sizeof(buf);
|
|
}
|
|
|
|
private:
|
|
void overrun(size_t needed) override {
|
|
assert(end >= ptr);
|
|
if (needed > static_cast<size_t>(end - ptr))
|
|
flush();
|
|
}
|
|
|
|
public:
|
|
size_t length() override {
|
|
flush();
|
|
return offset;
|
|
}
|
|
|
|
void flush() override {
|
|
offset += ptr - buf;
|
|
ptr = buf;
|
|
}
|
|
|
|
private:
|
|
ptrdiff_t offset;
|
|
rdr::U8 buf[8192]{};
|
|
};
|
|
|
|
class MockSConnection final : public rfb::SConnection {
|
|
public:
|
|
MockSConnection() {
|
|
setStreams(nullptr, &out);
|
|
|
|
setWriter(new rfb::SMsgWriter(&cp, &out, &udps));
|
|
}
|
|
|
|
~MockSConnection() override = default;
|
|
|
|
void writeUpdate(const rfb::UpdateInfo &ui, const rfb::PixelBuffer *pb) {
|
|
cache.clear();
|
|
|
|
manager.clearEncodingTime();
|
|
if (!ui.is_empty()) {
|
|
manager.writeUpdate(ui, pb, nullptr);
|
|
} else {
|
|
rfb::Region region{pb->getRect()};
|
|
manager.writeLosslessRefresh(region, pb, nullptr, 2000);
|
|
}
|
|
}
|
|
|
|
void setDesktopSize(int fb_width, int fb_height,
|
|
const rfb::ScreenSet &layout) override {
|
|
cp.width = fb_width;
|
|
cp.height = fb_height;
|
|
cp.screenLayout = layout;
|
|
|
|
writer()->writeExtendedDesktopSize(rfb::reasonServer, 0, cp.width, cp.height,
|
|
cp.screenLayout);
|
|
}
|
|
|
|
void sendStats(const bool toClient) override {
|
|
}
|
|
|
|
[[nodiscard]] bool canChangeKasmSettings() const override {
|
|
return true;
|
|
}
|
|
|
|
void udpUpgrade(const char *resp) override {
|
|
}
|
|
|
|
void udpDowngrade(const bool) override {
|
|
}
|
|
|
|
void subscribeUnixRelay(const char *name) override {
|
|
}
|
|
|
|
void unixRelay(const char *name, const rdr::U8 *buf, const unsigned len) override {
|
|
}
|
|
|
|
void handleFrameStats(rdr::U32 all, rdr::U32 render) override {
|
|
}
|
|
|
|
[[nodiscard]] auto getJpegStats() const {
|
|
return manager.jpegstats;
|
|
}
|
|
|
|
[[nodiscard]] auto getWebPStats() const {
|
|
return manager.webpstats;
|
|
}
|
|
|
|
[[nodiscard]] auto bytes() { return out.length(); }
|
|
[[nodiscard]] auto udp_bytes() { return udps.length(); }
|
|
|
|
protected:
|
|
MockStream out{};
|
|
MockStream udps{};
|
|
|
|
EncCache cache{};
|
|
EncodeManager manager{this, &cache};
|
|
};
|
|
|
|
class MockCConnection final : public MockTestConnection {
|
|
public:
|
|
explicit MockCConnection(const std::vector<rdr::S32> &encodings, rfb::ManagedPixelBuffer *pb) {
|
|
setStreams(&in, nullptr);
|
|
|
|
// Need to skip the initial handshake and ServerInit
|
|
setState(RFBSTATE_NORMAL);
|
|
// That also means that the reader and writer weren't set up
|
|
setReader(new rfb::CMsgReader(this, &in));
|
|
auto &pf = pb->getPF();
|
|
CMsgHandler::setPixelFormat(pf);
|
|
|
|
MockCConnection::setDesktopSize(pb->width(), pb->height());
|
|
|
|
cp.setPF(pf);
|
|
|
|
sc.cp.setPF(pf);
|
|
sc.setEncodings(std::size(encodings), encodings.data());
|
|
|
|
setFramebuffer(pb);
|
|
}
|
|
|
|
void setCursor(int width, int height, const rfb::Point &hotspot, const rdr::U8 *data,
|
|
const bool resizing) override {
|
|
}
|
|
|
|
~MockCConnection() override = default;
|
|
|
|
struct stats_t {
|
|
EncodeManager::codecstats_t jpeg_stats;
|
|
EncodeManager::codecstats_t webp_stats;
|
|
uint64_t bytes;
|
|
uint64_t udp_bytes;
|
|
};
|
|
|
|
[[nodiscard]] stats_t getStats() {
|
|
return {
|
|
sc.getJpegStats(),
|
|
sc.getWebPStats(),
|
|
sc.bytes(),
|
|
sc.udp_bytes()
|
|
};
|
|
}
|
|
|
|
void setDesktopSize(int w, int h) override {
|
|
CConnection::setDesktopSize(w, h);
|
|
|
|
if (screen_layout.num_screens())
|
|
screen_layout.remove_screen(0);
|
|
|
|
screen_layout.add_screen(rfb::Screen(0, 0, 0, w, h, 0));
|
|
}
|
|
|
|
void setNewFrame(const AVFrame *frame) override {
|
|
auto *pb = getFramebuffer();
|
|
const int width = pb->width();
|
|
const int height = pb->height();
|
|
const rfb::Rect rect(0, 0, width, height);
|
|
|
|
int dstStride{};
|
|
auto *buffer = pb->getBufferRW(rect, &dstStride);
|
|
|
|
const rfb::PixelFormat &pf = pb->getPF();
|
|
|
|
// Source data and stride from FFmpeg
|
|
const auto *srcData = frame->data[0];
|
|
const int srcStride = frame->linesize[0] / 3; // Convert bytes to pixels
|
|
|
|
// Convert from the RGB format to the PixelBuffer's format
|
|
pf.bufferFromRGB(buffer, srcData, width, srcStride, height);
|
|
|
|
// Commit changes
|
|
pb->commitBufferRW(rect);
|
|
}
|
|
|
|
void framebufferUpdateStart() override {
|
|
updates.clear();
|
|
}
|
|
|
|
void framebufferUpdateEnd() override {
|
|
const rfb::PixelBuffer *pb = getFramebuffer();
|
|
|
|
rfb::UpdateInfo ui;
|
|
const rfb::Region clip(pb->getRect());
|
|
|
|
updates.add_changed(pb->getRect());
|
|
|
|
updates.getUpdateInfo(&ui, clip);
|
|
sc.writeUpdate(ui, pb);
|
|
}
|
|
|
|
void dataRect(const rfb::Rect &r, int encoding) override {
|
|
}
|
|
|
|
void setColourMapEntries(int, int, rdr::U16 *) override {
|
|
}
|
|
|
|
void bell() override {
|
|
}
|
|
|
|
void serverCutText(const char *, rdr::U32) override {
|
|
}
|
|
|
|
void serverCutText(const char *str) override {
|
|
}
|
|
|
|
protected:
|
|
MockBufferStream in;
|
|
rfb::ScreenSet screen_layout;
|
|
rfb::SimpleUpdateTracker updates;
|
|
MockSConnection sc;
|
|
};
|
|
}
|
|
|
|
void report(std::vector<uint64_t> &totals, std::vector<uint64_t> &timings,
|
|
std::vector<benchmarking::MockCConnection::stats_t> &stats, const std::string_view results_file) {
|
|
auto totals_sum = std::accumulate(totals.begin(), totals.end(), 0.);
|
|
auto totals_avg = totals_sum / static_cast<double>(totals.size());
|
|
|
|
auto variance = 0.;
|
|
for (auto t: totals)
|
|
variance += (static_cast<double>(t) - totals_avg) * (static_cast<double>(t) - totals_avg);
|
|
|
|
variance /= static_cast<double>(totals.size());
|
|
auto stddev = std::sqrt(variance);
|
|
|
|
const auto sum = std::accumulate(timings.begin(), timings.end(), 0.);
|
|
const auto size = timings.size();
|
|
const auto average = sum / static_cast<double>(size);
|
|
|
|
double median{};
|
|
|
|
std::sort(timings.begin(), timings.end());
|
|
if (size % 2 == 0)
|
|
median = static_cast<double>(timings[size / 2]);
|
|
else
|
|
median = static_cast<double>(timings[size / 2 - 1] + timings[size / 2]) / 2.;
|
|
|
|
vlog.info("Mean time encoding frame: %f ms", average);
|
|
vlog.info("Median time encoding frame: %f ms", median);
|
|
vlog.info("Total time (mean): %f ms", totals_avg);
|
|
vlog.info("Total time (stddev): %f ms", stddev);
|
|
|
|
uint32_t jpeg_sum{}, jpeg_rects{}, webp_sum{}, webp_rects{};
|
|
uint64_t bytes{};
|
|
|
|
for (const auto &item: stats) {
|
|
jpeg_sum += item.jpeg_stats.ms;
|
|
jpeg_rects += item.jpeg_stats.rects;
|
|
webp_sum += item.webp_stats.ms;
|
|
webp_rects += item.webp_stats.rects;
|
|
bytes += item.bytes;
|
|
}
|
|
|
|
auto jpeg_ms = jpeg_sum / static_cast<double>(stats.size());
|
|
vlog.info("JPEG stats: %f ms", jpeg_ms);
|
|
jpeg_rects /= stats.size();
|
|
vlog.info("JPEG stats: %u rects", jpeg_rects);
|
|
auto webp_ms = webp_sum / static_cast<double>(stats.size());
|
|
webp_rects /= stats.size();
|
|
bytes /= stats.size();
|
|
vlog.info("WebP stats: %f ms", webp_ms);
|
|
vlog.info("WebP stats: %u rects", webp_rects);
|
|
vlog.info("Total bytes sent: %lu bytes", bytes);
|
|
|
|
tinyxml2::XMLDocument doc;
|
|
|
|
auto *test_suit = doc.NewElement("testsuite");
|
|
test_suit->SetAttribute("name", "Benchmark");
|
|
|
|
doc.InsertFirstChild(test_suit);
|
|
auto total_tests{0};
|
|
|
|
auto add_benchmark_item = [&doc, &test_suit, &total_tests](const char *name, auto time_value, auto other_value) {
|
|
auto *test_case = doc.NewElement("testcase");
|
|
test_case->SetAttribute("name", name);
|
|
test_case->SetAttribute("file", other_value);
|
|
test_case->SetAttribute("time", time_value);
|
|
test_case->SetAttribute("runs", 1);
|
|
test_case->SetAttribute("classname", "KasmVNC");
|
|
|
|
test_suit->InsertEndChild(test_case);
|
|
|
|
++total_tests;
|
|
};
|
|
|
|
constexpr auto mult = 1 / 1000.;
|
|
add_benchmark_item("Average time encoding frame, ms", average * mult, "");
|
|
add_benchmark_item("Median time encoding frame, ms", median * mult, "");
|
|
add_benchmark_item("Total time encoding, ms", 0, totals_avg);
|
|
add_benchmark_item("Total time encoding, stddev", 0, stddev);
|
|
add_benchmark_item("Mean JPEG stats, ms", jpeg_ms, "");
|
|
add_benchmark_item("Mean JPEG stats, rects", 0., jpeg_rects);
|
|
add_benchmark_item("Mean WebP stats, ms", webp_ms, "");
|
|
add_benchmark_item("Mean WebP stats, rects", 0, webp_rects);
|
|
|
|
add_benchmark_item("Data sent, KBs", 0, bytes / 1024);
|
|
|
|
doc.SaveFile(results_file.data());
|
|
}
|
|
|
|
void benchmark(std::string_view path, const std::string_view results_file) {
|
|
try {
|
|
vlog.info("Benchmarking with video file %s", path.data());
|
|
FFmpegFrameFeeder frame_feeder{};
|
|
frame_feeder.open(path);
|
|
|
|
static const rfb::PixelFormat pf{32, 24, false, true, 0xFF, 0xFF, 0xFF, 0, 8, 16};
|
|
std::vector<rdr::S32> encodings{
|
|
std::begin(benchmarking::default_encodings), std::end(benchmarking::default_encodings)
|
|
};
|
|
|
|
constexpr auto runs = 20;
|
|
std::vector<uint64_t> totals(runs, 0);
|
|
std::vector<benchmarking::MockCConnection::stats_t> stats(runs);
|
|
std::vector<uint64_t> timings{};
|
|
auto [width, height] = frame_feeder.get_frame_dimensions();
|
|
|
|
for (int run = 0; run < runs; ++run) {
|
|
auto *pb = new rfb::ManagedPixelBuffer{pf, width, height};
|
|
benchmarking::MockCConnection connection{encodings, pb};
|
|
|
|
vlog.info("RUN %d. Reading frames...", run);
|
|
auto play_stats = frame_feeder.play(&connection);
|
|
vlog.info("RUN %d. Done reading frames...", run);
|
|
|
|
timings.insert(timings.end(), play_stats.timings.begin(), play_stats.timings.end());
|
|
|
|
totals[run] = play_stats.total;
|
|
stats[run] = connection.getStats();
|
|
vlog.info("JPEG stats: %u ms", stats[run].jpeg_stats.ms);
|
|
vlog.info("WebP stats: %u ms", stats[run].webp_stats.ms);
|
|
vlog.info("RUN %d. Bytes sent %lu..", run, stats[run].bytes);
|
|
}
|
|
|
|
if (!timings.empty())
|
|
report(totals, timings, stats, results_file);
|
|
|
|
exit(0);
|
|
} catch (std::exception &e) {
|
|
vlog.error("Benchmarking failed: %s", e.what());
|
|
exit(1);
|
|
}
|
|
}
|