ZaStoGram_desktop/Telegram/SourceFiles/mtproto/web_proxy/web_proxy_flow.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

393 lines
11 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_flow.h"
#include <algorithm>
namespace MTP::WebProxy {
namespace {
[[nodiscard]] std::size_t Index(StreamClass value) {
return std::size_t(value);
}
} // namespace
const char *StreamClassName(StreamClass value) {
switch (value) {
case StreamClass::Interactive: return "interactive";
case StreamClass::Download: return "download";
case StreamClass::Upload: return "upload";
}
return "interactive";
}
UplinkScheduler::UplinkScheduler(UplinkLimits limits)
: _limits(limits) {
}
void UplinkScheduler::add(uint32 streamId, StreamClass streamClass) {
remove(streamId);
_streams.emplace(streamId, Entry{ .streamClass = streamClass });
}
void UplinkScheduler::remove(uint32 streamId) {
const auto i = _streams.find(streamId);
if (i == end(_streams)) {
return;
}
if (i->second.streamClass == StreamClass::Upload) {
_uploadInFlight -= i->second.inFlight;
}
// A stale id may stay in its ready queue; popReady() skips it.
_streams.erase(i);
}
void UplinkScheduler::clear() {
_streams.clear();
for (auto &queue : _ready) {
queue.clear();
}
_uploadInFlight = 0;
_uploadSkipped = 0;
}
void UplinkScheduler::markReady(uint32 streamId) {
const auto i = _streams.find(streamId);
if (i == end(_streams) || i->second.ready) {
return;
}
i->second.ready = true;
_ready[Index(i->second.streamClass)].push_back(streamId);
}
bool UplinkScheduler::hasReady(StreamClass streamClass) {
auto &queue = _ready[Index(streamClass)];
while (!queue.empty()) {
const auto i = _streams.find(queue.front());
if (i != end(_streams)
&& i->second.ready
&& i->second.streamClass == streamClass) {
return true;
}
queue.pop_front();
}
return false;
}
std::optional<uint32> UplinkScheduler::popReady(StreamClass streamClass) {
if (!hasReady(streamClass)) {
return std::nullopt;
}
auto &queue = _ready[Index(streamClass)];
const auto streamId = queue.front();
queue.pop_front();
_streams[streamId].ready = false;
return streamId;
}
std::optional<UplinkGrant> UplinkScheduler::next() {
const auto uploadRoom = _limits.uploadInFlight - _uploadInFlight;
const auto uploadEligible = (uploadRoom > 0)
&& hasReady(StreamClass::Upload);
const auto preferUpload = uploadEligible
&& (_uploadSkipped >= _limits.priorityBurst);
const auto order = preferUpload
? std::array{
StreamClass::Upload,
StreamClass::Interactive,
StreamClass::Download,
}
: std::array{
StreamClass::Interactive,
StreamClass::Download,
StreamClass::Upload,
};
for (const auto streamClass : order) {
const auto upload = (streamClass == StreamClass::Upload);
if (upload && !uploadEligible) {
continue;
}
const auto streamId = popReady(streamClass);
if (!streamId) {
continue;
}
if (upload) {
_uploadSkipped = 0;
} else if (uploadEligible) {
++_uploadSkipped;
}
const auto uploadFrame = std::min<int64>(
_limits.frameSize,
std::max<int64>(
_limits.uploadInFlight / 4,
_limits.minUploadFrame));
const auto maxBytes = upload
? int(std::min(uploadFrame, uploadRoom))
: _limits.frameSize;
return UplinkGrant{ .streamId = *streamId, .maxBytes = maxBytes };
}
return std::nullopt;
}
void UplinkScheduler::sent(uint32 streamId, int bytes) {
const auto i = _streams.find(streamId);
if (i == end(_streams) || bytes <= 0) {
return;
}
i->second.inFlight += bytes;
if (i->second.streamClass == StreamClass::Upload) {
_uploadInFlight += bytes;
}
}
void UplinkScheduler::acknowledged(uint32 streamId, int64 bytes) {
const auto i = _streams.find(streamId);
if (i == end(_streams) || bytes <= 0) {
return;
}
const auto acked = std::min(i->second.inFlight, bytes);
i->second.inFlight -= acked;
if (i->second.streamClass == StreamClass::Upload) {
_uploadInFlight -= acked;
}
}
void UplinkScheduler::setUploadInFlight(int64 bytes) {
_limits.uploadInFlight = std::max(bytes, int64(_limits.frameSize));
}
UplinkStats UplinkScheduler::stats() const {
auto result = UplinkStats{ .uploadInFlight = _uploadInFlight };
for (const auto &[streamId, entry] : _streams) {
++result.streams[Index(entry.streamClass)];
if (entry.ready) {
++result.ready[Index(entry.streamClass)];
}
}
return result;
}
bool UplinkScheduler::uploadBlocked() const {
if (_uploadInFlight < _limits.uploadInFlight) {
return false;
}
return std::any_of(begin(_streams), end(_streams), [](const auto &pair) {
return pair.second.ready
&& pair.second.streamClass == StreamClass::Upload;
});
}
int64 DownlinkCreditTarget(
const DownlinkLimits &limits,
StreamClass streamClass,
int downloadStreams) {
if (streamClass != StreamClass::Download) {
return limits.streamWindow;
}
const auto share = limits.downloadBudget
/ std::max(downloadStreams, 1);
return std::min(
std::clamp(share, limits.downloadMin, limits.downloadMax),
limits.streamWindow);
}
int64 DownlinkCreditRelease(
int64 relayCredit,
int64 withheld,
int64 target) {
return std::clamp(target - relayCredit, int64(0), std::max(withheld, int64(0)));
}
AdaptiveWindow::AdaptiveWindow(AdaptiveWindowLimits limits)
: _limits(limits)
, _window(std::clamp(limits.initial, limits.min, limits.max))
, _target(_window) {
}
void AdaptiveWindow::reset() {
_base.clear();
_lastDelay = std::nullopt;
_window = std::clamp(_limits.initial, _limits.min, _limits.max);
_target = _window;
_rate = 0;
}
std::optional<int64> AdaptiveWindow::baseDelay() const {
if (_base.empty()) {
return std::nullopt;
}
auto result = _base.front().second;
for (const auto &[bucket, value] : _base) {
result = std::min(result, value);
}
return result;
}
void AdaptiveWindow::noteDelay(int64 now, std::optional<int64> delay) {
if (!delay || *delay < 0) {
return;
}
_lastDelay = delay;
const auto bucket = now / _limits.baseBucket;
if (!_base.empty() && _base.back().first == bucket) {
_base.back().second = std::min(_base.back().second, *delay);
} else {
_base.emplace_back(bucket, *delay);
}
const auto buckets = _limits.baseHistory / _limits.baseBucket;
while (!_base.empty() && bucket - _base.front().first >= buckets) {
_base.pop_front();
}
}
void AdaptiveWindow::hold(const FlowSample &sample) {
noteDelay(sample.now, sample.delay);
}
void AdaptiveWindow::update(const FlowSample &sample) {
if (sample.interval <= 0) {
return;
}
const auto now = sample.now;
if (!sample.probing) {
const auto rate = sample.bytes * 1000 / sample.interval;
_rate = _rate ? ((_rate + rate) / 2) : rate;
}
noteDelay(now, sample.delay);
if (!sample.windowLimited || sample.probing) {
// An idle or application-limited flow says nothing about the
// path; keep the window for the next burst. A drained one only
// says what the base is.
return;
}
const auto clampWindow = [&](int64 value) {
return std::clamp(value, _limits.min, _limits.max);
};
const auto base = baseDelay();
if (!base) {
// Nothing tells how much is queued: grow cautiously, never past
// blindMax.
if (_window < _limits.blindMax) {
_window = std::min(
clampWindow(_window * _limits.maxGrowthPercent / 100),
std::max(_limits.blindMax, _limits.min));
}
return;
}
// While the window is the limit, rate == window / loop round trip.
// Below the target the loop runs at the base round trip and the target
// comes out above the window; once a queue builds, the loop round trip
// grows past base + targetDelay and the target drops below it.
_target = clampWindow(_rate
* (*base + _limits.targetDelay + sample.extraDelay)
/ 1000);
if (_target > _window) {
_window = std::min(
_target,
clampWindow(_window * _limits.maxGrowthPercent / 100));
} else if (_target < _window) {
_window = std::max(
_target,
clampWindow(_window * _limits.shrinkPercent / 100));
++_shrinks;
}
}
const char *ReceiveReasonName(ReceiveReason reason) {
switch (reason) {
case ReceiveReason::StreamClosed: return "stream_closed";
case ReceiveReason::CarrierDown: return "carrier_down";
case ReceiveReason::MaxWait: return "max_wait";
case ReceiveReason::CarrierStalled: return "carrier_stalled";
case ReceiveReason::Queued: return "request_queued";
case ReceiveReason::ReplyQueued: return "reply_queued";
case ReceiveReason::ReplyPending: return "reply_pending";
case ReceiveReason::ReplyMissing: return "reply_missing";
case ReceiveReason::ReplyTimeout: return "reply_timeout";
}
return "unknown";
}
bool CarrierStalled(
int64 now,
const CarrierHealth &carrier,
const LivenessLimits &limits) {
if (!carrier.connected || carrier.unackedBytes <= 0) {
return false;
}
const auto progress = std::max({
carrier.outstandingSince,
carrier.lastCreditAt,
carrier.lastDownlinkAt,
});
return (now - progress) >= limits.carrierStall;
}
ReceiveDecision DecideReceiveWait(
int64 now,
int64 waitStartedAt,
const CarrierHealth &carrier,
const StreamHealth &stream,
const LivenessLimits &limits) {
const auto fail = [](ReceiveReason reason) {
return ReceiveDecision{
.verdict = ReceiveVerdict::Fail,
.reason = reason,
};
};
if (!stream.open) {
return fail(ReceiveReason::StreamClosed);
} else if (!carrier.connected) {
return fail(ReceiveReason::CarrierDown);
}
const auto waited = now - waitStartedAt;
if (waited >= limits.maxWait) {
return fail(ReceiveReason::MaxWait);
}
const auto left = limits.maxWait - waited;
const auto wait = [&](ReceiveReason reason, int64 delay) {
return ReceiveDecision{
.verdict = ReceiveVerdict::Wait,
.reason = reason,
.waitMore = std::clamp(delay, int64(1), left),
};
};
if (CarrierStalled(now, carrier, limits)) {
// The carrier watchdog tears every stream down at once; failing
// here first would only reopen streams on the stuck carrier.
return wait(ReceiveReason::CarrierStalled, limits.recheck);
} else if (stream.queuedBytes > 0 || stream.unackedBytes > 0) {
// The request itself has not reached the backend yet, and the
// carrier is moving (otherwise it would be stalled above).
return wait(ReceiveReason::Queued, limits.recheck);
}
const auto replyFrom = std::max({
stream.deliveredAt,
stream.lastReceivedAt,
waitStartedAt,
});
const auto silent = now - replyFrom;
if (silent >= limits.busyReply) {
return fail(ReceiveReason::ReplyTimeout);
}
const auto downlinkIdle = now - carrier.lastDownlinkAt;
if (downlinkIdle < limits.busyWindow) {
return wait(ReceiveReason::ReplyQueued, limits.recheck);
}
// Nothing is arriving at all, so nothing can be queued in front of the
// reply: count the time both the request and the downlink were idle.
const auto quiet = std::min(silent, downlinkIdle);
if (quiet >= limits.quietReply) {
return fail(ReceiveReason::ReplyMissing);
}
return wait(ReceiveReason::ReplyPending, limits.quietReply - quiet);
}
} // namespace MTP::WebProxy