ZaStoGram_desktop/Telegram/SourceFiles/mtproto/web_proxy/web_proxy_selftest.cpp
loop-uh 84e2237558
All checks were successful
Desktop source guards / guards (push) Successful in 6s
Rework the WEB proxy data path around measured limits
A stand (test relay with a sink/echo backend, WAN emulator, env-gated
self-test in the client) showed the dev-17 limits, not the relay, capping
throughput: a fixed 1 MiB upload window and 2 MiB download credit, plus
2 file sessions per DC, while every bridge write waited for its own
round trip through the WebView.

- Bridge: frames written in one carrier turn go to the page as one
  batch, up to 4 page calls are in flight, and the injected script
  joins the frames the page posts in one task into one message.
- Windows: the upload window and the shared download credit follow the
  bandwidth-delay product of the credit loop plus 100 ms of queue
  (AdaptiveWindow), with a periodic drain to keep the base honest, a
  per-direction share when both are busy, and a hold while new streams'
  initial credit floods the relay's downlink.
- Upload and download session counts are upstream's again; upload
  frames shrink with a small window.
- Media sessions over MTProxy and WEB drop a regular temporary key
  borrowed from their DC and use the media cluster key: the regular key
  sent to a -N DC was answered with -404 and destroyed in a loop.
- web_carrier summaries report windows, rates, delays and bridge stats.
2026-09-24 03:18:20 +03:00

509 lines
13 KiB
C++

/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "mtproto/web_proxy/web_proxy_selftest.h"
#include "base/invoke_queued.h"
#include "logs.h"
#include "mtproto/web_proxy/web_proxy_transport.h"
#include "mtproto/web_proxy/web_proxy_webview.h"
#include <QtCore/QCoreApplication>
#include <QtCore/QFile>
#include <QtCore/QTimer>
#include <crl/crl_time.h>
#include <rpl/lifetime.h>
#include <algorithm>
#include <vector>
namespace MTP::WebProxy {
namespace {
constexpr auto kPingInterval = crl::time(100);
constexpr auto kPumpInterval = 5;
constexpr auto kIdlePhase = crl::time(3000);
constexpr auto kPhaseTimeout = crl::time(180 * 1000);
constexpr auto kClientBacklog = int64(1024 * 1024);
constexpr auto kChunk = 64 * 1024;
constexpr auto kPingSize = 16;
struct Config {
QString host;
QString secret;
int64 up = 32 * 1024 * 1024;
int64 down = 32 * 1024 * 1024;
int upStreams = 1;
int downStreams = 1;
QString ctl;
QStringList nets;
};
[[nodiscard]] std::optional<Config> ParseConfig() {
const auto value = qEnvironmentVariable("TDESKTOP_WEB_PROXY_SELFTEST");
const auto parts = value.split(':');
if (parts.size() < 2) {
return std::nullopt;
}
auto result = Config{ .host = parts[0], .secret = parts[1] };
for (auto i = 2; i < parts.size(); ++i) {
const auto pair = parts[i].split('=');
if (pair.size() != 2) {
continue;
} else if (pair[0] == u"ctl"_q) {
result.ctl = pair[1];
continue;
} else if (pair[0] == u"net"_q) {
result.nets = pair[1].split('|', Qt::SkipEmptyParts);
continue;
}
const auto number = pair[1].toLongLong();
if (number <= 0) {
continue;
} else if (pair[0] == u"up"_q) {
result.up = number * 1024 * 1024;
} else if (pair[0] == u"down"_q) {
result.down = number * 1024 * 1024;
} else if (pair[0] == u"ul"_q) {
result.upStreams = int(std::min(number, 16LL));
} else if (pair[0] == u"dl"_q) {
result.downStreams = int(std::min(number, 16LL));
}
}
return result;
}
[[nodiscard]] QByteArray Filler(int size) {
auto result = QByteArray(size, Qt::Uninitialized);
for (auto i = 0; i != size; ++i) {
result[i] = char((i * 131) ^ (i >> 7));
}
return result;
}
class SelfTest final : public QObject {
public:
SelfTest(not_null<Transport*> transport, Config config);
void start();
private:
enum class Phase {
Idle,
Upload,
Download,
Mixed,
Done,
};
struct Stream {
uint32 id = 0;
std::shared_ptr<StreamProbe> probe;
char kind = 0;
int64 total = 0;
int64 handed = 0;
int64 received = 0;
bool connected = false;
bool done = false;
crl::time finishedAt = 0;
QByteArray buffer;
};
struct Snapshot {
crl::time at = 0;
int64 upWrites = 0;
int64 upBytes = 0;
int64 upAckMs = 0;
int64 downMessages = 0;
int64 downBytes = 0;
};
Stream &open(char kind, int64 total);
void opened(uint32 id);
void received(uint32 id, QByteArray data);
void send(Stream &stream, QByteArray data);
void tick();
void pump(Stream &stream);
void ping();
void beginRound();
void beginPhase(Phase phase);
void finishPhase();
[[nodiscard]] bool phaseDone();
[[nodiscard]] Stream *find(uint32 id);
[[nodiscard]] static Snapshot Take();
[[nodiscard]] static QString PhaseName(Phase phase);
const not_null<Transport*> _transport;
const Config _config;
const QByteArray _filler;
std::vector<Stream> _streams;
uint32 _pingId = 0;
int64 _pingSequence = 0;
std::vector<crl::time> _pings;
Phase _phase = Phase::Idle;
int _round = 0;
Snapshot _phaseStart;
crl::time _lastPing = 0;
QTimer *_timer = nullptr;
};
SelfTest::SelfTest(not_null<Transport*> transport, Config config)
: _transport(transport)
, _config(std::move(config))
, _filler(Filler(kChunk)) {
}
void SelfTest::start() {
LOG(("Web Proxy SelfTest: start, up %1 MB x%2, down %3 MB x%4."
).arg(_config.up / (1024 * 1024)
).arg(_config.upStreams
).arg(_config.down / (1024 * 1024)
).arg(_config.downStreams));
_pingId = open('E', 0).id;
_timer = new QTimer(this);
_timer->setInterval(kPumpInterval);
QObject::connect(_timer, &QTimer::timeout, this, [=] { tick(); });
_timer->start();
beginRound();
}
void SelfTest::beginRound() {
if (_round < _config.nets.size() && !_config.ctl.isEmpty()) {
auto file = QFile(_config.ctl);
if (file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
file.write(_config.nets[_round].toUtf8().replace('/', ' '));
}
LOG(("Web Proxy SelfTest: round %1 net %2."
).arg(_round + 1
).arg(_config.nets[_round]));
}
beginPhase(Phase::Idle);
}
auto SelfTest::open(char kind, int64 total) -> Stream & {
_streams.push_back({
.id = Transport::NextStreamId(),
.probe = std::make_shared<StreamProbe>(),
.kind = kind,
.total = total,
});
auto &stream = _streams.back();
const auto id = stream.id;
_transport->registerStream(id, {
.context = this,
.connected = [=] { opened(id); },
.data = [=](QByteArray data) { received(id, std::move(data)); },
.disconnected = [=] {
LOG(("Web Proxy SelfTest: stream %1 closed by relay.").arg(id));
},
.failed = [=] {
LOG(("Web Proxy SelfTest: stream %1 failed.").arg(id));
},
.streamClass = (kind == 'S')
? StreamClass::Upload
: (kind == 'D')
? StreamClass::Download
: StreamClass::Interactive,
.probe = stream.probe,
});
return stream;
}
SelfTest::Stream *SelfTest::find(uint32 id) {
const auto i = ranges::find(_streams, id, &Stream::id);
return (i != end(_streams)) ? &*i : nullptr;
}
void SelfTest::send(Stream &stream, QByteArray data) {
stream.handed += data.size();
_transport->sendData(stream.id, std::move(data));
}
void SelfTest::opened(uint32 id) {
const auto stream = find(id);
if (!stream) {
return;
}
stream->connected = true;
auto tag = QByteArray(1, stream->kind);
if (stream->kind == 'D') {
const auto size = uint32(stream->total);
tag.append(char(size >> 24));
tag.append(char(size >> 16));
tag.append(char(size >> 8));
tag.append(char(size));
}
send(*stream, tag);
}
void SelfTest::received(uint32 id, QByteArray data) {
const auto stream = find(id);
if (!stream) {
return;
}
stream->received += data.size();
_transport->grantWindow(id, uint32(data.size()));
if (stream->kind != 'E') {
return;
}
stream->buffer.append(data);
const auto now = crl::now();
while (stream->buffer.size() >= kPingSize) {
auto sent = int64();
memcpy(&sent, stream->buffer.constData(), sizeof(sent));
stream->buffer.remove(0, kPingSize);
_pings.push_back(now - sent);
}
}
void SelfTest::pump(Stream &stream) {
if (!stream.connected || stream.kind != 'S') {
return;
}
while (stream.handed < stream.total + 1) {
const auto inClient = stream.handed - stream.probe->sentBytes.load();
if (inClient >= kClientBacklog) {
break;
}
const auto left = stream.total + 1 - stream.handed;
send(stream, (left >= kChunk) ? _filler : _filler.left(int(left)));
}
}
void SelfTest::ping() {
const auto stream = find(_pingId);
if (!stream || !stream->connected) {
return;
}
const auto now = crl::now();
if (now - _lastPing < kPingInterval) {
return;
}
_lastPing = now;
auto data = QByteArray(kPingSize, char(0));
memcpy(data.data(), &now, sizeof(now));
++_pingSequence;
memcpy(data.data() + 8, &_pingSequence, sizeof(_pingSequence));
send(*stream, data);
}
void SelfTest::tick() {
if (_phase == Phase::Done) {
return;
}
ping();
for (auto &stream : _streams) {
pump(stream);
}
const auto now = crl::now();
if (phaseDone() || (now - _phaseStart.at > kPhaseTimeout)) {
finishPhase();
}
}
bool SelfTest::phaseDone() {
if (_phase == Phase::Idle) {
return (crl::now() - _phaseStart.at >= kIdlePhase);
}
auto any = false;
auto all = true;
for (auto &stream : _streams) {
if (stream.kind == 'E' || stream.done) {
continue;
}
any = true;
const auto finished = (stream.kind == 'S')
? (stream.probe->sentBytes.load()
- stream.probe->unackedBytes.load() >= stream.total + 1)
: (stream.received >= stream.total);
if (!finished) {
all = false;
} else if (!stream.finishedAt) {
stream.finishedAt = crl::now();
}
}
return any && all;
}
SelfTest::Snapshot SelfTest::Take() {
const auto &bridge = Bridge();
return {
.at = crl::now(),
.upWrites = bridge.upWrites.load(),
.upBytes = bridge.upBytes.load(),
.upAckMs = bridge.upAckMsTotal.load(),
.downMessages = bridge.downMessages.load(),
.downBytes = bridge.downBytes.load(),
};
}
QString SelfTest::PhaseName(Phase phase) {
switch (phase) {
case Phase::Idle: return u"idle"_q;
case Phase::Upload: return u"upload"_q;
case Phase::Download: return u"download"_q;
case Phase::Mixed: return u"mixed"_q;
case Phase::Done: return u"done"_q;
}
return QString();
}
void SelfTest::beginPhase(Phase phase) {
_phase = phase;
_phaseStart = Take();
_pings.clear();
const auto up = (phase == Phase::Upload || phase == Phase::Mixed);
const auto down = (phase == Phase::Download || phase == Phase::Mixed);
if (up) {
for (auto i = 0; i != _config.upStreams; ++i) {
open('S', _config.up / _config.upStreams);
}
}
if (down) {
for (auto i = 0; i != _config.downStreams; ++i) {
open('D', _config.down / _config.downStreams);
}
}
}
void SelfTest::finishPhase() {
const auto end = Take();
const auto seconds = std::max(end.at - _phaseStart.at, crl::time(1))
/ 1000.;
auto up = int64();
auto down = int64();
auto upEnd = _phaseStart.at;
auto downEnd = _phaseStart.at;
for (auto &stream : _streams) {
if (stream.kind == 'E' || stream.done) {
continue;
}
const auto finished = stream.finishedAt
? stream.finishedAt
: end.at;
if (stream.kind == 'S') {
up += stream.probe->sentBytes.load()
- stream.probe->unackedBytes.load();
upEnd = std::max(upEnd, finished);
} else {
down += stream.received;
downEnd = std::max(downEnd, finished);
}
stream.done = true;
_transport->closeStream(stream.id);
}
auto pings = _pings;
ranges::sort(pings);
const auto percentile = [&](int percent) {
return pings.empty()
? crl::time(-1)
: pings[std::min(
int(pings.size()) - 1,
int(pings.size()) * percent / 100)];
};
const auto writes = end.upWrites - _phaseStart.upWrites;
const auto messages = end.downMessages - _phaseStart.downMessages;
const auto mb = [&](int64 bytes) {
return QString::number(bytes / seconds / (1024. * 1024.), 'f', 2);
};
const auto rate = [&](int64 bytes, crl::time till) {
const auto spent = std::max(till - _phaseStart.at, crl::time(1));
return QString::number(
bytes * 1000. / spent / (1024. * 1024.),
'f',
2);
};
LOG(("Web Proxy SelfTest: phase=%1 seconds=%2 up_MBps=%3 down_MBps=%4 "
"ping_n=%5 ping_p50=%6 ping_p95=%7 ping_max=%8 "
"bridge_up_writes=%9 bridge_up_avg_bytes=%10 bridge_up_ack_avg_ms=%11 "
"bridge_up_MBps=%12 bridge_down_msgs=%13 bridge_down_avg_bytes=%14 "
"bridge_down_MBps=%15"
).arg(PhaseName(_phase)
).arg(seconds, 0, 'f', 2
).arg(rate(up, upEnd)
).arg(rate(down, downEnd)
).arg(pings.size()
).arg(percentile(50)
).arg(percentile(95)
).arg(pings.empty() ? -1 : pings.back()
).arg(writes
).arg(writes ? ((end.upBytes - _phaseStart.upBytes) / writes) : 0
).arg(writes ? ((end.upAckMs - _phaseStart.upAckMs) / double(writes)) : 0., 0, 'f', 2
).arg(mb(end.upBytes - _phaseStart.upBytes)
).arg(messages
).arg(messages ? ((end.downBytes - _phaseStart.downBytes) / messages) : 0
).arg(mb(end.downBytes - _phaseStart.downBytes)));
switch (_phase) {
case Phase::Idle: beginPhase(Phase::Upload); break;
case Phase::Upload: beginPhase(Phase::Download); break;
case Phase::Download: beginPhase(Phase::Mixed); break;
case Phase::Mixed:
if (++_round < _config.nets.size()) {
beginRound();
break;
}
_phase = Phase::Done;
_timer->stop();
_transport->closeStream(_pingId);
LOG(("Web Proxy SelfTest: done."));
break;
case Phase::Done: break;
}
}
[[nodiscard]] rpl::lifetime &SelfTestLifetime() {
static auto result = rpl::lifetime();
return result;
}
} // namespace
std::optional<ProxyData> SelfTestProxy() {
const auto config = ParseConfig();
if (!config) {
return std::nullopt;
}
auto result = ProxyData();
result.type = ProxyData::Type::Web;
result.host = config->host;
result.port = 443;
result.password = config->secret;
return result.valid() ? std::make_optional(result) : std::nullopt;
}
void StartSelfTest() {
const auto config = ParseConfig();
if (!config) {
return;
}
const auto proxy = SelfTestProxy();
if (!proxy) {
LOG(("Web Proxy SelfTest: bad TDESKTOP_WEB_PROXY_SELFTEST value."));
return;
}
const auto launch = [config = *config] {
const auto transport = Transport::Instance();
if (!transport) {
return;
}
const auto test = new SelfTest(transport, config);
test->moveToThread(transport->thread());
InvokeQueued(test, [=] { test->start(); });
};
if (Transport::CurrentState(*proxy) == Transport::State::Connected) {
launch();
return;
}
Transport::StateChanges(
) | rpl::filter([=](const Transport::StateChange &change) {
return (change.proxy == *proxy)
&& (change.state == Transport::State::Connected);
}) | rpl::take(1) | rpl::on_next([=] {
launch();
}, SelfTestLifetime());
}
} // namespace MTP::WebProxy