Skip to content

Commit 893269c

Browse files
committed
feat: configurable parallel chunk transfers for faster downloads/uploads
Transfers were capped at ~5-7 MB/s because WTelegramClient requests 512KB file parts with only 2 requests in flight (its default since v4.x), making throughput latency-bound at roughly 1MB per round-trip to Telegram's data center. Add a ParallelTransfers setting (default 4, range 1-16) applied to the main client and, before each document download, to the media-DC client that WTelegram resolves internally - secondary DC clients do not inherit the main client's setting, so it must be propagated per instance. The applied value is tracked per client and adjusted by delta, which stays correct even while parts are in flight. Changes take effect on the next transfer without restarting.
1 parent 543e66f commit 893269c

3 files changed

Lines changed: 92 additions & 0 deletions

File tree

TelegramDownloader/Data/TelegramService.cs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ private void createDownloadFolder()
161161
private void newClient()
162162
{
163163
client = new WTelegram.Client(Convert.ToInt32(GeneralConfigStatic.tlconfig?.api_id ?? Environment.GetEnvironmentVariable("api_id")), GeneralConfigStatic.tlconfig?.hash_id ?? Environment.GetEnvironmentVariable("hash_id"), UserService.USERDATAFOLDER + "/WTelegram.session");
164+
ApplyConfiguredParallelTransfers(client);
164165
if (GeneralConfigStatic.config.ShouldShowLogInTerminal)
165166
{
166167
// WTelegram.Helpers.Log = (lvl, str) => Console.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss} [{"TDIWE!"[lvl]}] {str}");
@@ -171,6 +172,67 @@ private void newClient()
171172

172173
}
173174

175+
// WTelegramClient requests file parts through a per-client semaphore that
176+
// defaults to 2 parts in flight, capping transfers at ~1MB per round-trip.
177+
// Tracks the value applied to each client instance (main client and the
178+
// media-DC clients WTelegram creates internally, which do NOT inherit the
179+
// main client's ParallelTransfers).
180+
private static readonly ConditionalWeakTable<WTelegram.Client, StrongBox<int>> appliedParallelTransfers = new();
181+
private const int WTELEGRAM_DEFAULT_PARALLEL_TRANSFERS = 2;
182+
183+
public static int GetConfiguredParallelTransfers()
184+
{
185+
return Math.Clamp(GeneralConfigStatic.config?.ParallelTransfers ?? 4, 1, 16);
186+
}
187+
188+
private static void ApplyConfiguredParallelTransfers(WTelegram.Client c)
189+
{
190+
if (c == null)
191+
return;
192+
int desired = GetConfiguredParallelTransfers();
193+
int delta;
194+
lock (appliedParallelTransfers)
195+
{
196+
StrongBox<int> applied = appliedParallelTransfers.GetOrCreateValue(c);
197+
if (applied.Value == 0)
198+
applied.Value = WTELEGRAM_DEFAULT_PARALLEL_TRANSFERS;
199+
delta = desired - applied.Value;
200+
if (delta == 0)
201+
return;
202+
applied.Value = desired;
203+
}
204+
try
205+
{
206+
// The ParallelTransfers setter adjusts the semaphore relative to its
207+
// CURRENT count, which is lower while parts are in flight. Applying
208+
// our delta on top of the current value keeps the configured maximum
209+
// correct even if a transfer is running on this client.
210+
c.ParallelTransfers = c.ParallelTransfers + delta;
211+
}
212+
catch (Exception)
213+
{
214+
// Never let a tuning failure break a transfer.
215+
}
216+
}
217+
218+
/// <summary>
219+
/// Resolves the client instance that WTelegram's DownloadFileAsync will use
220+
/// for the given file DC (dc_id == 0 means the main client) and applies the
221+
/// configured chunk parallelism to it before the transfer starts.
222+
/// </summary>
223+
private async Task PrepareTransferClientAsync(int dcId)
224+
{
225+
try
226+
{
227+
WTelegram.Client c = dcId == 0 ? client : await client.GetClientForDC(-dcId, true);
228+
ApplyConfiguredParallelTransfers(c);
229+
}
230+
catch (Exception ex)
231+
{
232+
_logger.LogDebug(ex, "Could not prepare transfer client for DC {DcId}", dcId);
233+
}
234+
}
235+
174236
public async Task<User> CallQrGenerator(Action<string> func, CancellationToken ct, bool logoutFirst = false)
175237
{
176238
return await client.LoginWithQRCode(func, logoutFirst: logoutFirst, ct: ct);
@@ -701,6 +763,7 @@ public async Task<Message> uploadFile(string chatId, Stream file, string fileNam
701763

702764
try
703765
{
766+
ApplyConfiguredParallelTransfers(client);
704767
var inputFile = await client.UploadFileAsync(file, fileName, um.ProgressCallback);
705768
var result = await client.SendMediaAsync(peer, caption ?? fileName, inputFile, mimeType);
706769
_logger.LogInformation("File upload completed - FileName: {FileName}, MessageId: {MessageId}", fileName, result.id);
@@ -1205,6 +1268,7 @@ public async Task<Stream> DownloadFileAndReturn(ChatMessages message, Stream ms
12051268
model.name = filename;
12061269
_logger.LogInformation("Starting document download - FileName: {FileName}, Size: {SizeMB:F2}MB", filename, document.size / (1024.0 * 1024.0));
12071270
MemoryStream dest = new MemoryStream();
1271+
await PrepareTransferClientAsync(document.dc_id);
12081272
await client.DownloadFileAsync(document, ms ?? dest, (PhotoSizeBase)null, model.ProgressCallback);
12091273
_logger.LogInformation("Document download completed - FileName: {FileName}", filename);
12101274
return ms ?? dest;
@@ -1253,6 +1317,7 @@ public async Task<Stream> DownloadFileAndReturnWithOffset(ChatMessages message,
12531317
if (offset == 0)
12541318
{
12551319
MemoryStream dest = new MemoryStream();
1320+
await PrepareTransferClientAsync(document.dc_id);
12561321
await client.DownloadFileAsync(document, ms ?? dest, (PhotoSizeBase)null, model.ProgressCallback);
12571322
_logger.LogInformation("Document download completed - FileName: {FileName}", filename);
12581323
return ms ?? dest;
@@ -1365,6 +1430,7 @@ public async Task<string> DownloadFile(ChatMessages message, string fileName = n
13651430
_tis.addToDownloadList(model);
13661431
_logger.LogInformation("Starting file download to disk - FileName: {FileName}, Size: {SizeMB:F2}MB", filename, document.size / (1024.0 * 1024.0));
13671432
using var dest = new FileStream($"{Path.Combine(folder != null ? folder : Path.Combine(Environment.CurrentDirectory, "local", "temp"), filename)}", FileMode.Create, FileAccess.Write);
1433+
await PrepareTransferClientAsync(document.dc_id);
13681434
await client.DownloadFileAsync(document, dest, (PhotoSizeBase)null, model.ProgressCallback);
13691435
_logger.LogInformation("File download to disk completed - FileName: {FileName}", filename);
13701436
}

TelegramDownloader/Models/GeneralConfig.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,17 @@ public StreamingMode GetEffectiveStreamingMode()
178178
/// </summary>
179179
public int MemorySplitSizeGB { get; set; } = 2;
180180

181+
// Transfer Speed Settings
182+
/// <summary>
183+
/// Number of 512KB file chunks requested in parallel per transfer (1-16).
184+
/// WTelegramClient's default of 2 caps throughput at roughly 1MB per
185+
/// round-trip to Telegram's data center (~5-7 MB/s on typical latency).
186+
/// Higher values remove that latency bottleneck; the server-side speed
187+
/// limit for non-Premium accounts still applies. Takes effect on the
188+
/// next transfer, no restart needed.
189+
/// </summary>
190+
public int ParallelTransfers { get; set; } = 4;
191+
181192
}
182193

183194
public class TLConfig

TelegramDownloader/Pages/Config.razor

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,21 @@
230230
</div>
231231
</div>
232232

233+
<div class="config-item">
234+
<div class="config-item-info">
235+
<div class="config-item-label">
236+
<i class="bi bi-lightning-charge"></i>
237+
Parallel Chunk Transfers
238+
</div>
239+
<div class="config-item-description">
240+
Number of 512KB chunks requested in parallel per transfer (1-16). Higher values improve download/upload speed by removing the latency bottleneck; Premium accounts benefit the most. Applied on the next transfer.
241+
</div>
242+
</div>
243+
<div class="config-item-control">
244+
<NumberInput TValue="int" @bind-Value="Model!.ParallelTransfers" Min="1" Max="16" EnableMinMax="true" class="form-control" style="width: 80px;" />
245+
</div>
246+
</div>
247+
233248
<div class="config-item">
234249
<div class="config-item-info">
235250
<div class="config-item-label">

0 commit comments

Comments
 (0)